diff --git a/Cargo.lock b/Cargo.lock index 6ec81a7..c8870b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "autocfg" version = "1.3.0" @@ -172,8 +178,10 @@ dependencies = [ name = "dirbuilder" version = "0.1.0" dependencies = [ + "anyhow", "clap", "crossterm 0.28.1", + "gag", "tui", ] @@ -187,6 +195,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + +[[package]] +name = "gag" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a713bee13966e9fbffdf7193af71d54a6b35a0bb34997cd6c9519ebeb5005972" +dependencies = [ + "filedescriptor", + "tempfile", +] + [[package]] name = "heck" version = "0.5.0" @@ -258,6 +293,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -281,6 +325,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -381,6 +431,39 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tui" version = "0.19.0" @@ -464,6 +547,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index f93698d..3b73331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.89" clap = { version = "4.5.19", features = ["derive"] } crossterm = "0.28.1" +gag = "1.0.0" tui = "0.19.0" [lints.clippy] diff --git a/src/command.rs b/src/command.rs index 297c3a7..5a4961b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,3 +1,5 @@ +use std::io::Error; + use tui::widgets::{Block, Borders, Paragraph}; use crate::pane::prelude::*; @@ -16,12 +18,12 @@ impl Command { impl Pane for Command { fn cursor_position(&self) -> Option<(u16, u16)> { let x = u16::try_from(self.buf.len()) - .expect("too big to display"); + .unwrap_or(u16::MAX); let y = 0; Some((x, y)) } - fn lines_hint(&self) -> usize { + fn lines_hint(&self) -> u16 { 1 } @@ -32,13 +34,15 @@ impl Pane for Command { output.render_widget(widget, area); } - fn update(&mut self, key: KeyCode) -> Message { - match key { + fn update(&mut self, key: KeyCode) -> Result { + let result = match key { KeyCode::Char(c) => { self.buf.push(c); Message::Nothing }, KeyCode::Backspace => { self.buf.pop(); Message::Nothing }, KeyCode::Esc => Message::CancelCommand, - KeyCode::Enter => Message::ExecuteCommand, + KeyCode::Enter if !self.buf.is_empty() => Message::ExecuteCommand, _ => Message::Nothing, - } + }; + + Ok(result) } } \ No newline at end of file diff --git a/src/command_output.rs b/src/command_output.rs index fa6e203..b9fbd3c 100644 --- a/src/command_output.rs +++ b/src/command_output.rs @@ -26,9 +26,9 @@ impl Pane for CommandOutput { None } - fn lines_hint(&self) -> usize { + fn lines_hint(&self) -> u16 { match self.contents.as_ref() { - Ok(Some(s)) => s.lines().count(), + Ok(Some(s)) => s.lines().count().try_into().unwrap_or(u16::MAX), Ok(None) => 0, Err(_) => 1, } @@ -44,7 +44,7 @@ impl Pane for CommandOutput { output.render_widget(widget, area); } - fn update(&mut self, _: KeyCode) -> Message { + fn update(&mut self, _: KeyCode) -> Result { unimplemented!("Output does not receive input") } } \ No newline at end of file diff --git a/src/directory.rs b/src/directory.rs index 1809b17..0fda929 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -114,7 +114,7 @@ impl Directory { self.selection as usize } - pub fn update_navigation(&mut self, key: KeyCode) -> Message { + pub fn update_navigation(&mut self, key: KeyCode) -> Result { match key { KeyCode::Char(c) => { match c { @@ -127,7 +127,7 @@ impl Directory { self.editing = true; }, 'x' | 'X' => { - return Message::BeginCommand; + return Ok(Message::BeginCommand); } _ => (), }; @@ -138,9 +138,9 @@ impl Directory { self.src_name = Some(self.path()); self.editing = true; }, - KeyCode::Esc => return Message::Exit, + KeyCode::Esc => return Ok(Message::Exit), KeyCode::Backspace if self.selected().depth > 0 => { - self.delete().unwrap(); + self.delete()?; if self.selected().is_file { self.dirs.remove(self.selection()); } else { @@ -159,16 +159,16 @@ impl Directory { _ => (), } - Message::Nothing + Ok(Message::Nothing) } - pub fn update_edit(&mut self, key: KeyCode) -> Message { + pub fn update_edit(&mut self, key: KeyCode) -> Result { match key { KeyCode::Char(c) => self.selected_mut().name.push(c), KeyCode::Enter if !self.selected().name.is_empty() => { - self.create().unwrap(); + self.create()?; if let Some(src) = self.src_name.take() { - rename(src, self.path()).unwrap(); + rename(src, self.path())?; } self.editing = false; }, @@ -178,7 +178,7 @@ impl Directory { .and_then(|s| s.to_str()) .map_or(String::new(), ToOwned::to_owned); self.editing = false; - } else if self.selected().name.len() > 0 { + } else if ! self.selected().name.is_empty() { self.editing = false; } } @@ -188,7 +188,7 @@ impl Directory { _ => (), } - Message::Nothing + Ok(Message::Nothing) } } @@ -201,16 +201,18 @@ impl Pane for Directory { let x = u16::from(self.selected().depth) * 4 + u16::try_from(self.selected().name.len()) - .expect("name too long to display"); + .unwrap_or(u16::MAX); let y = self.selection; Some((x, y)) } - fn lines_hint(&self) -> usize { + fn lines_hint(&self) -> u16 { self.dirs.len() + .try_into() + .unwrap_or(u16::MAX) } - fn update(&mut self, key: KeyCode) -> Message { + fn update(&mut self, key: KeyCode) -> Result { if self.editing { self.update_edit(key) } else { diff --git a/src/main.rs b/src/main.rs index 5a08c6e..17d4225 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,12 +13,36 @@ use state::State; use terminal::Terminal; use pane::prelude::*; +use std::fs::{remove_file, OpenOptions}; use std::io::Error; +use anyhow::{Result as AnyResult, Error as AnyError}; use clap::Parser; use crossterm::event::{read, Event, KeyEvent}; +use gag::Redirect; -fn main() -> Result<(), Error> { +const LOG_FILE: &str = "dirbuilder.log"; +const LOG_EXISTS_MSG: &str = "the log file exists, implying dirbuider didn't exit cleanly last time. consult the log and delete it before rerunning."; + +fn main() -> AnyResult<()> { + let target = OpenOptions::new() + .write(true) + .create_new(true) + .open(LOG_FILE) + .map_err(|e| { + >::into(e).context(LOG_FILE).context(LOG_EXISTS_MSG) + })?; + + let _redirect = Redirect::stderr(target)?; + + do_tui()?; + + remove_file(LOG_FILE)?; + + Ok(()) +} + +fn do_tui() -> Result<(), Error> { let args = Args::parse(); let mut terminal = Terminal::new()?; @@ -39,7 +63,7 @@ fn main() -> Result<(), Error> { let Event::Key(KeyEvent { code: key, .. }) = read()? else { continue }; - match state.update(key) { + match state.update(key)? { Message::Nothing => (), Message::Exit => break, Message::BeginCommand => state.begin_command(), diff --git a/src/pane.rs b/src/pane.rs index 674b28c..7088a08 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1,4 +1,4 @@ -use std::io::Stdout; +use std::io::{Error, Stdout}; use crossterm::event::KeyCode; use tui::{layout::Rect, Frame}; @@ -7,11 +7,11 @@ use tui::backend::CrosstermBackend; pub trait Pane { fn cursor_position(&self) -> Option<(u16, u16)>; - fn lines_hint(&self) -> usize; + fn lines_hint(&self) -> u16; fn display(&self, output: OutputSink, area: Rect); - fn update(&mut self, key: KeyCode) -> Message; + fn update(&mut self, key: KeyCode) -> Result; } pub type OutputSink<'a, 'b> = &'a mut Frame<'b, CrosstermBackend>; diff --git a/src/state.rs b/src/state.rs index 8e0ab57..0e6435c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -32,21 +32,21 @@ impl State { } pub fn render(&self, terminal: &mut Terminal) -> Result<(), Error> { - let command_size = match &self.command { - None => 0, - Some(e) => match e { + let command_size = self.command.as_ref().map_or( + 0, + |e| match e { Either::Cmd(c) => c.lines_hint(), Either::CmdOut(o) => o.lines_hint(), } - }; + ); let (dir_area, bottom_area) = if command_size > 0 { (Rect { - height: self.size.height - command_size as u16 - 2, + height: self.size.height - command_size - 2, ..self.size }, Rect { - y: self.size.height - command_size as u16 - 2, - height: command_size as u16 + 2, + y: self.size.height - command_size - 2, + height: command_size + 2, ..self.size }) } else { @@ -84,7 +84,7 @@ impl State { let (x, y) = pane.cursor_position()?; let new_x = x + 1; let dy = if let Some(Either::Cmd(c)) = &self.command { - self.size.height - c.lines_hint() as u16 - 2 + self.size.height - c.lines_hint() - 2 } else { self.size.y }; @@ -93,12 +93,12 @@ impl State { Some((new_x, new_y)) } - pub fn update(&mut self, key: KeyCode) -> Message { + pub fn update(&mut self, key: KeyCode) -> Result { match self.command.as_mut() { None => self.dir.update(key), Some(either) => match either { Either::Cmd(c) => c.update(key), - Either::CmdOut(_) => { self.close_window(); Message::Nothing }, + Either::CmdOut(_) => { self.close_window(); Ok(Message::Nothing) }, } } } @@ -115,7 +115,8 @@ impl State { .split(' ') .filter(|s| !s.is_empty()); - let prog_name = iter.next().unwrap(); + let prog_name = iter.next() + .expect("must provide a program name"); let output = Executable::new(prog_name) .args(iter) .arg(self.dir.path())