mirror of https://github.com/sayanarijit/xplr
Not yet doing what it's supposed to
commit
f9c3edee06
@ -0,0 +1 @@
|
||||
/target
|
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…
Reference in New Issue