diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b8c5f2b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,431 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.2", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "dirbuilder" +version = "0.1.0" +dependencies = [ + "crossterm 0.28.1", + "tui", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "redox_syscall" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "mio 1.0.2", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags 1.3.2", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3207886 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dirbuilder" +version = "0.1.0" +edition = "2021" + +[dependencies] +crossterm = "0.28.1" +tui = "0.19.0" + +[lints.clippy] +enum_glob_use = 'deny' +pedantic = 'deny' +nursery = 'deny' +unwrap_used = 'deny' + +[profile.release] +opt-level = 'z' # size optimisation +lto = true # link time optimisation +codegen-units = 1 # fewer -> more optimisation +panic = 'abort' # abort on panic to avoid runtime +strip = 'symbols' # strip symbols, comment out if using perf diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7afca2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,122 @@ +// mod directory; +// mod pane; + +mod state; +use crossterm::cursor::Show; +use state::State; + +use std::io::{self, Error}; +use std::ops::{Deref, DerefMut}; +use tui::backend::CrosstermBackend; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use tui::Terminal; + +use crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; + +fn main() -> Result<(), Error> { + let mut terminal = MyTerminal::enter()?; + let size = terminal.size()?; + + let mut dir = State::new("root".into()); + + loop { + let (diagramme, bottom_buf) = dir.display(); + let bottom_height = u16::try_from(bottom_buf.lines().count()) + .expect("too many lines to display"); + + let rows = diagramme + .lines() + .take((size.height - bottom_height) as usize) + .enumerate() + .map(|(i, s)| { + let row = ListItem::new(s); + + if i == dir.selection() { + row.style(Style::default().fg(Color::Black).bg(if dir.is_editing() { Color::Rgb(197, 110, 31) } else { Color::White })) + } else { + row + } + }) + .collect(); + + let widget = table_vector(rows); + + let top_pane = Rect { + height: size.height - bottom_height-2, + ..size + }; + let bottom_pane = Rect { + y: top_pane.y + top_pane.height, + height: bottom_height+2, + ..top_pane + }; + + let bottom_widget = Paragraph::new(bottom_buf) + .block(Block::default().title("Output").borders(Borders::all())); + + terminal.draw( + |f| { + f.render_widget(widget, top_pane); + f.render_widget(bottom_widget, bottom_pane); + } + )?; + + dir.set_cursor(&mut terminal)?; + + let keep_going = dir.update()?; + if !keep_going { break } + } + + Ok(()) +} + +fn table_vector(rows: Vec>) -> List<'_> { + List::new(rows) + // You can set the style of the entire Table. + .style(Style::default().fg(Color::White)) + // As any other widget, a Table can be wrapped in a Block. + .block(Block::default().title("Dirbuilder").borders(Borders::all())) +} + +struct MyTerminal(Terminal>); + +impl MyTerminal { + /// Create and initialise a `Terminal` + fn enter() -> Result { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, Show)?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(Self(terminal)) + } +} + +impl Drop for MyTerminal { + fn drop(&mut self) { + let _ = disable_raw_mode(); + let _ = execute!( + self.0.backend_mut(), + LeaveAlternateScreen + ); + let _ = self.0.show_cursor(); + } +} + +impl Deref for MyTerminal { + type Target = Terminal>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for MyTerminal { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} \ No newline at end of file diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..ee4e544 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,289 @@ +use std::{io::Error, process::Command}; + +use crossterm::event::{read, Event, KeyCode, KeyEvent}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Mode { + #[default] + Default, + ChangingName, + GettingCommand, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct State { + dirs: Vec, + selection: u16, + mode: Mode, + cmd_buf: String, + cmd_out: String, +} +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Directory { + name: String, + depth: u8, + expanded: bool, + is_file: bool, +} +impl State { + pub fn new(root: String) -> Self { + Self { + dirs: vec![Directory::new(root, 0, false)], + selection: 0, + mode: Mode::Default, + cmd_buf: String::new(), + cmd_out: String::new(), + } + } + + pub fn select_prev(&mut self) { + if self.selection > 0 { + self.selection -= 1; + } + } + + pub fn select_next(&mut self) { + if self.selection()+1 < self.dirs.len() { + self.selection += 1; + } + } + + pub fn insert(&mut self, name: String, is_file: bool) { + let current_depth = self.selected().depth; + + let depth = if self.selected().is_file { + current_depth + } else { + current_depth + 1 + }; + + let new = Directory::new(name, depth, is_file); + let index = self.selection()+1; + self.dirs.insert(index, new); + } + + pub fn display(&self) -> (String, &str) { + let mut iter = self.dirs.iter().enumerate(); + let Some((_, init)) = iter.by_ref() + .next() + else { return (String::new(), "") }; + + let mut indent_parts = Vec::new(); + let mut result = init.name.clone() + "/\n"; + + for (i, current) in iter { + // first, determine if there is another item at this depth. + // that decides what art we use + let is_last_at_this_depth = is_last_at_this_depth(&self.dirs[i+1..], current.depth); + let (new_part, continue_part) = if is_last_at_this_depth { + (TreePart::Last, TreePart::Blank) + } else { + (TreePart::First, TreePart::Wait) + }; + + // setting indent_parts is important. + // indent_parts[i] is the indent for depth i, + // and it needs to be set to what we decided the `continue_part` + // needs to be. + // The if branch is for when we exceed the current depth and must + // add a new indent part. the else branch is when we already + // defined a branch at this point, and we do need to update it + // every time + // TODO: figure out why + if current.depth as usize > indent_parts.len() { + indent_parts.push(continue_part); + } else { + indent_parts[current.depth as usize-1] = continue_part; + } + + // I have inlined this to avoid making extra strings + // because I believe it is less efficient + indent_parts[0 .. current.depth as usize - 1] + .iter() + .copied() + .map(TreePart::display) + .for_each(|s| result += s); + + result.push_str(new_part.display()); + result.push_str(¤t.name); + if !current.is_file { + result.push('/'); + } + result.push('\n'); + } + + (result, &self.cmd_out) + } + + fn selected(&self) -> &Directory { + &self.dirs[self.selection()] + } + + fn selected_mut(&mut self) -> &mut Directory { + let idx = self.selection(); + &mut self.dirs[idx] + } + + pub const fn selection(&self) -> usize { + self.selection as usize + } + pub const fn is_editing(&self) -> bool { + matches!(self.mode, Mode::ChangingName) + } + + + pub fn set_cursor(&self, terminal: &mut super::MyTerminal) -> Result<(), Error> { + match self.mode { + Mode::Default => terminal.hide_cursor(), + Mode::ChangingName => { + terminal.show_cursor()?; + let x = + u16::from(self.selected().depth) * 4 + + u16::try_from(self.selected().name.len()) + .expect("name too long to display") + + 1; + let y = self.selection + 1; + terminal.set_cursor(x, y) + } + Mode::GettingCommand => { + terminal.show_cursor()?; + terminal.set_cursor(0, 0) + } + } + } + + pub fn update(&mut self) -> Result { + let Event::Key(KeyEvent { code, .. }) = read()? else { + return Ok(true); + }; + + match &mut self.mode { + Mode::Default => Ok(self.main_event(code)), + Mode::ChangingName => Ok(self.update_buffer(code)), + Mode::GettingCommand => self.update_command(code), + } + } + + pub fn main_event(&mut self, key: KeyCode) -> bool { + match key { + KeyCode::Char(c) => { + match c { + 'f' | 'F' => { + self.insert(String::new(), true); + self.select_next(); + self.mode = Mode::ChangingName; + }, + 'd' | 'D' => { + self.insert(String::new(), false); + self.select_next(); + self.mode = Mode::ChangingName; + }, + 'x' | 'X' => { + self.mode = Mode::GettingCommand; + } + _ => (), + }; + }, + KeyCode::Up => self.select_prev(), + KeyCode::Down => self.select_next(), + KeyCode::Enter => { + self.mode = Mode::ChangingName; + }, + KeyCode::Esc => return false, + KeyCode::Backspace => { + if self.selected().is_file { + let index = self.selection(); + self.dirs.remove(index); + if self.selection() == self.dirs.len() { + self.selection -= 1; + } + } + } + _ => (), + } + + true + } + + fn update_buffer(&mut self, key: KeyCode) -> bool { + match key { + KeyCode::Char(c) => self.selected_mut().name.push(c), + KeyCode::Enter | KeyCode::Esc if !self.selected().name.is_empty() => { + self.mode = Mode::Default; + }, + KeyCode::Backspace => { self.selected_mut().name.pop(); }, + _ => (), + } + + true + } + + fn update_command(&mut self, key: KeyCode) -> Result { + match key { + KeyCode::Char(c) => self.cmd_buf.push(c), + KeyCode::Backspace => { self.cmd_buf.pop(); }, + KeyCode::Enter => { + let mut args = self.cmd_buf.split(' '); + + let Some(program) = args.next() + else { return Ok(true) }; + + let out = Command::new(program) + .args(args) + .arg(self.selected().name.as_str()) + .output()? + .stdout; + + self.cmd_out = unsafe { String::from_utf8_unchecked(out) }; + self.mode = Mode::Default; + } + _ => (), + } + + Ok(true) + } +} + +fn is_last_at_this_depth(slice: &[Directory], target_depth: u8) -> bool { + for d in slice { + if d.depth < target_depth { return true } + + if d.depth == target_depth { return false } + } + + true +} + +impl Directory { + pub const fn new(name: String, depth: u8, is_file: bool) -> Self { + Self { + name, + depth, + expanded: true, + is_file, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TreePart { + /// `├──` + First, + /// `│ ` + Wait, + /// `└──` + Last, + /// (blank) + Blank +} +impl TreePart { + /// convert to ascii art + pub const fn display(self) -> &'static str { + match self { + Self::First => "├── ", + Self::Wait => "│ ", + Self::Last => "└── ", + Self::Blank => " ", + } + } +} \ No newline at end of file