diff --git a/src/state.rs b/src/directory.rs similarity index 50% rename from src/state.rs rename to src/directory.rs index ee4e544..23f8a3c 100644 --- a/src/state.rs +++ b/src/directory.rs @@ -1,38 +1,25 @@ -use std::{io::Error, process::Command}; +mod display; +mod item; -use crossterm::event::{read, Event, KeyCode, KeyEvent}; +use crossterm::event::KeyCode; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum Mode { - #[default] - Default, - ChangingName, - GettingCommand, -} +use crate::pane::Pane; + +use display::TreePart; +use item::Item; -#[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, + dirs: Vec, + selection: u16, + editing: bool, } -impl State { +impl Directory { pub fn new(root: String) -> Self { Self { - dirs: vec![Directory::new(root, 0, false)], + dirs: vec![Item::new(root, 0, false)], selection: 0, - mode: Mode::Default, - cmd_buf: String::new(), - cmd_out: String::new(), + editing: false, } } @@ -57,16 +44,110 @@ impl State { current_depth + 1 }; - let new = Directory::new(name, depth, is_file); - let index = self.selection()+1; - self.dirs.insert(index, new); + let new = Item::new(name, depth, is_file); + self.dirs.insert(self.selection()+1, new); } - pub fn display(&self) -> (String, &str) { + fn selected(&self) -> &Item { + &self.dirs[self.selection()] + } + + fn selected_mut(&mut self) -> &mut Item { + let idx = self.selection(); + &mut self.dirs[idx] + } + + pub const fn selection(&self) -> usize { + self.selection as usize + } + + pub fn is_editing(&self) -> bool { self.editing } + + pub fn update_navigation(&mut self, key: KeyCode) -> bool { + match key { + KeyCode::Char(c) => { + match c { + 'f' | 'F' => { + self.insert(String::new(), true); + self.select_next(); + self.editing = true; + }, + 'd' | 'D' => { + self.insert(String::new(), false); + self.select_next(); + self.editing = true; + }, + _ => (), + }; + }, + KeyCode::Up => self.select_prev(), + KeyCode::Down => self.select_next(), + KeyCode::Enter => { + self.editing = true + }, + KeyCode::Esc => return false, + KeyCode::Backspace => { + if self.selected().is_file { + self.dirs.remove(self.selection()); + if self.selection() == self.dirs.len() { + self.selection -= 1; + } + } + } + _ => (), + } + + true + } + + pub fn update_edit(&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.editing = false; + }, + KeyCode::Backspace => { self.selected_mut().name.pop(); }, + _ => (), + } + + true + } + +} + +impl Pane for Directory { + const NAME: &'static str = "Dirbuilder"; + + fn cursor_position(&self) -> Option<(u16, u16)> { + if !self.editing { + return None; + } + + 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; + Some((x, y)) + } + + fn lines_hint(&self) -> usize { + self.dirs.len() + } + + fn update(&mut self, key: KeyCode) -> bool { + if self.editing { + self.update_edit(key) + } else { + self.update_navigation(key) + } + } + + fn display(&self) -> String { let mut iter = self.dirs.iter().enumerate(); let Some((_, init)) = iter.by_ref() .next() - else { return (String::new(), "") }; + else { return String::new() }; let mut indent_parts = Vec::new(); let mut result = init.name.clone() + "/\n"; @@ -111,140 +192,12 @@ impl State { } 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) + result } } -fn is_last_at_this_depth(slice: &[Directory], target_depth: u8) -> bool { +fn is_last_at_this_depth(slice: &[Item], target_depth: u8) -> bool { for d in slice { if d.depth < target_depth { return true } @@ -252,38 +205,4 @@ fn is_last_at_this_depth(slice: &[Directory], target_depth: u8) -> bool { } 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 diff --git a/src/directory/display.rs b/src/directory/display.rs new file mode 100644 index 0000000..c426b76 --- /dev/null +++ b/src/directory/display.rs @@ -0,0 +1,22 @@ +#[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 diff --git a/src/directory/item.rs b/src/directory/item.rs new file mode 100644 index 0000000..3f7f4e9 --- /dev/null +++ b/src/directory/item.rs @@ -0,0 +1,19 @@ + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Item { + pub(crate) name: String, + pub(crate) depth: u8, + pub(crate) expanded: bool, + pub(crate) is_file: bool, +} + +impl Item { + pub const fn new(name: String, depth: u8, is_file: bool) -> Self { + Self { + name, + depth, + expanded: true, + is_file, + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7afca2..cf69ea5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ -// mod directory; -// mod pane; +mod directory; +mod pane; -mod state; use crossterm::cursor::Show; -use state::State; +use crossterm::event::{read, Event, KeyEvent}; +use pane::Pane; +use directory::Directory; 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::widgets::{Block, Borders, List, ListItem}; use tui::Terminal; use crossterm::{ @@ -22,16 +22,13 @@ fn main() -> Result<(), Error> { let mut terminal = MyTerminal::enter()?; let size = terminal.size()?; - let mut dir = State::new("root".into()); + let mut dir = Directory::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 diagramme = dir.display(); let rows = diagramme .lines() - .take((size.height - bottom_height) as usize) .enumerate() .map(|(i, s)| { let row = ListItem::new(s); @@ -46,29 +43,24 @@ fn main() -> Result<(), Error> { 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); + f.render_widget(widget, size); } )?; - dir.set_cursor(&mut terminal)?; + if let Some((x, y)) = dir.cursor_position() { + terminal.show_cursor()?; + terminal.set_cursor(x, y)?; + } else { + terminal.hide_cursor()?; + } - let keep_going = dir.update()?; + let event = read()?; + + let Event::Key(KeyEvent { code: key, .. }) = event else { continue }; + + let keep_going = dir.update(key); if !keep_going { break } } diff --git a/src/pane.rs b/src/pane.rs new file mode 100644 index 0000000..2057864 --- /dev/null +++ b/src/pane.rs @@ -0,0 +1,18 @@ +use crossterm::event::KeyCode; + +pub trait Pane { + const NAME: &'static str; + + fn cursor_position(&self) -> Option<(u16, u16)>; + + fn lines_hint(&self) -> usize; + + fn display(&self) -> String; + + fn update(&mut self, key: KeyCode) -> bool; +} + +pub enum Message { + BeginCommand, + ExecuteCommand, +} \ No newline at end of file