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)]
|
use crate::pane::Pane;
|
||||||
enum Mode {
|
|
||||||
#[default]
|
use display::TreePart;
|
||||||
Default,
|
use item::Item;
|
||||||
ChangingName,
|
|
||||||
GettingCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Directory {
|
pub struct Directory {
|
||||||
name: String,
|
dirs: Vec<Item>,
|
||||||
depth: u8,
|
selection: u16,
|
||||||
expanded: bool,
|
editing: bool,
|
||||||
is_file: bool,
|
|
||||||
}
|
}
|
||||||
impl State {
|
impl Directory {
|
||||||
pub fn new(root: String) -> Self {
|
pub fn new(root: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
dirs: vec![Directory::new(root, 0, false)],
|
dirs: vec![Item::new(root, 0, false)],
|
||||||
selection: 0,
|
selection: 0,
|
||||||
mode: Mode::Default,
|
editing: false,
|
||||||
cmd_buf: String::new(),
|
|
||||||
cmd_out: String::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,16 +44,110 @@ impl State {
|
||||||
current_depth + 1
|
current_depth + 1
|
||||||
};
|
};
|
||||||
|
|
||||||
let new = Directory::new(name, depth, is_file);
|
let new = Item::new(name, depth, is_file);
|
||||||
let index = self.selection()+1;
|
self.dirs.insert(self.selection()+1, new);
|
||||||
self.dirs.insert(index, 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 mut iter = self.dirs.iter().enumerate();
|
||||||
let Some((_, init)) = iter.by_ref()
|
let Some((_, init)) = iter.by_ref()
|
||||||
.next()
|
.next()
|
||||||
else { return (String::new(), "") };
|
else { return String::new() };
|
||||||
|
|
||||||
let mut indent_parts = Vec::new();
|
let mut indent_parts = Vec::new();
|
||||||
let mut result = init.name.clone() + "/\n";
|
let mut result = init.name.clone() + "/\n";
|
||||||
|
@ -112,139 +193,11 @@ impl State {
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
(result, &self.cmd_out)
|
result
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
fn is_last_at_this_depth(slice: &[Item], target_depth: u8) -> 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_last_at_this_depth(slice: &[Directory], target_depth: u8) -> bool {
|
|
||||||
for d in slice {
|
for d in slice {
|
||||||
if d.depth < target_depth { return true }
|
if d.depth < target_depth { return true }
|
||||||
|
|
||||||
|
@ -253,37 +206,3 @@ fn is_last_at_this_depth(slice: &[Directory], target_depth: u8) -> bool {
|
||||||
|
|
||||||
true
|
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 directory;
|
||||||
// mod pane;
|
mod pane;
|
||||||
|
|
||||||
mod state;
|
|
||||||
use crossterm::cursor::Show;
|
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::io::{self, Error};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use tui::backend::CrosstermBackend;
|
use tui::backend::CrosstermBackend;
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::style::{Color, Style};
|
use tui::style::{Color, Style};
|
||||||
use tui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
use tui::widgets::{Block, Borders, List, ListItem};
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
@ -22,16 +22,13 @@ fn main() -> Result<(), Error> {
|
||||||
let mut terminal = MyTerminal::enter()?;
|
let mut terminal = MyTerminal::enter()?;
|
||||||
let size = terminal.size()?;
|
let size = terminal.size()?;
|
||||||
|
|
||||||
let mut dir = State::new("root".into());
|
let mut dir = Directory::new("root".into());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (diagramme, bottom_buf) = dir.display();
|
let diagramme = dir.display();
|
||||||
let bottom_height = u16::try_from(bottom_buf.lines().count())
|
|
||||||
.expect("too many lines to display");
|
|
||||||
|
|
||||||
let rows = diagramme
|
let rows = diagramme
|
||||||
.lines()
|
.lines()
|
||||||
.take((size.height - bottom_height) as usize)
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, s)| {
|
.map(|(i, s)| {
|
||||||
let row = ListItem::new(s);
|
let row = ListItem::new(s);
|
||||||
|
@ -46,29 +43,24 @@ fn main() -> Result<(), Error> {
|
||||||
|
|
||||||
let widget = table_vector(rows);
|
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(
|
terminal.draw(
|
||||||
|f| {
|
|f| {
|
||||||
f.render_widget(widget, top_pane);
|
f.render_widget(widget, size);
|
||||||
f.render_widget(bottom_widget, bottom_pane);
|
|
||||||
}
|
}
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
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 }
|
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