commands work

main
nick 2024-10-02 14:56:07 -04:00
parent e1ef2d9a18
commit 4f9b52151c
6 changed files with 233 additions and 27 deletions

44
src/command.rs Normal file
View File

@ -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,
}
}
}

50
src/command_output.rs Normal file
View File

@ -0,0 +1,50 @@
use std::io::Error;
use tui::widgets::{Block, Borders, Paragraph};
use crate::pane::prelude::*;
pub struct CommandOutput {
contents: Result<Option<String>, Error>,
}
impl CommandOutput {
pub fn new(src: Result<Vec<u8>, 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("<BINARY>"),
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")
}
}

View File

@ -1,10 +1,11 @@
mod display; mod display;
mod item; mod item;
use crossterm::event::KeyCode; use tui::widgets::{Block, Borders, List, ListItem};
use tui::{layout::Rect, style::{Color, Style}, 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 display::TreePart;
use item::Item; use item::Item;
@ -125,9 +126,8 @@ impl Pane for Directory {
let x = u16::from(self.selected().depth) * 4 let x = u16::from(self.selected().depth) * 4
+ u16::try_from(self.selected().name.len()) + u16::try_from(self.selected().name.len())
.expect("name too long to display") .expect("name too long to display");
+ 1; let y = self.selection;
let y = self.selection + 1;
Some((x, y)) Some((x, y))
} }

View File

@ -1,11 +1,13 @@
mod command;
mod command_output;
mod directory; mod directory;
mod pane; mod pane;
mod terminal;
mod state; mod state;
mod terminal;
use pane::Message;
use terminal::Terminal;
use state::State; use state::State;
use terminal::Terminal;
use pane::prelude::*;
use std::io::Error; use std::io::Error;
@ -15,10 +17,10 @@ fn main() -> Result<(), Error> {
let mut terminal = Terminal::new()?; let mut terminal = Terminal::new()?;
let area = terminal.size()?; let area = terminal.size()?;
let mut state = State::new(); let mut state = State::new(area);
loop { loop {
state.render(&mut terminal, area)?; state.render(&mut terminal)?;
if let Some((x, y)) = state.cursor_position() { if let Some((x, y)) = state.cursor_position() {
terminal.show_cursor()?; terminal.show_cursor()?;
@ -33,8 +35,9 @@ fn main() -> Result<(), Error> {
match state.update(key) { match state.update(key) {
Message::Nothing => (), Message::Nothing => (),
Message::Exit => break, Message::Exit => break,
Message::BeginCommand => todo!("begin command"), Message::BeginCommand => state.begin_command(),
Message::ExecuteCommand => todo!("execute command"), Message::ExecuteCommand => state.execute_command(),
Message::CancelCommand => state.cancel_command(),
} }
} }

View File

@ -1,7 +1,8 @@
use std::io::Stdout; use std::io::Stdout;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use tui::{backend::CrosstermBackend, layout::Rect, Frame}; use tui::{layout::Rect, Frame};
use tui::backend::CrosstermBackend;
pub trait Pane { pub trait Pane {
fn cursor_position(&self) -> Option<(u16, u16)>; fn cursor_position(&self) -> Option<(u16, u16)>;
@ -20,5 +21,16 @@ pub enum Message {
Nothing, Nothing,
Exit, Exit,
BeginCommand, BeginCommand,
CancelCommand,
ExecuteCommand, ExecuteCommand,
}
pub mod prelude {
pub use super::{
OutputSink,
Message,
Pane,
};
pub use tui::layout::Rect;
pub use crossterm::event::KeyCode;
} }

View File

@ -1,40 +1,137 @@
use std::hint::unreachable_unchecked;
use std::io::Error; use std::io::Error;
use std::process::Command as Executable;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use tui::layout::Rect; use tui::layout::Rect;
use crate::pane::{Message, Pane}; use crate::command::Command;
use crate::command_output::CommandOutput;
use crate::directory::Directory; use crate::directory::Directory;
use crate::pane::{Message, Pane};
use crate::terminal::Terminal; use crate::terminal::Terminal;
enum Either<T, U> {
Cmd(T),
CmdOut(U),
}
pub struct State { pub struct State {
panes: Vec<Box<dyn Pane>>, dir: Directory,
selected: usize, size: Rect,
command: Option<Either<Command, CommandOutput>>
} }
impl State { impl State {
pub fn new() -> Self { pub fn new(area: Rect) -> Self {
let dir = Directory::new("root".into());
Self { Self {
panes: vec![ Box::new( Directory::new("root".into()) ) ], dir,
selected: 0, 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( terminal.draw(
|output| |output| {
self.panes self.dir.display(output, dir_area);
.iter() if let Some(command) = self.command.as_ref() {
.for_each(|p| p.display(output, area)) match command {
Either::Cmd(cmd) => cmd.display(output, bottom_area),
Either::CmdOut(cmd) => cmd.display(output, bottom_area),
}
}
}
)?; )?;
Ok(()) 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)> { 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 { 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;
} }
} }