133 lines
3.8 KiB
Rust
133 lines
3.8 KiB
Rust
/*
|
|
* Copyright (c) 2023 Marceline Cramer
|
|
* Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU Affero General Public License as published by the Free
|
|
* Software Foundation, either version 3 of the License, or (at your option) any
|
|
* later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
* details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
*/
|
|
|
|
use std::{
|
|
env::args,
|
|
ffi::OsString,
|
|
fs::File,
|
|
io::{Read, Stdout, Write},
|
|
os::fd::FromRawFd,
|
|
path::Path,
|
|
};
|
|
|
|
use crossterm::{terminal, QueueableCommand, Result};
|
|
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
|
|
|
mod actions;
|
|
mod buffer;
|
|
mod config;
|
|
mod keybinds;
|
|
mod state;
|
|
mod theme;
|
|
|
|
#[derive(Copy, Clone, Debug, Default)]
|
|
pub struct Cursor {
|
|
pub column: usize,
|
|
pub line: usize,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum Direction {
|
|
Left,
|
|
Down,
|
|
Up,
|
|
Right,
|
|
}
|
|
|
|
fn screen_main(stdout: &mut Stdout, mut state: state::State) -> Result<()> {
|
|
let poll_timeout = std::time::Duration::from_millis(100);
|
|
while !state.quit {
|
|
state.draw(stdout)?;
|
|
if let true = crossterm::event::poll(poll_timeout)? {
|
|
let event = crossterm::event::read()?;
|
|
state.on_event(event);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let argv = args().collect::<Vec<String>>();
|
|
let mut buf = Vec::new();
|
|
let file_name: Option<OsString>;
|
|
|
|
match argv.get(1).map(|s| s.as_str()) {
|
|
Some("-") | None => {
|
|
file_name = None;
|
|
let stdin = 0; // get stdin as a file descriptor
|
|
if unsafe { libc::isatty(stdin) } == 0 {
|
|
unsafe { File::from_raw_fd(stdin) }
|
|
} else {
|
|
File::open("/dev/null")?
|
|
}
|
|
}
|
|
Some(path) => {
|
|
let input_file = Path::new(path);
|
|
file_name = Some(OsString::from(&path));
|
|
|
|
File::open(input_file).unwrap_or_else(|_| {
|
|
let mut err = String::new();
|
|
if !input_file.exists() {
|
|
err = "No such file or directory.".to_string();
|
|
} else if input_file.is_dir() {
|
|
err = "Is a directory.".to_string();
|
|
}
|
|
eprintln!("{}: {}: {}", argv[0], path, err);
|
|
exit(EX_UNAVAILABLE);
|
|
})
|
|
}
|
|
}
|
|
.read_to_end(&mut buf)?;
|
|
|
|
let text = String::from_utf8(buf).unwrap_or_else(|_| {
|
|
eprintln!(
|
|
"{}: {}: File contents are not valid UTF-8.",
|
|
argv[0], argv[1]
|
|
);
|
|
exit(EX_DATAERR);
|
|
});
|
|
|
|
let state = state::State::from_str(file_name, &text)?;
|
|
|
|
// begin to enter alternate screen
|
|
let mut stdout = std::io::stdout();
|
|
terminal::enable_raw_mode()?;
|
|
stdout.queue(terminal::EnterAlternateScreen)?;
|
|
|
|
// register a panic handler to exit the alternate screen
|
|
let old_panic_handler = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
let mut stdout = std::io::stdout();
|
|
let _ = stdout.queue(terminal::LeaveAlternateScreen);
|
|
let _ = terminal::disable_raw_mode();
|
|
let _ = stdout.flush();
|
|
old_panic_handler(info)
|
|
}));
|
|
|
|
// run inner event loop
|
|
let result = screen_main(&mut stdout, state);
|
|
|
|
// exit alternate screen
|
|
stdout.queue(terminal::LeaveAlternateScreen)?;
|
|
terminal::disable_raw_mode()?;
|
|
|
|
result
|
|
}
|