From e1ef2d9a1828b589a53ea059dc883896a659d2cd Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 2 Oct 2024 12:28:14 -0400 Subject: [PATCH] half a step forward for organisation --- src/directory.rs | 55 ++++++++++++++------ src/directory/item.rs | 8 +-- src/main.rs | 114 ++++++++---------------------------------- src/pane.rs | 14 ++++-- src/state.rs | 40 +++++++++++++++ src/terminal.rs | 54 ++++++++++++++++++++ 6 files changed, 169 insertions(+), 116 deletions(-) create mode 100644 src/state.rs create mode 100644 src/terminal.rs diff --git a/src/directory.rs b/src/directory.rs index 23f8a3c..909de40 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -2,8 +2,9 @@ mod display; mod item; use crossterm::event::KeyCode; +use tui::{layout::Rect, style::{Color, Style}, widgets::{Block, Borders, List, ListItem}}; -use crate::pane::Pane; +use crate::pane::{Message, OutputSink, Pane}; use display::TreePart; use item::Item; @@ -60,10 +61,8 @@ impl Directory { 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 { + pub fn update_navigation(&mut self, key: KeyCode) -> Message { match key { KeyCode::Char(c) => { match c { @@ -77,15 +76,18 @@ impl Directory { self.select_next(); self.editing = true; }, + 'x' | 'X' => { + return Message::BeginCommand; + } _ => (), }; }, KeyCode::Up => self.select_prev(), KeyCode::Down => self.select_next(), KeyCode::Enter => { - self.editing = true + self.editing = true; }, - KeyCode::Esc => return false, + KeyCode::Esc => return Message::Exit, KeyCode::Backspace => { if self.selected().is_file { self.dirs.remove(self.selection()); @@ -97,10 +99,10 @@ impl Directory { _ => (), } - true + Message::Nothing } - pub fn update_edit(&mut self, key: KeyCode) -> bool { + pub fn update_edit(&mut self, key: KeyCode) -> Message { match key { KeyCode::Char(c) => self.selected_mut().name.push(c), KeyCode::Enter | KeyCode::Esc if !self.selected().name.is_empty() => { @@ -110,14 +112,12 @@ impl Directory { _ => (), } - true + Message::Nothing } } impl Pane for Directory { - const NAME: &'static str = "Dirbuilder"; - fn cursor_position(&self) -> Option<(u16, u16)> { if !self.editing { return None; @@ -135,7 +135,7 @@ impl Pane for Directory { self.dirs.len() } - fn update(&mut self, key: KeyCode) -> bool { + fn update(&mut self, key: KeyCode) -> Message { if self.editing { self.update_edit(key) } else { @@ -143,11 +143,11 @@ impl Pane for Directory { } } - fn display(&self) -> String { + fn display(&self, output: OutputSink, area: Rect) { let mut iter = self.dirs.iter().enumerate(); let Some((_, init)) = iter.by_ref() .next() - else { return String::new() }; + else { return }; let mut indent_parts = Vec::new(); let mut result = init.name.clone() + "/\n"; @@ -193,7 +193,24 @@ impl Pane for Directory { result.push('\n'); } - result + let rows = result + .lines() + .enumerate() + .map(|(i, s)| { + let row = ListItem::new(s); + + if i == self.selection() { + let color = if self.editing { Color::Rgb(197, 110, 31) } else { Color::White }; + row.style(Style::default().fg(Color::Black).bg(color)) + } else { + row + } + }) + .collect(); + + let list = into_list(rows); + + output.render_widget(list, area); } } @@ -205,4 +222,12 @@ fn is_last_at_this_depth(slice: &[Item], target_depth: u8) -> bool { } true +} + +fn into_list(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())) } \ No newline at end of file diff --git a/src/directory/item.rs b/src/directory/item.rs index 3f7f4e9..d209a2c 100644 --- a/src/directory/item.rs +++ b/src/directory/item.rs @@ -1,10 +1,10 @@ #[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, + pub(super) name: String, + pub(super) depth: u8, + pub(super) expanded: bool, + pub(super) is_file: bool, } impl Item { diff --git a/src/main.rs b/src/main.rs index cf69ea5..09c627a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,114 +1,42 @@ mod directory; mod pane; +mod terminal; +mod state; + +use pane::Message; +use terminal::Terminal; +use state::State; + +use std::io::Error; -use crossterm::cursor::Show; 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::style::{Color, Style}; -use tui::widgets::{Block, Borders, List, ListItem}; -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 terminal = Terminal::new()?; + let area = terminal.size()?; - let mut dir = Directory::new("root".into()); + let mut state = State::new(); loop { - let diagramme = dir.display(); + state.render(&mut terminal, area)?; - let rows = diagramme - .lines() - .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); - - terminal.draw( - |f| { - f.render_widget(widget, size); - } - )?; - - if let Some((x, y)) = dir.cursor_position() { + if let Some((x, y)) = state.cursor_position() { terminal.show_cursor()?; terminal.set_cursor(x, y)?; } else { terminal.hide_cursor()?; } - let event = read()?; + let Event::Key(KeyEvent { code: key, .. }) = read()? + else { continue }; - let Event::Key(KeyEvent { code: key, .. }) = event else { continue }; - - let keep_going = dir.update(key); - if !keep_going { break } + match state.update(key) { + Message::Nothing => (), + Message::Exit => break, + Message::BeginCommand => todo!("begin command"), + Message::ExecuteCommand => todo!("execute command"), + } } 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/pane.rs b/src/pane.rs index 2057864..147f5ce 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1,18 +1,24 @@ +use std::io::Stdout; + use crossterm::event::KeyCode; +use tui::{backend::CrosstermBackend, layout::Rect, Frame}; pub trait Pane { - const NAME: &'static str; - fn cursor_position(&self) -> Option<(u16, u16)>; fn lines_hint(&self) -> usize; - fn display(&self) -> String; + fn display(&self, output: OutputSink, area: Rect); - fn update(&mut self, key: KeyCode) -> bool; + fn update(&mut self, key: KeyCode) -> Message; } +pub type OutputSink<'a, 'b> = &'a mut Frame<'b, CrosstermBackend>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Message { + Nothing, + Exit, BeginCommand, ExecuteCommand, } \ No newline at end of file diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..70c8cc4 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,40 @@ +use std::io::Error; + +use crossterm::event::KeyCode; +use tui::layout::Rect; + +use crate::pane::{Message, Pane}; +use crate::directory::Directory; +use crate::terminal::Terminal; + +pub struct State { + panes: Vec>, + selected: usize, +} +impl State { + pub fn new() -> Self { + Self { + panes: vec![ Box::new( Directory::new("root".into()) ) ], + selected: 0, + } + } + + pub fn render(&self, terminal: &mut Terminal, area: Rect) -> Result<(), Error> { + terminal.draw( + |output| + self.panes + .iter() + .for_each(|p| p.display(output, area)) + )?; + + Ok(()) + } + + pub fn cursor_position(&self) -> Option<(u16, u16)> { + self.panes[self.selected].cursor_position() + } + + pub fn update(&mut self, key: KeyCode) -> Message { + self.panes[self.selected].update(key) + } +} \ No newline at end of file diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..25a3c4f --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,54 @@ +use std::{ + io::{stdout, Error, Stdout}, + ops::{Deref, DerefMut}, +}; + +use crossterm::{ + cursor::Show, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen} +}; +use tui::{backend::CrosstermBackend, Terminal as TuiTerminal}; + +pub struct Terminal(TuiTerminal>); + +impl Terminal { + /// Create and initialise a `Terminal` + pub fn new() -> Result { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen, Show)?; + let backend = CrosstermBackend::new(stdout); + let terminal = TuiTerminal::new(backend)?; + Ok(Self(terminal)) + } +} + +// this is Drop::drop and not, say, an exit function +// so that if it panics or something, we can be decently +// confident that we returned to normal mode. +impl Drop for Terminal { + fn drop(&mut self) { + // have to ignore all these errors because + // Drop can't do anything useful with them + let _ = execute!( + self.0.backend_mut(), + LeaveAlternateScreen + ); + let _ = disable_raw_mode(); + let _ = self.0.show_cursor(); + } +} + +impl Deref for Terminal { + type Target = TuiTerminal>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Terminal { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} \ No newline at end of file