one step backward for reorganisation
parent
0ee44327f3
commit
fd44a36d73
|
@ -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<Directory>,
|
||||
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<Item>,
|
||||
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<bool, Error> {
|
||||
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<bool, Error> {
|
||||
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 => " ",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 => " ",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
48
src/main.rs
48
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 }
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue