From 4f9b52151ce780ba8f864e798b85a200777116e8 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 2 Oct 2024 14:56:07 -0400 Subject: [PATCH] commands work --- src/command.rs | 44 +++++++++++++++ src/command_output.rs | 50 +++++++++++++++++ src/directory.rs | 12 ++--- src/main.rs | 17 +++--- src/pane.rs | 14 ++++- src/state.rs | 123 +++++++++++++++++++++++++++++++++++++----- 6 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 src/command.rs create mode 100644 src/command_output.rs diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..297c3a7 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,44 @@ +use tui::widgets::{Block, Borders, Paragraph}; + +use crate::pane::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Command { + buf: String +} +impl Command { + pub const fn new() -> Self { + Self { buf: String::new() } + } + + pub fn buf(&self) -> &str { &self.buf } +} +impl Pane for Command { + fn cursor_position(&self) -> Option<(u16, u16)> { + let x = u16::try_from(self.buf.len()) + .expect("too big to display"); + let y = 0; + Some((x, y)) + } + + fn lines_hint(&self) -> usize { + 1 + } + + fn display(&self, output: OutputSink, area: Rect) { + let widget = Paragraph::new(self.buf.as_str()) + .block(Block::default().title("Command").borders(Borders::all())); + + output.render_widget(widget, area); + } + + fn update(&mut self, key: KeyCode) -> Message { + 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, + _ => Message::Nothing, + } + } +} \ No newline at end of file diff --git a/src/command_output.rs b/src/command_output.rs new file mode 100644 index 0000000..fa6e203 --- /dev/null +++ b/src/command_output.rs @@ -0,0 +1,50 @@ +use std::io::Error; + +use tui::widgets::{Block, Borders, Paragraph}; + +use crate::pane::prelude::*; + +pub struct CommandOutput { + contents: Result, Error>, +} +impl CommandOutput { + pub fn new(src: Result, Error>) -> Self { + Self { + contents: src.map(|s| String::from_utf8(s).ok()) + } + } + + pub fn should_display(&self) -> bool { + match &self.contents { + Ok(Some(s)) => ! s.is_empty(), + _ => true, + } + } +} +impl Pane for CommandOutput { + fn cursor_position(&self) -> Option<(u16, u16)> { + None + } + + fn lines_hint(&self) -> usize { + match self.contents.as_ref() { + Ok(Some(s)) => s.lines().count(), + Ok(None) => 0, + Err(_) => 1, + } + } + + fn display(&self, output: OutputSink, area: Rect) { + let widget = match self.contents.as_ref() { + Ok(None) => Paragraph::new(""), + Ok(Some(s)) => Paragraph::new(s.as_str()), + Err(e) => Paragraph::new(e.to_string()), + }.block(Block::default().title("Output").borders(Borders::all())); + + output.render_widget(widget, area); + } + + fn update(&mut self, _: KeyCode) -> Message { + unimplemented!("Output does not receive input") + } +} \ No newline at end of file diff --git a/src/directory.rs b/src/directory.rs index 909de40..7cb9d51 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -1,10 +1,11 @@ mod display; mod item; -use crossterm::event::KeyCode; -use tui::{layout::Rect, style::{Color, Style}, widgets::{Block, Borders, List, ListItem}}; +use tui::widgets::{Block, Borders, List, ListItem}; +use tui::style::{Color, Style}; +use tui::layout::Rect; -use crate::pane::{Message, OutputSink, Pane}; +use crate::pane::prelude::*; use display::TreePart; use item::Item; @@ -125,9 +126,8 @@ 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") - + 1; - let y = self.selection + 1; + .expect("name too long to display"); + let y = self.selection; Some((x, y)) } diff --git a/src/main.rs b/src/main.rs index 09c627a..b217f9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ +mod command; +mod command_output; mod directory; mod pane; -mod terminal; mod state; +mod terminal; -use pane::Message; -use terminal::Terminal; use state::State; +use terminal::Terminal; +use pane::prelude::*; use std::io::Error; @@ -15,10 +17,10 @@ fn main() -> Result<(), Error> { let mut terminal = Terminal::new()?; let area = terminal.size()?; - let mut state = State::new(); + let mut state = State::new(area); loop { - state.render(&mut terminal, area)?; + state.render(&mut terminal)?; if let Some((x, y)) = state.cursor_position() { terminal.show_cursor()?; @@ -33,8 +35,9 @@ fn main() -> Result<(), Error> { match state.update(key) { Message::Nothing => (), Message::Exit => break, - Message::BeginCommand => todo!("begin command"), - Message::ExecuteCommand => todo!("execute command"), + Message::BeginCommand => state.begin_command(), + Message::ExecuteCommand => state.execute_command(), + Message::CancelCommand => state.cancel_command(), } } diff --git a/src/pane.rs b/src/pane.rs index 147f5ce..674b28c 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1,7 +1,8 @@ use std::io::Stdout; use crossterm::event::KeyCode; -use tui::{backend::CrosstermBackend, layout::Rect, Frame}; +use tui::{layout::Rect, Frame}; +use tui::backend::CrosstermBackend; pub trait Pane { fn cursor_position(&self) -> Option<(u16, u16)>; @@ -20,5 +21,16 @@ pub enum Message { Nothing, Exit, BeginCommand, + CancelCommand, ExecuteCommand, +} + +pub mod prelude { + pub use super::{ + OutputSink, + Message, + Pane, + }; + pub use tui::layout::Rect; + pub use crossterm::event::KeyCode; } \ No newline at end of file diff --git a/src/state.rs b/src/state.rs index 70c8cc4..ae5504c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,40 +1,137 @@ +use std::hint::unreachable_unchecked; use std::io::Error; +use std::process::Command as Executable; use crossterm::event::KeyCode; use tui::layout::Rect; -use crate::pane::{Message, Pane}; +use crate::command::Command; +use crate::command_output::CommandOutput; use crate::directory::Directory; +use crate::pane::{Message, Pane}; use crate::terminal::Terminal; +enum Either { + Cmd(T), + CmdOut(U), +} + pub struct State { - panes: Vec>, - selected: usize, + dir: Directory, + size: Rect, + command: Option> } impl State { - pub fn new() -> Self { + pub fn new(area: Rect) -> Self { + let dir = Directory::new("root".into()); Self { - panes: vec![ Box::new( Directory::new("root".into()) ) ], - selected: 0, + dir, + size: area, + command: None, } } - pub fn render(&self, terminal: &mut Terminal, area: Rect) -> Result<(), Error> { + pub fn render(&self, terminal: &mut Terminal) -> Result<(), Error> { + let command_size = match &self.command { + None => 0, + Some(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, + ..self.size + }, Rect { + y: self.size.height - command_size as u16 - 2, + height: command_size as u16 + 2, + ..self.size + }) + } else { + (self.size, self.size) + }; + terminal.draw( - |output| - self.panes - .iter() - .for_each(|p| p.display(output, area)) + |output| { + self.dir.display(output, dir_area); + if let Some(command) = self.command.as_ref() { + match command { + Either::Cmd(cmd) => cmd.display(output, bottom_area), + Either::CmdOut(cmd) => cmd.display(output, bottom_area), + } + } + } )?; Ok(()) } + const fn current_pane(&self) -> &dyn Pane { + match self.command.as_ref() { + None => &self.dir, + Some(either) => match either { + Either::Cmd(c) => c, + Either::CmdOut(o) => o, + } + } + } + pub fn cursor_position(&self) -> Option<(u16, u16)> { - self.panes[self.selected].cursor_position() + let pane = self.current_pane(); + + 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 + } else { + self.size.y + }; + let new_y = y + dy + 1; + + Some((new_x, new_y)) } pub fn update(&mut self, key: KeyCode) -> Message { - self.panes[self.selected].update(key) + 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 }, + } + } + } + + pub fn begin_command(&mut self) { + self.command = Some(Either::Cmd(Command::new())); + } + + pub fn execute_command(&mut self) { + let Some(Either::Cmd(cmd)) = self.command.take() + else { unsafe { unreachable_unchecked() } }; + + let mut iter = cmd.buf() + .split(' '); + + let prog_name = iter.next().unwrap(); + let output = Executable::new(prog_name) + .args(iter) + .output(); + + let command_output = CommandOutput::new(output.map(|o| o.stdout)); + if !command_output.should_display() { + return; + } + + self.command = Some(Either::CmdOut(command_output)); + } + + pub fn cancel_command(&mut self) { + self.command = None; + } + + pub fn close_window(&mut self) { + self.command = None; } } \ No newline at end of file