commands work
parent
e1ef2d9a18
commit
4f9b52151c
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
17
src/main.rs
17
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
src/pane.rs
14
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;
|
||||
}
|
123
src/state.rs
123
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<T, U> {
|
||||
Cmd(T),
|
||||
CmdOut(U),
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
panes: Vec<Box<dyn Pane>>,
|
||||
selected: usize,
|
||||
dir: Directory,
|
||||
size: Rect,
|
||||
command: Option<Either<Command, CommandOutput>>
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue