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