tek/crates/tek/src/tui/file_browser.rs

188 lines
5.9 KiB
Rust

use crate::*;
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
use FileBrowserCommand::*;
use super::phrase_list::PhraseListMode::{Import, Export};
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<Tui>
}
render!(|self: FileBrowser|{
Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})
});
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(String),
}
command!(|self: FileBrowserCommand, state: PhraseListModel|{
let mode = state.phrases_mode_mut();
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(Import(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
Confirm => {
if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
}
},
_ => todo!(),
},
Some(PhraseListMode::Export(index, ref mut browser)) => match self {
Cancel => {
*mode = None;
},
Chdir(cwd) => {
*mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?));
},
Select(index) => {
browser.index = index;
},
_ => unreachable!()
},
_ => unreachable!(),
};
None
});
input_to_command!(FileBrowserCommand:<Tui>|state:PhraseListModel,from|{
if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Select(
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
),
key_pat!(Down) => Select(
browser.index.saturating_add(1) % browser.len()
),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});
input_to_command!(PhraseLengthCommand:<Tui>|state:PhraseListModel,from|{
if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Self::Inc,
key_pat!(Down) => Self::Dec,
key_pat!(Right) => Self::Next,
key_pat!(Left) => Self::Prev,
key_pat!(Enter) => Self::Set(*length),
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});