Not yet doing what it's supposed to

pull/3/head
Arijit Basu 3 years ago
commit f9c3edee06
No known key found for this signature in database
GPG Key ID: 7D7BF809E7378863

1
.gitignore vendored

@ -0,0 +1 @@
/target

1118
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
[package]
name = "xplr"
version = "0.1.0"
authors = ["Arijit Basu <sayanarijit@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tui = { version = "0.14", default-features = false, features = ['crossterm', 'serde'] }
termion = "1.5"
crossterm = "0.18"
dirs = "3.0.1"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
handlebars = "3.5.3"
[dev-dependencies]
criterion = "0.3"
[[bench]]
name = "navigation"
harness = false

@ -0,0 +1,4 @@
A Hackable TUI File Explorer
============================
Don't run it without copying the `config.yml` inside `~/.config/xplr`.

@ -0,0 +1,46 @@
use criterion::{criterion_group, criterion_main, Criterion};
use std::fs;
use xplr::*;
fn criterion_benchmark(c: &mut Criterion) {
let app = app::create()
.expect("failed to create app")
.change_directory(&"/tmp/xplr_bench".to_string())
.unwrap();
fs::create_dir_all("/tmp/xplr_bench").unwrap();
(1..10000).for_each(|i| {
fs::File::create(format!("/tmp/xplr_bench/{}", i)).unwrap();
});
c.bench_function("focus next item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusNextItem))
})
});
c.bench_function("focus previous item", |b| {
b.iter(|| {
app.clone().handle(&config::Action::Global(
config::GlobalAction::FocusPreviousItem,
))
})
});
c.bench_function("focus first item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusFirstItem))
})
});
c.bench_function("focus last item", |b| {
b.iter(|| {
app.clone()
.handle(&config::Action::Global(config::GlobalAction::FocusLastItem))
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

@ -0,0 +1,161 @@
general:
show_hidden: false
table:
header:
cols:
- format: "│ path"
- format: "is symlink"
- format: "index"
height: 1
style:
add_modifier:
bits: 1
sub_modifier:
bits: 0
row:
cols:
- format: "{{tree}}{{prefix}}{{icon}} {{relativePath}}{{#if isDir}}/{{/if}}{{suffix}}"
- format: "{{isSymlink}}"
- format: "{{focusRelativeIndex}}/{{bufferRelativeIndex}}/{{index}}/{{totalItems}}"
col_spacing: 3
col_widths:
- percentage: 60
- percentage: 20
- percentage: 20
tree:
- format: "├─"
- format: "├─"
- format: "╰─"
normal_ui:
prefix: " "
suffix: " "
focused_ui:
prefix: "▸ ["
suffix: "]"
style:
fg: Blue
add_modifier:
bits: 1
sub_modifier:
bits: 0
selected_ui:
prefix: " {"
suffix: "}"
style:
fg: LightGreen
add_modifier:
bits: 0
sub_modifier:
bits: 0
filetypes:
directory:
icon: ""
style:
add_modifier:
bits: 1
sub_modifier:
bits: 0
file:
icon: ""
symlink:
icon: ""
style:
fg: Blue
add_modifier:
bits: 1
sub_modifier:
bits: 0
extension:
md:
icon: ""
lock:
icon: ""
special:
poetry.lock:
icon: ""
key_bindings:
global:
ctrl-c:
help: quit
actions:
- Quit
q:
help: quit
actions:
- Quit
escape:
help: quit
actions:
- Quit
left:
help: back
actions:
- Back
dot:
help: toggle show hidden
actions:
- ToggleShowHidden
right:
help: enter
actions:
- Enter
up:
help: up
actions:
- FocusPreviousItem
down:
help: down
actions:
- FocusNextItem
shift-g:
help: bottom
actions:
- FocusLastItem
tilda:
help: go home
actions:
- ChangeDirectory: ~
forward-slash:
help: go root
actions:
- ChangeDirectory: / # not working for some reason
explore_mode:
space:
help: select
actions:
- Select
- FocusNextItem
g:
help: go to
actions:
- EnterSubmode: GoTo
explore_submodes:
GoTo:
g:
help: top
actions:
- FocusFirstItem
- ExitSubmode
select_mode:
space:
help: toggle selection
actions:
- ToggleSelection
- FocusNextItem

@ -0,0 +1,757 @@
use crate::config::{Action, Config, ExploreModeAction, GlobalAction, Mode, SelectModeAction};
use crate::error::Error;
use crate::input::Key;
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::path::PathBuf;
pub const UNSUPPORTED_STR: &str = "???";
pub const TOTAL_ROWS: usize = 50;
pub const TEMPLATE_TABLE_ROW: &str = "TEMPLATE_TABLE_ROW";
fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
let p = path_user_input.as_ref();
if !p.starts_with("~") {
return Some(p.to_path_buf());
}
if p == Path::new("~") {
return dirs::home_dir();
}
dirs::home_dir().map(|mut h| {
if h == Path::new("/") {
// Corner case: `h` root directory;
// don't prepend extra `/`, just drop the tilde.
p.strip_prefix("~").unwrap().to_path_buf()
} else {
h.push(p.strip_prefix("~/").unwrap());
h
}
})
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DirectoryBuffer {
pub focus: Option<usize>,
pub items: Vec<(PathBuf, DirectoryItemMetadata)>,
pub total: usize,
}
impl DirectoryBuffer {
pub fn relative_focus(focus: usize) -> usize {
focus.min(TOTAL_ROWS)
}
pub fn explore(
path: &PathBuf,
show_hidden: bool,
) -> Result<(usize, impl Iterator<Item = (PathBuf, String)>), Error> {
let hide_hidden = !show_hidden;
let total = fs::read_dir(&path)?
.filter_map(|d| d.ok().map(|e| e.path()))
.filter_map(|p| p.canonicalize().ok())
.filter_map(|abs_path| {
abs_path
.file_name()
.map(|rel_path| rel_path.to_str().unwrap_or(UNSUPPORTED_STR).to_string())
})
.filter(|rel_path| !(hide_hidden && rel_path.starts_with('.')))
.count();
let items = fs::read_dir(&path)?
.filter_map(|d| d.ok().map(|e| e.path()))
.filter_map(|p| p.canonicalize().ok())
.filter_map(|abs_path| {
abs_path.file_name().map(|rel_path| {
(
abs_path.to_path_buf(),
rel_path.to_str().unwrap_or(UNSUPPORTED_STR).to_string(),
)
})
})
.filter(move |(_, rel_path)| !(hide_hidden && rel_path.starts_with('.')));
Ok((total, items))
}
pub fn load(
config: &Config,
focus: Option<usize>,
path: &PathBuf,
show_hidden: bool,
selected_paths: &HashSet<PathBuf>,
) -> Result<DirectoryBuffer, Error> {
let offset = focus
.map(|f| (f.max(TOTAL_ROWS) - TOTAL_ROWS, f.max(TOTAL_ROWS)))
.unwrap_or((0, TOTAL_ROWS));
let (total, items) = DirectoryBuffer::explore(&path, show_hidden)?;
let visible: Vec<(PathBuf, DirectoryItemMetadata)> = items
.enumerate()
.skip_while(|(i, _)| *i < offset.0)
.take_while(|(i, _)| *i <= offset.1)
.enumerate()
.map(|(rel_idx, (net_idx, (abs, rel)))| {
let ext = abs
.extension()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
(net_idx, rel_idx, abs, rel, ext)
})
.map(|(net_idx, rel_idx, abs, rel, ext)| {
let absolute_path: String =
abs.as_os_str().to_str().unwrap_or(UNSUPPORTED_STR).into();
let relative_path = rel.to_string();
let extension = ext.to_string();
let is_dir = abs.is_dir();
let is_file = abs.is_file();
let maybe_meta = abs.metadata().ok();
let is_symlink = maybe_meta
.clone()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let is_readonly = maybe_meta
.clone()
.map(|m| m.permissions().readonly())
.unwrap_or(false);
let (focus_idx, is_focused) =
focus.map(|f| (f, net_idx == f)).unwrap_or((0, false));
let is_selected = selected_paths.contains(&abs);
let ui = if is_focused {
&config.general.focused_ui
} else if is_selected {
&config.general.selected_ui
} else {
&config.general.normal_ui
};
let is_first_item = net_idx == 0;
let is_last_item = net_idx == total.max(1) - 1;
let tree = config
.general
.table
.tree
.clone()
.map(|t| {
if is_last_item {
t.2.format.clone()
} else if is_first_item {
t.0.format.clone()
} else {
t.1.format.clone()
}
})
.unwrap_or_default();
let filetype = config
.filetypes
.special
.get(&relative_path)
.or_else(|| config.filetypes.extension.get(&extension))
.unwrap_or_else(|| {
if is_symlink {
&config.filetypes.symlink
} else if is_dir {
&config.filetypes.directory
} else {
&config.filetypes.file
}
});
let focus_relative_index = if focus_idx <= net_idx {
format!(" {}", net_idx - focus_idx)
} else {
format!("-{}", focus_idx - net_idx)
};
let m = DirectoryItemMetadata {
absolute_path,
relative_path,
extension,
icon: filetype.icon.clone(),
prefix: ui.prefix.clone(),
suffix: ui.suffix.clone(),
tree: tree.into(),
is_symlink,
is_first_item,
is_last_item,
is_dir,
is_file,
is_readonly,
is_selected,
is_focused,
index: net_idx + 1,
focus_relative_index,
buffer_relative_index: rel_idx + 1,
total_items: total,
};
(abs.to_owned(), m)
})
.collect();
let focus = focus.map(|f| {
if Self::relative_focus(f) >= visible.len() {
visible.len().max(1) - 1
} else {
f
}
});
Ok(Self {
total,
items: visible,
focus,
})
}
pub fn focused_item(&self) -> Option<(PathBuf, DirectoryItemMetadata)> {
self.focus.and_then(|f| {
self.items
.get(Self::relative_focus(f))
.map(|f| f.to_owned())
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectoryItemMetadata {
pub absolute_path: String,
pub relative_path: String,
pub extension: String,
pub icon: String,
pub prefix: String,
pub suffix: String,
pub tree: String,
pub is_first_item: bool,
pub is_last_item: bool,
pub is_symlink: bool,
pub is_dir: bool,
pub is_file: bool,
pub is_readonly: bool,
pub is_selected: bool,
pub is_focused: bool,
pub index: usize,
pub focus_relative_index: String,
pub buffer_relative_index: usize,
pub total_items: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct App {
pub config: Config,
pub pwd: PathBuf,
pub directory_buffer: DirectoryBuffer,
pub saved_buffers: HashMap<PathBuf, DirectoryBuffer>,
pub selected_paths: HashSet<PathBuf>,
pub mode: Mode,
pub show_hidden: bool,
pub result: Option<String>,
}
impl App {
pub fn new(
config: &Config,
pwd: &PathBuf,
saved_buffers: &HashMap<PathBuf, DirectoryBuffer>,
selected_paths: &HashSet<PathBuf>,
mode: Mode,
show_hidden: bool,
focus: Option<usize>,
) -> Result<Self, Error> {
let directory_buffer =
DirectoryBuffer::load(config, focus.or(Some(0)), pwd, show_hidden, selected_paths)?;
let mut saved_buffers = saved_buffers.clone();
saved_buffers.insert(pwd.into(), directory_buffer.to_owned());
Ok(Self {
config: config.to_owned(),
pwd: pwd.to_owned(),
directory_buffer,
saved_buffers,
selected_paths: selected_paths.to_owned(),
mode,
show_hidden,
result: None,
})
}
pub fn exit_submode(self) -> Result<Self, Error> {
let mut app = self;
let mode = match app.mode {
Mode::ExploreSubmode(_) => Mode::Explore,
Mode::SelectSubmode(_) => Mode::Select,
m => m,
};
app.mode = mode;
Ok(app)
}
pub fn toggle_hidden(self) -> Result<Self, Error> {
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
!self.show_hidden,
self.directory_buffer.focus,
)
}
pub fn focus_first_item(self) -> Result<Self, Error> {
let focus = if self.directory_buffer.total == 0 {
None
} else {
Some(0)
};
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
self.show_hidden,
focus,
)
}
pub fn focus_last_item(self) -> Result<Self, Error> {
let focus = if self.directory_buffer.total == 0 {
None
} else {
Some(self.directory_buffer.total - 1)
};
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
self.show_hidden,
focus,
)
}
pub fn change_directory(self, dir: &String) -> Result<Self, Error> {
self.focus_path(&PathBuf::from(dir))?.enter()
}
pub fn focus_next_item(self) -> Result<Self, Error> {
let len = self.directory_buffer.total;
let focus = self
.directory_buffer
.focus
.map(|f| (len - 1).min(f + 1))
.or(Some(0));
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
self.show_hidden,
focus,
)
}
pub fn focus_previous_item(self) -> Result<Self, Error> {
let len = self.directory_buffer.total;
let focus = if len == 0 {
None
} else {
self.directory_buffer
.focus
.map(|f| Some(1.max(f) - 1))
.unwrap_or(Some(len - 1))
};
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
self.show_hidden,
focus,
)
}
pub fn focus_path(self, path: &PathBuf) -> Result<Self, Error> {
expand_tilde(path)
.unwrap_or(path.into())
.parent()
.map(|pwd| {
let (_, items) = DirectoryBuffer::explore(&pwd.into(), self.show_hidden)?;
let focus = items
.enumerate()
.find_map(|(i, (p, _))| {
if p.as_path() == path.as_path() {
Some(i)
} else {
None
}
})
.or(Some(0));
Self::new(
&self.config,
&pwd.into(),
&self.saved_buffers,
&self.selected_paths,
self.mode.clone(),
self.show_hidden,
focus,
)
})
.unwrap_or_else(|| Ok(self.to_owned()))
}
pub fn focus_by_index(self, idx: &usize) -> Result<Self, Error> {
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode.clone(),
self.show_hidden,
Some(idx.clone()),
)
}
pub fn focus_by_buffer_relative_index(self, idx: &usize) -> Result<Self, Error> {
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode.clone(),
self.show_hidden,
Some(DirectoryBuffer::relative_focus(idx.clone())),
)
}
pub fn focus_by_focus_relative_index(self, idx: &isize) -> Result<Self, Error> {
Self::new(
&self.config,
&self.pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode.clone(),
self.show_hidden,
self.directory_buffer
.focus
.map(|f| ((f as isize) + idx).min(0) as usize), // TODO: make it safer
)
}
pub fn enter(self) -> Result<Self, Error> {
let pwd = self
.directory_buffer
.focused_item()
.map(|(p, _)| p)
.map(|p| if p.is_dir() { p } else { self.pwd.clone() })
.unwrap_or_else(|| self.pwd.clone());
let focus = self.saved_buffers.get(&pwd).and_then(|b| b.focus);
Self::new(
&self.config,
&pwd,
&self.saved_buffers,
&self.selected_paths,
self.mode,
self.show_hidden,
focus,
)
}
pub fn back(self) -> Result<Self, Error> {
let app = self.clone();
self.focus_path(&app.pwd)
}
pub fn select(self) -> Result<Self, Error> {
let selected_paths = self
.directory_buffer
.focused_item()
.map(|(p, _)| {
let mut selected_paths = self.selected_paths.clone();
selected_paths.insert(p);
selected_paths
})
.unwrap_or_else(|| self.selected_paths.clone());
let mut app = self;
app.selected_paths = selected_paths;
app.mode = Mode::Select;
Ok(app)
}
pub fn toggle_selection(self) -> Result<Self, Error> {
let selected_paths = self
.directory_buffer
.focused_item()
.map(|(p, _)| {
let mut selected_paths = self.selected_paths.clone();
if selected_paths.contains(&p) {
selected_paths.remove(&p);
} else {
selected_paths.insert(p);
}
selected_paths
})
.unwrap_or_else(|| self.selected_paths.clone());
let mode = if selected_paths.len() == 0 {
Mode::Explore
} else {
Mode::Select
};
let mut app = self;
app.selected_paths = selected_paths;
app.mode = mode;
Ok(app)
}
pub fn enter_submode(self, submode: &String) -> Result<Self, Error> {
let mut app = self;
app.mode = Mode::ExploreSubmode(submode.clone());
Ok(app)
}
pub fn print_focused_and_quit(self) -> Result<Self, Error> {
let mut app = self;
app.result = app
.directory_buffer
.focused_item()
.and_then(|(p, _)| p.to_str().map(|s| s.to_string()));
Ok(app)
}
pub fn print_pwd_and_quit(self) -> Result<Self, Error> {
let mut app = self;
app.result = app.pwd.to_str().map(|s| s.to_string());
Ok(app)
}
pub fn print_selected_and_quit(self) -> Result<Self, Error> {
let mut app = self;
app.result = Some(
app.selected_paths
.clone()
.iter()
.filter_map(|p| p.to_str())
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join("\n"),
);
Ok(app)
}
pub fn print_app_state_and_quit(self) -> Result<Self, Error> {
let state = serde_yaml::to_string(&self)?;
let mut app = self;
app.result = Some(state);
Ok(app)
}
pub fn quit(self) -> Result<Self, Error> {
Err(Error::Interrupted)
}
pub fn actions_from_key(&self, key: Key) -> Option<Vec<Action>> {
self.config
.key_bindings
.global
.get(&key)
.map(|m| {
m.actions
.iter()
.map(|a| Action::Global(a.clone()))
.collect()
})
.or_else(|| match &self.mode {
Mode::Explore => self.config.key_bindings.explore_mode.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::ExploreMode(a.clone()))
.collect()
}),
Mode::Select => self.config.key_bindings.select_mode.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::SelectMode(a.clone()))
.collect()
}),
Mode::ExploreSubmode(sub) => self
.config
.key_bindings
.explore_submodes
.get(sub)
.and_then(|kb| {
kb.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::ExploreMode(a.clone()))
.collect()
})
}),
Mode::SelectSubmode(sub) => self
.config
.key_bindings
.select_submodes
.get(sub)
.and_then(|kb| {
kb.get(&key).map(|m| {
m.actions
.iter()
.map(|a| Action::SelectMode(a.clone()))
.collect()
})
}),
})
}
pub fn handle(self, action: &Action) -> Result<Self, Error> {
match action {
// Global actions
Action::Global(GlobalAction::ToggleShowHidden) => self.toggle_hidden(),
Action::Global(GlobalAction::Back) => self.back(),
Action::Global(GlobalAction::Enter) => self.enter(),
Action::Global(GlobalAction::FocusNext) => self.focus_next_item(),
Action::Global(GlobalAction::FocusPrevious) => self.focus_previous_item(),
Action::Global(GlobalAction::FocusFirst) => self.focus_first_item(),
Action::Global(GlobalAction::FocusLast) => self.focus_last_item(),
Action::Global(GlobalAction::FocusPath(path)) => self.focus_path(&path.into()),
Action::Global(GlobalAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::Global(GlobalAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::Global(GlobalAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::Global(GlobalAction::ChangeDirectory(dir)) => self.change_directory(&dir),
Action::Global(GlobalAction::PrintPwdAndQuit) => self.print_pwd_and_quit(),
Action::Global(GlobalAction::PrintAppStateAndQuit) => self.print_app_state_and_quit(),
Action::Global(GlobalAction::Quit) => self.quit(),
// Explore mode
Action::ExploreMode(ExploreModeAction::ToggleShowHidden) => self.toggle_hidden(),
Action::ExploreMode(ExploreModeAction::Back) => self.back(),
Action::ExploreMode(ExploreModeAction::Enter) => self.enter(),
Action::ExploreMode(ExploreModeAction::FocusNext) => self.focus_next_item(),
Action::ExploreMode(ExploreModeAction::FocusPrevious) => self.focus_previous_item(),
Action::ExploreMode(ExploreModeAction::FocusFirst) => self.focus_first_item(),
Action::ExploreMode(ExploreModeAction::FocusLast) => self.focus_last_item(),
Action::ExploreMode(ExploreModeAction::FocusPath(path)) => {
self.focus_path(&path.into())
}
Action::ExploreMode(ExploreModeAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::ExploreMode(ExploreModeAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::ExploreMode(ExploreModeAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::ExploreMode(ExploreModeAction::ChangeDirectory(dir)) => {
self.change_directory(&dir)
}
Action::ExploreMode(ExploreModeAction::Select) => self.select(),
Action::ExploreMode(ExploreModeAction::EnterSubmode(submode)) => {
self.enter_submode(submode)
}
Action::ExploreMode(ExploreModeAction::ExitSubmode) => self.exit_submode(),
Action::ExploreMode(ExploreModeAction::PrintFocusedAndQuit) => {
self.print_focused_and_quit()
}
Action::ExploreMode(ExploreModeAction::PrintPwdAndQuit) => self.print_pwd_and_quit(),
Action::ExploreMode(ExploreModeAction::PrintAppStateAndQuit) => {
self.print_app_state_and_quit()
}
Action::ExploreMode(ExploreModeAction::Quit) => self.quit(),
// Select mode
Action::SelectMode(SelectModeAction::ToggleShowHidden) => self.toggle_hidden(),
Action::SelectMode(SelectModeAction::Back) => self.back(),
Action::SelectMode(SelectModeAction::Enter) => self.enter(),
Action::SelectMode(SelectModeAction::FocusNext) => self.focus_next_item(),
Action::SelectMode(SelectModeAction::FocusPrevious) => self.focus_previous_item(),
Action::SelectMode(SelectModeAction::FocusFirst) => self.focus_first_item(),
Action::SelectMode(SelectModeAction::FocusLast) => self.focus_last_item(),
Action::SelectMode(SelectModeAction::FocusPath(path)) => self.focus_path(&path.into()),
Action::SelectMode(SelectModeAction::FocusPathByIndex(n)) => self.focus_by_index(n),
Action::SelectMode(SelectModeAction::FocusPathByBufferRelativeIndex(n)) => {
self.focus_by_buffer_relative_index(&n)
}
Action::SelectMode(SelectModeAction::FocusPathByFocusRelativeIndex(n)) => {
self.focus_by_focus_relative_index(&n)
}
Action::SelectMode(SelectModeAction::ChangeDirectory(dir)) => {
self.change_directory(&dir)
}
Action::SelectMode(SelectModeAction::ToggleSelection) => self.toggle_selection(),
Action::SelectMode(SelectModeAction::EnterSubmode(submode)) => {
self.enter_submode(submode)
}
Action::SelectMode(SelectModeAction::ExitSubmode) => self.exit_submode(),
Action::SelectMode(SelectModeAction::PrintSelectedAndQuit) => {
self.print_selected_and_quit()
}
Action::SelectMode(SelectModeAction::PrintAppStateAndQuit) => {
self.print_app_state_and_quit()
}
Action::SelectMode(SelectModeAction::Quit) => self.quit(),
}
}
}
pub fn create() -> Result<App, Error> {
let config_dir = dirs::config_dir()
.unwrap_or(PathBuf::from("."))
.join("xplr");
let config_file = config_dir.join("config.yml");
let config: Config = if config_file.exists() {
serde_yaml::from_reader(BufReader::new(&File::open(config_file)?))?
} else {
Config::default()
};
let pwd = PathBuf::from("./")
.canonicalize()
.unwrap_or(PathBuf::from("/"));
App::new(
&config,
&pwd,
&Default::default(),
&Default::default(),
Mode::Explore,
config.general.show_hidden,
None,
)
}

@ -0,0 +1,492 @@
use crate::input::Key;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tui::layout::Constraint as TUIConstraint;
use tui::style::Color;
use tui::style::Modifier;
use tui::style::Style;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Mode {
Explore,
ExploreSubmode(String),
Select,
SelectSubmode(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Format {
Line,
Pretty,
Yaml,
YamlPretty,
Template(String),
}
impl Default for Format {
fn default() -> Self {
Self::Line
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GlobalAction {
// Common actions
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPath(String),
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
ChangeDirectory(String),
// Quit options
PrintPwdAndQuit,
PrintAppStateAndQuit,
Quit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ExploreModeAction {
// Common actions
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
FocusPath(String),
ChangeDirectory(String),
// Explore mode exclusive options
EnterSubmode(String),
ExitSubmode,
Select,
// Unselect,
// SelectAll,
// SelectAllRecursive,
// Quit options
PrintFocusedAndQuit,
PrintPwdAndQuit,
PrintAppStateAndQuit,
Quit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SelectModeAction {
// Common actions
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
FocusPath(String),
ChangeDirectory(String),
// Select mode exclusive options
EnterSubmode(String),
ExitSubmode,
// Select,
// Unselect,
// SelectAll,
// SelectAllRecursive,
// UnselectAll,
// UnSelectAllRecursive,
ToggleSelection,
// ClearSelectedPaths,
// Quit options
PrintSelectedAndQuit,
PrintAppStateAndQuit,
Quit,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Global(GlobalAction),
ExploreMode(ExploreModeAction),
SelectMode(SelectModeAction),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GlobalActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<GlobalAction>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExploreModeActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<ExploreModeAction>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectModeActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<SelectModeAction>,
}
pub type ExploreSubmodeActionMenu = HashMap<Key, ExploreModeActionMenu>;
pub type SelectSubmodeActionMenu = HashMap<Key, SelectModeActionMenu>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindings {
pub global: HashMap<Key, GlobalActionMenu>,
#[serde(default)]
pub explore_mode: HashMap<Key, ExploreModeActionMenu>,
#[serde(default)]
pub explore_submodes: HashMap<String, ExploreSubmodeActionMenu>,
#[serde(default)]
pub select_mode: HashMap<Key, SelectModeActionMenu>,
#[serde(default)]
pub select_submodes: HashMap<String, SelectSubmodeActionMenu>,
}
impl Default for KeyBindings {
fn default() -> Self {
let mut global: HashMap<Key, GlobalActionMenu> = Default::default();
let mut explore_mode: HashMap<Key, ExploreModeActionMenu> = Default::default();
let explore_submodes: HashMap<String, ExploreSubmodeActionMenu> = Default::default();
let mut select_mode: HashMap<Key, SelectModeActionMenu> = Default::default();
let select_submodes: HashMap<String, SelectSubmodeActionMenu> = Default::default();
global.insert(
Key::CtrlC,
GlobalActionMenu {
help: "quit".into(),
actions: vec![GlobalAction::Quit],
},
);
global.insert(
Key::Q,
GlobalActionMenu {
help: "quit".into(),
actions: vec![GlobalAction::Quit],
},
);
global.insert(
Key::Escape,
GlobalActionMenu {
help: "quit".into(),
actions: vec![GlobalAction::Quit],
},
);
global.insert(
Key::Left,
GlobalActionMenu {
help: "left".into(),
actions: vec![GlobalAction::Back],
},
);
global.insert(
Key::Dot,
GlobalActionMenu {
help: "show/hide hidden files".into(),
actions: vec![GlobalAction::ToggleShowHidden],
},
);
global.insert(
Key::Right,
GlobalActionMenu {
help: "enter".into(),
actions: vec![GlobalAction::Enter],
},
);
global.insert(
Key::Up,
GlobalActionMenu {
help: "<prev".into(),
actions: vec![GlobalAction::FocusPrevious],
},
);
global.insert(
Key::Down,
GlobalActionMenu {
help: "next>".into(),
actions: vec![GlobalAction::FocusNext],
},
);
global.insert(
Key::G,
GlobalActionMenu {
help: "top".into(),
actions: vec![GlobalAction::FocusFirst],
},
);
global.insert(
Key::ShiftG,
GlobalActionMenu {
help: "bottom".into(),
actions: vec![GlobalAction::FocusLast],
},
);
global.insert(
Key::Tilda,
GlobalActionMenu {
help: "home".into(),
actions: vec![GlobalAction::ChangeDirectory("~".to_string())],
},
);
explore_mode.insert(
Key::Space,
ExploreModeActionMenu {
help: "select".into(),
actions: vec![ExploreModeAction::Select, ExploreModeAction::FocusNext],
},
);
select_mode.insert(
Key::Space,
SelectModeActionMenu {
help: "select/unselect".into(),
actions: vec![
SelectModeAction::ToggleSelection,
SelectModeAction::FocusNext,
],
},
);
Self {
global,
explore_mode,
explore_submodes,
select_mode,
select_submodes,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileTypeConfig {
#[serde(default)]
pub icon: String,
#[serde(default)]
pub style: Style,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileTypesConfig {
#[serde(default)]
pub directory: FileTypeConfig,
#[serde(default)]
pub file: FileTypeConfig,
#[serde(default)]
pub symlink: FileTypeConfig,
#[serde(default)]
pub extension: HashMap<String, FileTypeConfig>,
#[serde(default)]
pub special: HashMap<String, FileTypeConfig>,
}
impl Default for FileTypesConfig {
fn default() -> Self {
FileTypesConfig {
directory: FileTypeConfig {
icon: "d".into(),
style: Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Blue),
},
file: FileTypeConfig {
icon: "f".into(),
style: Default::default(),
},
symlink: FileTypeConfig {
icon: "s".into(),
style: Style::default()
.add_modifier(Modifier::ITALIC)
.fg(Color::Cyan),
},
extension: Default::default(),
special: Default::default(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UIConfig {
#[serde(default)]
pub prefix: String,
#[serde(default)]
pub suffix: String,
#[serde(default)]
pub style: Style,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UIElement {
#[serde(default)]
pub format: String,
#[serde(default)]
pub style: Style,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableRowConfig {
#[serde(default)]
pub cols: Vec<UIElement>,
#[serde(default)]
pub style: Style,
#[serde(default)]
pub height: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Constraint {
Percentage(u16),
Ratio(u32, u32),
Length(u16),
Max(u16),
Min(u16),
}
impl Default for Constraint {
fn default() -> Self {
Self::Min(1)
}
}
impl Into<TUIConstraint> for Constraint {
fn into(self) -> TUIConstraint {
match self {
Self::Length(n) => TUIConstraint::Length(n),
Self::Percentage(n) => TUIConstraint::Percentage(n),
Self::Ratio(x, y) => TUIConstraint::Ratio(x, y),
Self::Max(n) => TUIConstraint::Max(n),
Self::Min(n) => TUIConstraint::Min(n),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableConfig {
#[serde(default)]
pub header: Option<TableRowConfig>,
#[serde(default)]
pub row: TableRowConfig,
#[serde(default)]
pub style: Style,
#[serde(default)]
pub tree: Option<(UIElement, UIElement, UIElement)>,
#[serde(default)]
pub col_spacing: u16,
#[serde(default)]
pub col_widths: Vec<Constraint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
#[serde(default)]
pub show_hidden: bool,
#[serde(default)]
pub table: TableConfig,
#[serde(default)]
pub normal_ui: UIConfig,
#[serde(default)]
pub focused_ui: UIConfig,
#[serde(default)]
pub selected_ui: UIConfig,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
show_hidden: false,
table: TableConfig {
header: None,
row: TableRowConfig {
cols: vec![UIElement {
format: "{{tree}}{{prefix}}{{relativePath}}{{#if isDir}}/{{/if}}{{suffix}}"
.into(),
style: Default::default(),
}],
style: Default::default(),
height: 1,
},
style: Default::default(),
tree: Some((
UIElement {
format: "├─".into(),
style: Default::default(),
},
UIElement {
format: "├─".into(),
style: Default::default(),
},
UIElement {
format: "└─".into(),
style: Default::default(),
},
)),
col_spacing: 1,
col_widths: vec![Constraint::Percentage(100)],
},
normal_ui: UIConfig {
prefix: " ".into(),
suffix: " ".into(),
style: Default::default(),
},
focused_ui: UIConfig {
prefix: "▸ [".into(),
suffix: "]".into(),
style: Style::default().add_modifier(Modifier::BOLD),
},
selected_ui: UIConfig {
prefix: " {".into(),
suffix: "}".into(),
style: Style::default().add_modifier(Modifier::BOLD),
},
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub filetypes: FileTypesConfig,
#[serde(default)]
pub key_bindings: KeyBindings,
}

@ -0,0 +1,32 @@
use std::io;
use handlebars;
use serde_yaml;
#[derive(Debug)]
pub enum Error {
// Not an error but,
Interrupted,
// Real errors
TemplateError(handlebars::TemplateError),
YamlError(serde_yaml::Error),
IOError(io::Error),
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Self::IOError(err)
}
}
impl From<handlebars::TemplateError> for Error {
fn from(err: handlebars::TemplateError) -> Self {
Self::TemplateError(err)
}
}
impl From<serde_yaml::Error> for Error {
fn from(err: serde_yaml::Error) -> Self {
Self::YamlError(err)
}
}

@ -0,0 +1,483 @@
use termion::event::Key as TermionKey;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Key {
Backspace,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
BackTab,
Delete,
Insert,
Return,
Space,
Tab,
Escape,
Zero,
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
CtrlZero,
CtrlOne,
CtrlTwo,
CtrlThree,
CtrlFour,
CtrlFive,
CtrlSix,
CtrlSeven,
CtrlEight,
CtrlNine,
AltZero,
AltOne,
AltTwo,
AltThree,
AltFour,
AltFive,
AltSix,
AltSeven,
AltEight,
AltNine,
ShiftZero,
ShiftOne,
ShiftTwo,
ShiftThree,
ShiftFour,
ShiftFive,
ShiftSix,
ShiftSeven,
ShiftEight,
ShiftNine,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
CtrlA,
CtrlB,
CtrlC,
CtrlD,
CtrlE,
CtrlF,
CtrlG,
CtrlH,
CtrlI,
CtrlJ,
CtrlK,
CtrlL,
CtrlM,
CtrlN,
CtrlO,
CtrlP,
CtrlQ,
CtrlR,
CtrlS,
CtrlT,
CtrlU,
CtrlV,
CtrlW,
CtrlX,
CtrlY,
CtrlZ,
AltA,
AltB,
AltC,
AltD,
AltE,
AltF,
AltG,
AltH,
AltI,
AltJ,
AltK,
AltL,
AltM,
AltN,
AltO,
AltP,
AltQ,
AltR,
AltS,
AltT,
AltU,
AltV,
AltW,
AltX,
AltY,
AltZ,
ShiftA,
ShiftB,
ShiftC,
ShiftD,
ShiftE,
ShiftF,
ShiftG,
ShiftH,
ShiftI,
ShiftJ,
ShiftK,
ShiftL,
ShiftM,
ShiftN,
ShiftO,
ShiftP,
ShiftQ,
ShiftR,
ShiftS,
ShiftT,
ShiftU,
ShiftV,
ShiftW,
ShiftX,
ShiftY,
ShiftZ,
CtrlShiftA,
CtrlShiftB,
CtrlShiftC,
CtrlShiftD,
CtrlShiftE,
CtrlShiftF,
CtrlShiftG,
CtrlShiftH,
CtrlShiftI,
CtrlShiftJ,
CtrlShiftK,
CtrlShiftL,
CtrlShiftM,
CtrlShiftN,
CtrlShiftO,
CtrlShiftP,
CtrlShiftQ,
CtrlShiftR,
CtrlShiftS,
CtrlShiftT,
CtrlShiftU,
CtrlShiftV,
CtrlShiftW,
CtrlShiftX,
CtrlShiftY,
CtrlShiftZ,
AltShiftA,
AltShiftB,
AltShiftC,
AltShiftD,
AltShiftE,
AltShiftF,
AltShiftG,
AltShiftH,
AltShiftI,
AltShiftJ,
AltShiftK,
AltShiftL,
AltShiftM,
AltShiftN,
AltShiftO,
AltShiftP,
AltShiftQ,
AltShiftR,
AltShiftS,
AltShiftT,
AltShiftU,
AltShiftV,
AltShiftW,
AltShiftX,
AltShiftY,
AltShiftZ,
Plus,
Minus,
Backtick,
Tilda,
Underscore,
Equals,
Semicolon,
Colon,
SingleQuote,
DoubleQuote,
ForwardSlash,
BackSlash,
Dot,
Comma,
QuestionMark,
NotSupported,
}
impl Key {
pub fn from_termion_event(key: TermionKey) -> Self {
match key {
TermionKey::Backspace => Key::Backspace,
TermionKey::Left => Key::Left,
TermionKey::Right => Key::Right,
TermionKey::Up => Key::Up,
TermionKey::Down => Key::Down,
TermionKey::Home => Key::Home,
TermionKey::End => Key::End,
TermionKey::PageUp => Key::PageUp,
TermionKey::PageDown => Key::PageDown,
TermionKey::BackTab => Key::BackTab,
TermionKey::Delete => Key::Delete,
TermionKey::Insert => Key::Insert,
TermionKey::Char('\n') => Key::Return,
TermionKey::Char(' ') => Key::Space,
TermionKey::Char('\t') => Key::Tab,
TermionKey::Esc => Key::Escape,
TermionKey::Char('0') => Key::Zero,
TermionKey::Char('1') => Key::One,
TermionKey::Char('2') => Key::Two,
TermionKey::Char('3') => Key::Three,
TermionKey::Char('4') => Key::Four,
TermionKey::Char('5') => Key::Five,
TermionKey::Char('6') => Key::Six,
TermionKey::Char('7') => Key::Seven,
TermionKey::Char('8') => Key::Eight,
TermionKey::Char('9') => Key::Nine,
TermionKey::Ctrl('0') => Key::CtrlZero,
TermionKey::Ctrl('1') => Key::CtrlOne,
TermionKey::Ctrl('2') => Key::CtrlTwo,
TermionKey::Ctrl('3') => Key::CtrlThree,
TermionKey::Ctrl('4') => Key::CtrlFour,
TermionKey::Ctrl('5') => Key::CtrlFive,
TermionKey::Ctrl('6') => Key::CtrlSix,
TermionKey::Ctrl('7') => Key::CtrlSeven,
TermionKey::Ctrl('8') => Key::CtrlEight,
TermionKey::Ctrl('9') => Key::CtrlNine,
TermionKey::Alt('0') => Key::AltZero,
TermionKey::Alt('1') => Key::AltOne,
TermionKey::Alt('2') => Key::AltTwo,
TermionKey::Alt('3') => Key::AltThree,
TermionKey::Alt('4') => Key::AltFour,
TermionKey::Alt('5') => Key::AltFive,
TermionKey::Alt('6') => Key::AltSix,
TermionKey::Alt('7') => Key::AltSeven,
TermionKey::Alt('8') => Key::AltEight,
TermionKey::Alt('9') => Key::AltNine,
TermionKey::Char('a') => Key::A,
TermionKey::Char('b') => Key::B,
TermionKey::Char('c') => Key::C,
TermionKey::Char('d') => Key::D,
TermionKey::Char('e') => Key::E,
TermionKey::Char('f') => Key::F,
TermionKey::Char('g') => Key::G,
TermionKey::Char('h') => Key::H,
TermionKey::Char('i') => Key::I,
TermionKey::Char('j') => Key::J,
TermionKey::Char('k') => Key::K,
TermionKey::Char('l') => Key::L,
TermionKey::Char('m') => Key::M,
TermionKey::Char('n') => Key::N,
TermionKey::Char('o') => Key::O,
TermionKey::Char('p') => Key::P,
TermionKey::Char('q') => Key::Q,
TermionKey::Char('r') => Key::R,
TermionKey::Char('s') => Key::S,
TermionKey::Char('t') => Key::T,
TermionKey::Char('u') => Key::U,
TermionKey::Char('v') => Key::V,
TermionKey::Char('w') => Key::W,
TermionKey::Char('x') => Key::X,
TermionKey::Char('y') => Key::Y,
TermionKey::Char('z') => Key::Z,
TermionKey::Ctrl('a') => Key::CtrlA,
TermionKey::Ctrl('b') => Key::CtrlB,
TermionKey::Ctrl('c') => Key::CtrlC,
TermionKey::Ctrl('d') => Key::CtrlD,
TermionKey::Ctrl('e') => Key::CtrlE,
TermionKey::Ctrl('f') => Key::CtrlF,
TermionKey::Ctrl('g') => Key::CtrlG,
TermionKey::Ctrl('h') => Key::CtrlH,
TermionKey::Ctrl('i') => Key::CtrlI,
TermionKey::Ctrl('j') => Key::CtrlJ,
TermionKey::Ctrl('k') => Key::CtrlK,
TermionKey::Ctrl('l') => Key::CtrlL,
TermionKey::Ctrl('m') => Key::CtrlM,
TermionKey::Ctrl('n') => Key::CtrlN,
TermionKey::Ctrl('o') => Key::CtrlO,
TermionKey::Ctrl('p') => Key::CtrlP,
TermionKey::Ctrl('q') => Key::CtrlQ,
TermionKey::Ctrl('r') => Key::CtrlR,
TermionKey::Ctrl('s') => Key::CtrlS,
TermionKey::Ctrl('t') => Key::CtrlT,
TermionKey::Ctrl('u') => Key::CtrlU,
TermionKey::Ctrl('v') => Key::CtrlV,
TermionKey::Ctrl('w') => Key::CtrlW,
TermionKey::Ctrl('x') => Key::CtrlX,
TermionKey::Ctrl('y') => Key::CtrlY,
TermionKey::Ctrl('z') => Key::CtrlZ,
TermionKey::Alt('a') => Key::AltA,
TermionKey::Alt('b') => Key::AltB,
TermionKey::Alt('c') => Key::AltC,
TermionKey::Alt('d') => Key::AltD,
TermionKey::Alt('e') => Key::AltE,
TermionKey::Alt('f') => Key::AltF,
TermionKey::Alt('g') => Key::AltG,
TermionKey::Alt('h') => Key::AltH,
TermionKey::Alt('i') => Key::AltI,
TermionKey::Alt('j') => Key::AltJ,
TermionKey::Alt('k') => Key::AltK,
TermionKey::Alt('l') => Key::AltL,
TermionKey::Alt('m') => Key::AltM,
TermionKey::Alt('n') => Key::AltN,
TermionKey::Alt('o') => Key::AltO,
TermionKey::Alt('p') => Key::AltP,
TermionKey::Alt('q') => Key::AltQ,
TermionKey::Alt('r') => Key::AltR,
TermionKey::Alt('s') => Key::AltS,
TermionKey::Alt('t') => Key::AltT,
TermionKey::Alt('u') => Key::AltU,
TermionKey::Alt('v') => Key::AltV,
TermionKey::Alt('w') => Key::AltW,
TermionKey::Alt('x') => Key::AltX,
TermionKey::Alt('y') => Key::AltY,
TermionKey::Alt('z') => Key::AltZ,
TermionKey::Char('A') => Key::ShiftA,
TermionKey::Char('B') => Key::ShiftB,
TermionKey::Char('C') => Key::ShiftC,
TermionKey::Char('D') => Key::ShiftD,
TermionKey::Char('E') => Key::ShiftE,
TermionKey::Char('F') => Key::ShiftF,
TermionKey::Char('G') => Key::ShiftG,
TermionKey::Char('H') => Key::ShiftH,
TermionKey::Char('I') => Key::ShiftI,
TermionKey::Char('J') => Key::ShiftJ,
TermionKey::Char('K') => Key::ShiftK,
TermionKey::Char('L') => Key::ShiftL,
TermionKey::Char('M') => Key::ShiftM,
TermionKey::Char('N') => Key::ShiftN,
TermionKey::Char('O') => Key::ShiftO,
TermionKey::Char('P') => Key::ShiftP,
TermionKey::Char('Q') => Key::ShiftQ,
TermionKey::Char('R') => Key::ShiftR,
TermionKey::Char('S') => Key::ShiftS,
TermionKey::Char('T') => Key::ShiftT,
TermionKey::Char('U') => Key::ShiftU,
TermionKey::Char('V') => Key::ShiftV,
TermionKey::Char('W') => Key::ShiftW,
TermionKey::Char('X') => Key::ShiftX,
TermionKey::Char('Y') => Key::ShiftY,
TermionKey::Char('Z') => Key::ShiftZ,
TermionKey::Ctrl('A') => Key::CtrlShiftA,
TermionKey::Ctrl('B') => Key::CtrlShiftB,
TermionKey::Ctrl('C') => Key::CtrlShiftC,
TermionKey::Ctrl('D') => Key::CtrlShiftD,
TermionKey::Ctrl('E') => Key::CtrlShiftE,
TermionKey::Ctrl('F') => Key::CtrlShiftF,
TermionKey::Ctrl('G') => Key::CtrlShiftG,
TermionKey::Ctrl('H') => Key::CtrlShiftH,
TermionKey::Ctrl('I') => Key::CtrlShiftI,
TermionKey::Ctrl('J') => Key::CtrlShiftJ,
TermionKey::Ctrl('K') => Key::CtrlShiftK,
TermionKey::Ctrl('L') => Key::CtrlShiftL,
TermionKey::Ctrl('M') => Key::CtrlShiftM,
TermionKey::Ctrl('N') => Key::CtrlShiftN,
TermionKey::Ctrl('O') => Key::CtrlShiftO,
TermionKey::Ctrl('P') => Key::CtrlShiftP,
TermionKey::Ctrl('Q') => Key::CtrlShiftQ,
TermionKey::Ctrl('R') => Key::CtrlShiftR,
TermionKey::Ctrl('S') => Key::CtrlShiftS,
TermionKey::Ctrl('T') => Key::CtrlShiftT,
TermionKey::Ctrl('U') => Key::CtrlShiftU,
TermionKey::Ctrl('V') => Key::CtrlShiftV,
TermionKey::Ctrl('W') => Key::CtrlShiftW,
TermionKey::Ctrl('X') => Key::CtrlShiftX,
TermionKey::Ctrl('Y') => Key::CtrlShiftY,
TermionKey::Ctrl('Z') => Key::CtrlShiftZ,
TermionKey::Alt('A') => Key::AltShiftA,
TermionKey::Alt('B') => Key::AltShiftB,
TermionKey::Alt('C') => Key::AltShiftC,
TermionKey::Alt('D') => Key::AltShiftD,
TermionKey::Alt('E') => Key::AltShiftE,
TermionKey::Alt('F') => Key::AltShiftF,
TermionKey::Alt('G') => Key::AltShiftG,
TermionKey::Alt('H') => Key::AltShiftH,
TermionKey::Alt('I') => Key::AltShiftI,
TermionKey::Alt('J') => Key::AltShiftJ,
TermionKey::Alt('K') => Key::AltShiftK,
TermionKey::Alt('L') => Key::AltShiftL,
TermionKey::Alt('M') => Key::AltShiftM,
TermionKey::Alt('N') => Key::AltShiftN,
TermionKey::Alt('O') => Key::AltShiftO,
TermionKey::Alt('P') => Key::AltShiftP,
TermionKey::Alt('Q') => Key::AltShiftQ,
TermionKey::Alt('R') => Key::AltShiftR,
TermionKey::Alt('S') => Key::AltShiftS,
TermionKey::Alt('T') => Key::AltShiftT,
TermionKey::Alt('U') => Key::AltShiftU,
TermionKey::Alt('V') => Key::AltShiftV,
TermionKey::Alt('W') => Key::AltShiftW,
TermionKey::Alt('X') => Key::AltShiftX,
TermionKey::Alt('Y') => Key::AltShiftY,
TermionKey::Alt('Z') => Key::AltShiftZ,
TermionKey::Char('+') => Key::Plus,
TermionKey::Char('-') => Key::Minus,
TermionKey::Char('`') => Key::Backtick,
TermionKey::Char('~') => Key::Tilda,
TermionKey::Char('_') => Key::Underscore,
TermionKey::Char('=') => Key::Equals,
TermionKey::Char(';') => Key::Semicolon,
TermionKey::Char(':') => Key::Colon,
TermionKey::Char('\'') => Key::SingleQuote,
TermionKey::Char('"') => Key::DoubleQuote,
TermionKey::Char('/') => Key::ForwardSlash,
TermionKey::Char('\\') => Key::BackSlash,
TermionKey::Char('.') => Key::Dot,
TermionKey::Char(',') => Key::Comma,
TermionKey::Char('?') => Key::QuestionMark,
_ => Key::NotSupported,
}
}
}

@ -0,0 +1,5 @@
pub mod ui;
pub mod input;
pub mod config;
pub mod app;
pub mod error;

@ -0,0 +1,61 @@
use handlebars::Handlebars;
use std::io;
use termion::{input::MouseTerminal, input::TermRead, raw::IntoRawMode, screen::AlternateScreen};
use tui::backend::CrosstermBackend;
use tui::widgets::{ListState, TableState};
use tui::Terminal;
use xplr::app;
use xplr::error::Error;
use xplr::input::Key;
use xplr::ui;
fn main() -> Result<(), Error> {
let mut app = app::create()?;
let mut hb = Handlebars::new();
hb.register_template_string(
app::TEMPLATE_TABLE_ROW,
&app.config
.clone()
.general
.table
.row
.cols
.iter()
.map(|c| c.format.to_string())
.collect::<Vec<String>>()
.join("\t"),
)?;
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let stdin = io::stdin();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let keys = stdin
.keys()
.map(|e| e.map_or(Key::NotSupported, |e| Key::from_termion_event(e)));
let mut table_state = TableState::default();
let mut list_state = ListState::default();
terminal.draw(|f| ui::draw(&app, &hb, f, &mut table_state, &mut list_state))?;
'outer: for key in keys {
if let Some(actions) = app.actions_from_key(key) {
for action in actions.iter() {
app = app.handle(action)?;
terminal.draw(|f| ui::draw(&app, &hb, f, &mut table_state, &mut list_state))?;
if app.result.is_some() {
break 'outer;
}
}
};
}
std::mem::drop(terminal);
println!("{}", app.result.unwrap_or("".into()));
Ok(())
}

@ -0,0 +1,120 @@
use handlebars::Handlebars;
use tui::backend::Backend;
use tui::layout::{Constraint as TUIConstraint, Direction, Layout};
use tui::widgets::{Block, Borders, Cell, List, ListItem, ListState, Row, Table, TableState};
use tui::Frame;
use crate::app;
pub fn draw<B: Backend>(
app: &app::App,
hb: &Handlebars,
f: &mut Frame<B>,
table_state: &mut TableState,
list_state: &mut ListState,
) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints([TUIConstraint::Percentage(70), TUIConstraint::Percentage(30)].as_ref())
.split(f.size());
let body = app
.directory_buffer
.items
.iter()
.map(|(_, m)| {
let txt = hb
.render(app::TEMPLATE_TABLE_ROW, &m)
.ok()
.unwrap_or_else(|| app::UNSUPPORTED_STR.into())
.split("\t")
.map(|x| Cell::from(x.to_string()))
.collect::<Vec<Cell>>();
let style = if m.is_focused {
app.config.general.focused_ui.style
} else if m.is_selected {
app.config.general.selected_ui.style
} else {
app.config
.filetypes
.special
.get(&m.relative_path)
.or_else(|| app.config.filetypes.extension.get(&m.extension))
.unwrap_or_else(|| {
if m.is_symlink {
&app.config.filetypes.symlink
} else if m.is_dir {
&app.config.filetypes.directory
} else {
&app.config.filetypes.file
}
})
.style
};
(txt, style)
})
.map(|(t, s)| Row::new(t).style(s))
.collect::<Vec<Row>>();
let table_constraints: Vec<TUIConstraint> = app
.config
.general
.table
.col_widths
.clone()
.into_iter()
.map(|c| c.into())
.collect();
let table = Table::new(body)
.widths(&table_constraints)
.style(app.config.general.table.style)
.highlight_style(app.config.general.focused_ui.style)
.column_spacing(app.config.general.table.col_spacing)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!(" {} ", app.pwd.to_str().unwrap_or("???"))),
);
let table = app
.config
.general
.table
.header
.clone()
.map(|h| {
table.clone().header(
Row::new(
h.cols
.iter()
.map(|c| Cell::from(c.format.to_owned()))
.collect::<Vec<Cell>>(),
)
.height(h.height)
.style(h.style),
)
})
.unwrap_or_else(|| table.clone());
table_state.select(
app.directory_buffer
.focus
.map(app::DirectoryBuffer::relative_focus),
);
f.render_stateful_widget(table, chunks[0], table_state);
let selected: Vec<ListItem> = app
.selected_paths
.iter()
.map(|p| p.to_str().unwrap_or(app::UNSUPPORTED_STR))
.map(String::from)
.map(ListItem::new)
.collect();
// Selected items
let selected_list =
List::new(selected).block(Block::default().borders(Borders::ALL).title(" Selected "));
f.render_stateful_widget(selected_list, chunks[1], list_state);
}

@ -0,0 +1,16 @@
use xplr::*;
#[test]
fn test_key_down() {
let mut app = app::create().expect("failed to create app");
assert_eq!(app.directory_buffer.focus, Some(0));
let actions = app.actions_from_key(input::Key::Down).unwrap();
for action in actions {
app = app.handle(&action).unwrap()
}
assert_eq!(app.directory_buffer.focus, Some(1));
}
Loading…
Cancel
Save