Too big of a rewrite

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

37
Cargo.lock generated

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.15"
@ -473,6 +475,22 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "0.7.9"
@ -1001,6 +1019,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
@ -1019,6 +1046,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.1"
@ -1133,13 +1166,15 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xplr"
version = "0.1.13"
version = "0.2.0"
dependencies = [
"criterion",
"crossterm",
"dirs",
"handlebars",
"mime_guess",
"serde",
"serde_json",
"serde_yaml",
"shellwords",
"termion",

@ -1,6 +1,6 @@
[package]
name = "xplr"
version = "0.1.13" # Update app.rs
version = "0.2.0" # Update app.rs
authors = ["Arijit Basu <sayanarijit@gmail.com>"]
edition = "2018"
description = "An experimental, minimal, configurable TUI file explorer, stealing ideas from nnn and fzf."
@ -17,9 +17,11 @@ termion = "1.5"
crossterm = "0.18"
dirs = "3.0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
handlebars = "3.5.3"
shellwords = "1.0.0"
mime_guess = "2.0.3"
[dev-dependencies]
criterion = "0.3"

File diff suppressed because it is too large Load Diff

@ -1,349 +1,23 @@
use crate::app::ExternalMsg;
use crate::app::VERSION;
use crate::input::Key;
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use std::fs;
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),
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Explore => {
write!(f, "explore")
}
Self::Select => {
write!(f, "select")
}
Self::ExploreSubmode(s) => {
write!(f, "explore({})", &s)
}
Self::SelectSubmode(s) => {
write!(f, "select({})", &s)
}
}
}
}
impl Mode {
pub fn does_support(self, action: &Action) -> bool {
match (self, action) {
// Special
(_, Action::Terminate) => true,
(_, Action::NumberInput(_)) => true,
// Explore mode
(Self::Explore, Action::Back) => true,
(Self::Explore, Action::Call(_)) => true,
(Self::Explore, Action::ChangeDirectory(_)) => true,
(Self::Explore, Action::Enter) => true,
(Self::Explore, Action::EnterSubmode(_)) => true,
(Self::Explore, Action::ExitSubmode) => false,
(Self::Explore, Action::FocusFirst) => true,
(Self::Explore, Action::FocusLast) => true,
(Self::Explore, Action::FocusNext) => true,
(Self::Explore, Action::FocusPath(_)) => true,
(Self::Explore, Action::FocusPathByBufferRelativeIndex(_)) => true,
(Self::Explore, Action::FocusPathByFocusRelativeIndex(_)) => true,
(Self::Explore, Action::FocusPathByIndex(_)) => true,
(Self::Explore, Action::FocusPrevious) => true,
(Self::Explore, Action::PrintAppState) => true,
(Self::Explore, Action::PrintFocused) => true,
(Self::Explore, Action::PrintSelected) => false,
(Self::Explore, Action::Quit) => true,
(Self::Explore, Action::Select) => true,
(Self::Explore, Action::ToggleSelection) => false,
(Self::Explore, Action::ToggleShowHidden) => true,
// Explore submode
(Self::ExploreSubmode(_), Action::ExitSubmode) => true,
(Self::ExploreSubmode(_), a) => Self::does_support(Self::Explore, a),
// Select mode
(Self::Select, Action::Back) => true,
(Self::Select, Action::Call(_)) => true,
(Self::Select, Action::ChangeDirectory(_)) => true,
(Self::Select, Action::Enter) => true,
(Self::Select, Action::EnterSubmode(_)) => true,
(Self::Select, Action::ExitSubmode) => true,
(Self::Select, Action::FocusFirst) => true,
(Self::Select, Action::FocusLast) => true,
(Self::Select, Action::FocusNext) => true,
(Self::Select, Action::FocusPath(_)) => true,
(Self::Select, Action::FocusPathByBufferRelativeIndex(_)) => true,
(Self::Select, Action::FocusPathByFocusRelativeIndex(_)) => true,
(Self::Select, Action::FocusPathByIndex(_)) => true,
(Self::Select, Action::FocusPrevious) => true,
(Self::Select, Action::PrintAppState) => true,
(Self::Select, Action::PrintFocused) => false,
(Self::Select, Action::PrintSelected) => true,
(Self::Select, Action::Quit) => true,
(Self::Select, Action::Select) => false,
(Self::Select, Action::ToggleSelection) => true,
(Self::Select, Action::ToggleShowHidden) => true,
// Select submode
(Self::SelectSubmode(_), Action::ExitSubmode) => true,
(Self::SelectSubmode(_), a) => Self::does_support(Self::Select, a),
}
}
}
#[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 struct CommandConfig {
pub command: String,
pub struct Action {
#[serde(default)]
pub args: Vec<String>,
}
pub help: Option<String>,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Action {
NumberInput(u8),
ToggleShowHidden,
Back,
Enter,
FocusPrevious,
FocusNext,
FocusFirst,
FocusLast,
FocusPathByIndex(usize),
FocusPathByBufferRelativeIndex(usize),
FocusPathByFocusRelativeIndex(isize),
FocusPath(String),
ChangeDirectory(String),
Call(CommandConfig),
EnterSubmode(String),
ExitSubmode,
Select,
// Unselect,
// SelectAll,
// SelectAllRecursive,
// UnselectAll,
// UnSelectAllRecursive,
ToggleSelection,
// ClearSelectedPaths,
// Quit options
PrintFocused,
PrintSelected,
PrintAppState,
Quit,
Terminate,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ActionMenu {
#[serde(default)]
pub help: String,
pub actions: Vec<Action>,
}
pub type SubmodeActionMenu = HashMap<Key, ActionMenu>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindings {
pub global: HashMap<Key, ActionMenu>,
#[serde(default)]
pub explore_mode: HashMap<Key, ActionMenu>,
#[serde(default)]
pub explore_submodes: HashMap<String, SubmodeActionMenu>,
#[serde(default)]
pub select_mode: HashMap<Key, ActionMenu>,
#[serde(default)]
pub select_submodes: HashMap<String, SubmodeActionMenu>,
}
impl KeyBindings {
pub fn filtered(&self, mode: &Mode) -> HashMap<Key, (String, Vec<Action>)> {
let mode_bindings: Option<HashMap<Key, ActionMenu>> = match mode {
Mode::Explore => Some(self.explore_mode.clone()),
Mode::ExploreSubmode(s) => self.explore_submodes.clone().get(s).map(|a| a.to_owned()),
Mode::Select => Some(self.select_mode.clone()),
Mode::SelectSubmode(s) => self.select_submodes.clone().get(s).map(|a| a.to_owned()),
};
let kb = self.global.clone().into_iter();
let kb: HashMap<Key, ActionMenu> = if let Some(modal_kb) = mode_bindings {
kb.chain(modal_kb.into_iter()).collect()
} else {
kb.collect()
};
kb.into_iter()
.map(|(k, am)| {
(
k.clone(),
(
am.help,
am.actions
.into_iter()
.filter(|a| mode.clone().does_support(a))
.collect::<Vec<Action>>(),
),
)
})
.filter(|(_, (_, actions))| !actions.is_empty())
.collect()
}
}
impl Default for KeyBindings {
fn default() -> Self {
let yaml = r###"
global:
ctrl-c:
help: quit
actions:
- Terminate
q:
help: quit
actions:
- Quit
pound:
help: print debug info
actions:
- PrintAppState
up:
help: up
actions:
- FocusPrevious
down:
help: down
actions:
- FocusNext
shift-g:
help: bottom
actions:
- FocusLast
tilde:
help: go home
actions:
- ChangeDirectory: "~"
dot:
help: toggle show hidden
actions:
- ToggleShowHidden
right:
help: enter
actions:
- Enter
left:
help: back
actions:
- Back
o:
help: open
actions:
- Call:
command: bash
args:
- "-c"
- FILE="{{relativePath}}" && xdg-open "${FILE:?}" &> /dev/null
e:
help: edit
actions:
- Call:
command: bash
args:
- -c
- FILE="{{relativePath}}" && "${EDITOR:-vim}" "${FILE:?}"
forward-slash:
help: search
actions:
- Call:
command: bash
args:
- "-c"
- FILE="$(ls -a | fzf)" && xplr "${FILE:?}" || xplr "${PWD:?}"
- Quit
s:
help: shell
actions:
- Call:
command: bash
esc:
help: quit
actions:
- Quit
explore_mode:
g:
help: go to
actions:
- EnterSubmode: GoTo
return:
help: done
actions:
- PrintFocused
space:
help: select
actions:
- Select
- FocusNext
explore_submodes:
GoTo:
g:
help: top
actions:
- FocusFirst
- ExitSubmode
select_mode:
space:
help: toggle selection
actions:
- ToggleSelection
- FocusNext
g:
help: go to
actions:
- EnterSubmode: GoTo
return:
help: done
actions:
- PrintSelected
- Quit
select_submodes:
GoTo:
g:
help: top
actions:
- FocusFirst
- ExitSubmode
"###;
serde_yaml::from_str(yaml).unwrap()
}
pub messages: Vec<ExternalMsg>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -363,6 +37,8 @@ pub struct FileTypesConfig {
#[serde(default)]
pub symlink: FileTypeConfig,
#[serde(default)]
pub mime_essence: HashMap<String, FileTypeConfig>,
#[serde(default)]
pub extension: HashMap<String, FileTypeConfig>,
#[serde(default)]
pub special: HashMap<String, FileTypeConfig>,
@ -390,6 +66,7 @@ impl Default for FileTypesConfig {
.fg(Color::Cyan),
},
mime_essence: Default::default(),
extension: Default::default(),
special: Default::default(),
}
@ -472,12 +149,16 @@ pub struct TableConfig {
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,
}
@ -489,9 +170,9 @@ impl Default for GeneralConfig {
table:
header:
cols:
- format: "│ path"
- format: "is symlink"
- format: "index"
- format: "│ path"
- format: "type"
- format: " index"
height: 1
style:
add_modifier:
@ -500,9 +181,9 @@ impl Default for GeneralConfig {
bits: 0
row:
cols:
- format: "{{tree}}{{prefix}}{{icon}} {{relativePath}}{{#if isDir}}/{{/if}}{{suffix}}"
- format: "{{isSymlink}}"
- format: "{{focusRelativeIndex}}/{{bufferRelativeIndex}}/{{index}}/{{total}}"
- format: "{{{tree}}}{{{prefix}}}{{{icon}}} {{{relativePath}}}{{#if isDir}}/{{/if}}{{{suffix}}}"
- format: "{{{mimeEssence}}}"
- format: "{{#if isBeforeFocus}}-{{else}} {{/if}}{{{relativeIndex}}}/{{{index}}}/{{{total}}}"
col_spacing: 3
col_widths:
@ -543,6 +224,203 @@ impl Default for GeneralConfig {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipesConfig {
pub msg_in: String,
pub focus_out: String,
pub selected_out: String,
pub mode_out: String,
}
impl Default for PipesConfig {
fn default() -> Self {
let pipesdir = dirs::runtime_dir()
.unwrap_or("/tmp".into())
.join("xplr")
.join("session")
.join(std::process::id().to_string())
.join("pipe");
fs::create_dir_all(&pipesdir).unwrap();
let msg_in = pipesdir.join("msg_in").to_string_lossy().to_string();
let focus_out = pipesdir.join("focus_out").to_string_lossy().to_string();
let selected_out = pipesdir.join("selected_out").to_string_lossy().to_string();
let mode_out = pipesdir.join("mode_out").to_string_lossy().to_string();
fs::write(&msg_in, "").unwrap();
fs::write(&focus_out, "").unwrap();
fs::write(&selected_out, "").unwrap();
fs::write(&mode_out, "").unwrap();
Self {
msg_in,
focus_out,
selected_out,
mode_out,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindings {
#[serde(default)]
pub on_key: BTreeMap<String, Action>,
#[serde(default)]
pub on_alphabet: Option<Action>,
#[serde(default)]
pub on_number: Option<Action>,
#[serde(default)]
pub on_special_character: Option<Action>,
#[serde(default)]
pub default: Option<Action>,
}
impl Default for KeyBindings {
fn default() -> Self {
let on_key: BTreeMap<String, Action> = serde_yaml::from_str(
r###"
up:
help: up
messages:
- FocusPrevious
down:
help: down
messages:
- FocusNext
right:
help: enter
messages:
- Enter
left:
help: back
messages:
- Back
g:
help: go to
messages:
- SwitchMode: goto
G:
help: bottom
messages:
- FocusLast
s:
help: shell
messages:
- Call:
command: bash
args: []
/:
help: find
messages:
- Call:
command: bash
args:
- "-c"
- |
PTH="$(echo -e ${XPLR_DIRECTORY_NODES:?} | sed -s 's/,/\n/g' | fzf)"
if [ -d "$PTH" ]; then
echo "ChangeDirectory: ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}"
elif [ -f "$PTH" ]; then
echo "FocusPath: ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}"
fi
space:
help: toggle selection
messages:
- ToggleSelection
- FocusNext
d:
help: debug
messages:
- Debug: /tmp/xplr.yml
enter:
help: quit with result
messages:
- PrintResultAndQuit
"#":
help: quit with debug
messages:
- PrintAppStateAndQuit
esc:
help: cancel & quit
messages:
- Terminate
q:
help: cancel & quit
messages:
- Terminate
"###,
)
.unwrap();
let default = Some(Action {
help: None,
messages: vec![ExternalMsg::SwitchMode("default".into())],
});
let on_number = Some(Action {
help: Some("input".to_string()),
messages: vec![
ExternalMsg::BufferStringFromKey,
ExternalMsg::SwitchMode("number".into()),
],
});
Self {
on_key,
on_alphabet: Default::default(),
on_number,
on_special_character: Default::default(),
default,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Mode {
pub name: String,
#[serde(default)]
pub help: Option<String>,
#[serde(default)]
pub extra_help: Option<String>,
#[serde(default)]
pub key_bindings: KeyBindings,
}
impl Default for Mode {
fn default() -> Self {
Self {
name: "default".into(),
help: Default::default(),
extra_help: Default::default(),
key_bindings: Default::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub version: String,
@ -554,16 +432,78 @@ pub struct Config {
pub filetypes: FileTypesConfig,
#[serde(default)]
pub key_bindings: KeyBindings,
pub pipes: PipesConfig,
#[serde(default)]
pub modes: HashMap<String, Mode>,
}
impl Default for Config {
fn default() -> Self {
let goto_mode: Mode = serde_yaml::from_str(
r###"
name: go to
key_bindings:
on_key:
g:
help: top
messages:
- FocusFirst
- SwitchMode: default
"###,
)
.unwrap();
let number_mode: Mode = serde_yaml::from_str(
r###"
name: number
key_bindings:
on_key:
up:
help: go up
messages:
- FocusPreviousByRelativeIndexFromInput
- ResetInputBuffer
- SwitchMode: default
down:
help: go down
messages:
- FocusNextByRelativeIndexFromInput
- ResetInputBuffer
- SwitchMode: default
enter:
help: go down
messages:
- FocusByIndexFromInput
- ResetInputBuffer
- SwitchMode: default
on_number:
help: input
messages:
- BufferStringFromKey
default:
messages:
- ResetInputBuffer
- SwitchMode: default
"###,
)
.unwrap();
let mut modes: HashMap<String, Mode> = Default::default();
modes.insert("default".into(), Mode::default());
modes.insert("goto".into(), goto_mode);
modes.insert("number".into(), number_mode);
Self {
version: VERSION.into(),
general: Default::default(),
filetypes: Default::default(),
key_bindings: Default::default(),
pipes: Default::default(),
modes,
}
}
}

@ -0,0 +1,59 @@
use crate::app::DirectoryBuffer;
use crate::app::Node;
use crate::app::Task;
use crate::app::{InternalMsg, MsgIn};
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
use std::thread;
pub fn explore(parent: String, focused_path: Option<String>, tx: Sender<Task>) {
let path = PathBuf::from(&parent);
let path_cloned = path.clone();
let tx_cloned = tx.clone();
thread::spawn(move || {
let nodes: Vec<Node> = fs::read_dir(&path)
.unwrap()
.filter_map(|d| {
d.ok().map(|e| {
e.path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
})
})
.map(|name| Node::new(parent.clone(), name))
.collect();
let focus_index = if let Some(focus) = focused_path {
nodes
.iter()
.enumerate()
.find(|(_, n)| n.relative_path == focus)
.map(|(i, _)| i)
.unwrap_or(0)
} else {
0
};
let dir = DirectoryBuffer::new(parent.clone(), nodes, focus_index);
tx.send(Task::new(
1,
MsgIn::Internal(InternalMsg::AddDirectory(parent, dir)),
None,
))
.unwrap();
});
if let Some(grand_parent) = path_cloned.parent() {
explore(
grand_parent.to_string_lossy().to_string(),
path_cloned
.file_name()
.map(|f| f.to_string_lossy().to_string()),
tx_cloned,
);
}
}

@ -1,10 +1,45 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
use termion::event::Key as TermionKey;
use serde_yaml;
use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Key {
Number(u8),
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Num0,
Num1,
Num2,
Num3,
Num4,
Num5,
Num6,
Num7,
Num8,
Num9,
AltNum0,
AltNum1,
AltNum2,
AltNum3,
AltNum4,
AltNum5,
AltNum6,
AltNum7,
AltNum8,
AltNum9,
Backspace,
Left,
@ -18,7 +53,7 @@ pub enum Key {
BackTab,
Delete,
Insert,
Return,
Enter,
Space,
Tab,
Esc,
@ -131,297 +166,401 @@ pub enum Key {
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,
Tilde,
Underscore,
Equals,
Semicolon,
Colon,
SingleQuote,
DoubleQuote,
ForwardSlash,
BackSlash,
Dot,
Comma,
QuestionMark,
Pound,
Special(char),
NotSupported,
}
impl std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
let key_str = self.to_char().map(|c| c.to_string()).unwrap_or_else(|| {
serde_yaml::to_value(self)
.ok()
.and_then(|v| v.as_str().map(|v| v.to_string()))
.unwrap_or_default()
});
write!(f, "{}", key_str)
}
}
impl Key {
pub fn from_termion_event(key: TermionKey) -> Self {
match key {
TermionKey::Char('0') => Key::Number(0),
TermionKey::Char('1') => Key::Number(1),
TermionKey::Char('2') => Key::Number(2),
TermionKey::Char('3') => Key::Number(3),
TermionKey::Char('4') => Key::Number(4),
TermionKey::Char('5') => Key::Number(5),
TermionKey::Char('6') => Key::Number(6),
TermionKey::Char('7') => Key::Number(7),
TermionKey::Char('8') => Key::Number(8),
TermionKey::Char('9') => Key::Number(9),
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::Esc,
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::Tilde,
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,
TermionKey::Char('#') => Key::Pound,
_ => Key::NotSupported,
pub fn from_event(key: KeyEvent) -> Self {
match key.modifiers {
KeyModifiers::CONTROL => match key.code {
KeyCode::Char('a') => Key::CtrlA,
KeyCode::Char('b') => Key::CtrlB,
KeyCode::Char('c') => Key::CtrlC,
KeyCode::Char('d') => Key::CtrlD,
KeyCode::Char('e') => Key::CtrlE,
KeyCode::Char('f') => Key::CtrlF,
KeyCode::Char('g') => Key::CtrlG,
KeyCode::Char('h') => Key::CtrlH,
KeyCode::Char('i') => Key::CtrlI,
KeyCode::Char('j') => Key::CtrlJ,
KeyCode::Char('k') => Key::CtrlK,
KeyCode::Char('l') => Key::CtrlL,
KeyCode::Char('m') => Key::CtrlM,
KeyCode::Char('n') => Key::CtrlN,
KeyCode::Char('o') => Key::CtrlO,
KeyCode::Char('p') => Key::CtrlP,
KeyCode::Char('q') => Key::CtrlQ,
KeyCode::Char('r') => Key::CtrlR,
KeyCode::Char('s') => Key::CtrlS,
KeyCode::Char('t') => Key::CtrlT,
KeyCode::Char('u') => Key::CtrlU,
KeyCode::Char('v') => Key::CtrlV,
KeyCode::Char('w') => Key::CtrlW,
KeyCode::Char('x') => Key::CtrlX,
KeyCode::Char('y') => Key::CtrlY,
KeyCode::Char('z') => Key::CtrlZ,
KeyCode::Char(c) => c.into(),
_ => Key::NotSupported,
},
KeyModifiers::ALT => match key.code {
KeyCode::Char('0') => Key::AltNum0,
KeyCode::Char('1') => Key::AltNum1,
KeyCode::Char('2') => Key::AltNum2,
KeyCode::Char('3') => Key::AltNum3,
KeyCode::Char('4') => Key::AltNum4,
KeyCode::Char('5') => Key::AltNum5,
KeyCode::Char('6') => Key::AltNum6,
KeyCode::Char('7') => Key::AltNum7,
KeyCode::Char('8') => Key::AltNum8,
KeyCode::Char('9') => Key::AltNum9,
KeyCode::Char('a') => Key::AltA,
KeyCode::Char('b') => Key::AltB,
KeyCode::Char('c') => Key::AltC,
KeyCode::Char('d') => Key::AltD,
KeyCode::Char('e') => Key::AltE,
KeyCode::Char('f') => Key::AltF,
KeyCode::Char('g') => Key::AltG,
KeyCode::Char('h') => Key::AltH,
KeyCode::Char('i') => Key::AltI,
KeyCode::Char('j') => Key::AltJ,
KeyCode::Char('k') => Key::AltK,
KeyCode::Char('l') => Key::AltL,
KeyCode::Char('m') => Key::AltM,
KeyCode::Char('n') => Key::AltN,
KeyCode::Char('o') => Key::AltO,
KeyCode::Char('p') => Key::AltP,
KeyCode::Char('q') => Key::AltQ,
KeyCode::Char('r') => Key::AltR,
KeyCode::Char('s') => Key::AltS,
KeyCode::Char('t') => Key::AltT,
KeyCode::Char('u') => Key::AltU,
KeyCode::Char('v') => Key::AltV,
KeyCode::Char('w') => Key::AltW,
KeyCode::Char('x') => Key::AltX,
KeyCode::Char('y') => Key::AltY,
KeyCode::Char('z') => Key::AltZ,
KeyCode::Char(c) => c.into(),
_ => Key::NotSupported,
},
_ => match key.code {
KeyCode::F(1) => Key::F1,
KeyCode::F(2) => Key::F2,
KeyCode::F(3) => Key::F3,
KeyCode::F(4) => Key::F4,
KeyCode::F(5) => Key::F5,
KeyCode::F(6) => Key::F6,
KeyCode::F(7) => Key::F7,
KeyCode::F(8) => Key::F8,
KeyCode::F(9) => Key::F9,
KeyCode::F(10) => Key::F10,
KeyCode::F(11) => Key::F11,
KeyCode::F(12) => Key::F12,
KeyCode::Backspace => Key::Backspace,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::BackTab => Key::BackTab,
KeyCode::Delete => Key::Delete,
KeyCode::Insert => Key::Insert,
KeyCode::Enter => Key::Enter,
KeyCode::Tab => Key::Tab,
KeyCode::Esc => Key::Esc,
KeyCode::Char(c) => c.into(),
_ => Key::NotSupported,
},
}
}
pub fn is_alphabet(&self) -> bool {
match self {
Self::A => true,
Self::B => true,
Self::C => true,
Self::D => true,
Self::E => true,
Self::F => true,
Self::G => true,
Self::H => true,
Self::I => true,
Self::J => true,
Self::K => true,
Self::L => true,
Self::M => true,
Self::N => true,
Self::O => true,
Self::P => true,
Self::Q => true,
Self::R => true,
Self::S => true,
Self::T => true,
Self::U => true,
Self::V => true,
Self::W => true,
Self::X => true,
Self::Y => true,
Self::Z => true,
Self::ShiftA => true,
Self::ShiftB => true,
Self::ShiftC => true,
Self::ShiftD => true,
Self::ShiftE => true,
Self::ShiftF => true,
Self::ShiftG => true,
Self::ShiftH => true,
Self::ShiftI => true,
Self::ShiftJ => true,
Self::ShiftK => true,
Self::ShiftL => true,
Self::ShiftM => true,
Self::ShiftN => true,
Self::ShiftO => true,
Self::ShiftP => true,
Self::ShiftQ => true,
Self::ShiftR => true,
Self::ShiftS => true,
Self::ShiftT => true,
Self::ShiftU => true,
Self::ShiftV => true,
Self::ShiftW => true,
Self::ShiftX => true,
Self::ShiftY => true,
Self::ShiftZ => true,
_ => false,
}
}
pub fn is_number(&self) -> bool {
match self {
Self::Num0 => true,
Self::Num1 => true,
Self::Num2 => true,
Self::Num3 => true,
Self::Num4 => true,
Self::Num5 => true,
Self::Num6 => true,
Self::Num7 => true,
Self::Num8 => true,
Self::Num9 => true,
_ => false,
}
}
pub fn is_special_character(&self) -> bool {
match self {
Self::Special(_) => true,
_ => false,
}
}
pub fn to_char(&self) -> Option<char> {
match self {
Self::Num0 => Some('0'),
Self::Num1 => Some('1'),
Self::Num2 => Some('2'),
Self::Num3 => Some('3'),
Self::Num4 => Some('4'),
Self::Num5 => Some('5'),
Self::Num6 => Some('6'),
Self::Num7 => Some('7'),
Self::Num8 => Some('8'),
Self::Num9 => Some('9'),
Self::A => Some('a'),
Self::B => Some('b'),
Self::C => Some('c'),
Self::D => Some('d'),
Self::E => Some('e'),
Self::F => Some('f'),
Self::G => Some('g'),
Self::H => Some('h'),
Self::I => Some('i'),
Self::J => Some('j'),
Self::K => Some('k'),
Self::L => Some('l'),
Self::M => Some('m'),
Self::N => Some('n'),
Self::O => Some('o'),
Self::P => Some('p'),
Self::Q => Some('q'),
Self::R => Some('r'),
Self::S => Some('s'),
Self::T => Some('t'),
Self::U => Some('u'),
Self::V => Some('v'),
Self::W => Some('w'),
Self::X => Some('x'),
Self::Y => Some('y'),
Self::Z => Some('z'),
Self::ShiftA => Some('A'),
Self::ShiftB => Some('B'),
Self::ShiftC => Some('C'),
Self::ShiftD => Some('D'),
Self::ShiftE => Some('E'),
Self::ShiftF => Some('F'),
Self::ShiftG => Some('G'),
Self::ShiftH => Some('H'),
Self::ShiftI => Some('I'),
Self::ShiftJ => Some('J'),
Self::ShiftK => Some('K'),
Self::ShiftL => Some('L'),
Self::ShiftM => Some('M'),
Self::ShiftN => Some('N'),
Self::ShiftO => Some('O'),
Self::ShiftP => Some('P'),
Self::ShiftQ => Some('Q'),
Self::ShiftR => Some('R'),
Self::ShiftS => Some('S'),
Self::ShiftT => Some('T'),
Self::ShiftU => Some('U'),
Self::ShiftV => Some('V'),
Self::ShiftW => Some('W'),
Self::ShiftX => Some('X'),
Self::ShiftY => Some('Y'),
Self::ShiftZ => Some('Z'),
Self::Special(c) => Some(c.to_owned()),
_ => None,
}
}
}
impl From<char> for Key {
fn from(c: char) -> Self {
match c {
'0' => Key::Num0,
'1' => Key::Num1,
'2' => Key::Num2,
'3' => Key::Num3,
'4' => Key::Num4,
'5' => Key::Num5,
'6' => Key::Num6,
'7' => Key::Num7,
'8' => Key::Num8,
'9' => Key::Num9,
'a' => Key::A,
'b' => Key::B,
'c' => Key::C,
'd' => Key::D,
'e' => Key::E,
'f' => Key::F,
'g' => Key::G,
'h' => Key::H,
'i' => Key::I,
'j' => Key::J,
'k' => Key::K,
'l' => Key::L,
'm' => Key::M,
'n' => Key::N,
'o' => Key::O,
'p' => Key::P,
'q' => Key::Q,
'r' => Key::R,
's' => Key::S,
't' => Key::T,
'u' => Key::U,
'v' => Key::V,
'w' => Key::W,
'x' => Key::X,
'y' => Key::Y,
'z' => Key::Z,
'A' => Key::ShiftA,
'B' => Key::ShiftB,
'C' => Key::ShiftC,
'D' => Key::ShiftD,
'E' => Key::ShiftE,
'F' => Key::ShiftF,
'G' => Key::ShiftG,
'H' => Key::ShiftH,
'I' => Key::ShiftI,
'J' => Key::ShiftJ,
'K' => Key::ShiftK,
'L' => Key::ShiftL,
'M' => Key::ShiftM,
'N' => Key::ShiftN,
'O' => Key::ShiftO,
'P' => Key::ShiftP,
'Q' => Key::ShiftQ,
'R' => Key::ShiftR,
'S' => Key::ShiftS,
'T' => Key::ShiftT,
'U' => Key::ShiftU,
'V' => Key::ShiftV,
'W' => Key::ShiftW,
'X' => Key::ShiftX,
'Y' => Key::ShiftY,
'Z' => Key::ShiftZ,
' ' => Key::Space,
'\t' => Key::Tab,
'\n' => Key::Enter,
c => Key::Special(c),
}
}
}
impl From<String> for Key {
fn from(string: String) -> Self {
string
.chars()
.next()
.map(|c| c.into())
.unwrap_or(Key::NotSupported)
}
}
impl From<&str> for Key {
fn from(string: &str) -> Self {
string.to_string().into()
}
}
impl Ord for Key {
fn cmp(&self, other: &Self) -> Ordering {
// Notice that the we flip the ordering on costs.
// In case of a tie we compare positions - this step is necessary
// to make implementations of `PartialEq` and `Ord` consistent.
other.to_string().cmp(&self.to_string())
}
}
impl PartialOrd for Key {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

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

@ -1,16 +1,21 @@
use crossterm::event::Event;
use crossterm::terminal as term;
use crossterm::{event, execute};
use handlebars::{handlebars_helper, Handlebars};
use shellwords;
use std::env;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use termion::get_tty;
use termion::{input::TermRead, screen::AlternateScreen};
use tui::backend::CrosstermBackend;
use tui::widgets::{ListState, TableState};
use tui::Terminal;
use xplr::app;
use xplr::app::Task;
use xplr::error::Error;
use xplr::explorer;
use xplr::input::Key;
use xplr::ui;
@ -18,15 +23,29 @@ handlebars_helper!(shellescape: |v: str| format!("{}", shellwords::escape(v)));
handlebars_helper!(readfile: |v: str| fs::read_to_string(v).unwrap_or_default());
fn main() -> Result<(), Error> {
let mut app = app::create()?;
let mut pwd = PathBuf::from(env::args().skip(1).next().unwrap_or(".".into()))
.canonicalize()
.unwrap_or_default();
let mut focused_path = None;
if pwd.is_file() {
focused_path = pwd.file_name().map(|n| n.to_string_lossy().to_string());
pwd = pwd.parent().map(|p| p.into()).unwrap_or_default();
}
let pwd = pwd.to_string_lossy().to_string();
let mut last_pwd = pwd.clone();
let mut app = app::App::new(pwd.clone());
let mut hb = Handlebars::new();
hb.register_helper("shellescape", Box::new(shellescape));
hb.register_helper("readfile", Box::new(readfile));
hb.register_template_string(
app::TEMPLATE_TABLE_ROW,
&app.config
.clone()
&app.config()
.general
.table
.row
@ -37,88 +56,211 @@ fn main() -> Result<(), Error> {
.join("\t"),
)?;
let stdin = io::stdin();
let stdout = get_tty()?;
// let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
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 result = Ok(());
let mut output = None;
let mut table_state = TableState::default();
let mut list_state = ListState::default();
let (tx_key, rx) = mpsc::channel();
let tx_init = tx_key.clone();
let tx_pipe = tx_key.clone();
let tx_explorer = tx_key.clone();
term::enable_raw_mode().unwrap();
terminal.draw(|f| ui::draw(&app, &hb, f, &mut table_state, &mut list_state))?;
let mut stdout = get_tty().unwrap();
// let mut stdout = stdout.lock();
execute!(stdout, term::EnterAlternateScreen).unwrap();
// let stdout = MouseTerminal::from(stdout);
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
terminal.hide_cursor()?;
let mut result = Ok(());
'outer: for key in keys {
if let Some(actions) = app.actions_from_key(&key) {
for action in actions.iter() {
app = match app.handle(action) {
Ok(mut a) => {
terminal
.draw(|f| ui::draw(&a, &hb, f, &mut table_state, &mut list_state))?;
match a.task.clone() {
Task::NoOp => {}
Task::Quit => {
term::disable_raw_mode().unwrap();
std::mem::drop(terminal);
break 'outer;
}
Task::PrintAndQuit(txt) => {
term::disable_raw_mode().unwrap();
std::mem::drop(terminal);
if !txt.is_empty() {
println!("{}", &txt);
};
break 'outer;
}
Task::Call(cmd) => {
term::disable_raw_mode().unwrap();
std::mem::drop(terminal);
if let Some((_, meta)) = a.directory_buffer.focused() {
let _ = std::process::Command::new(cmd.command.clone())
.current_dir(&a.directory_buffer.pwd)
.args(
cmd.args
.iter()
.map(|arg| hb.render_template(arg, &meta).unwrap()),
)
.status();
};
term::enable_raw_mode().unwrap();
let stdout = get_tty()?;
let stdout = AlternateScreen::from(stdout);
let backend = CrosstermBackend::new(stdout);
terminal = Terminal::new(backend)?;
a = a.refresh()?;
terminal.draw(|f| {
ui::draw(&a, &hb, f, &mut table_state, &mut list_state)
})?;
}
};
a.task = Task::NoOp;
a
}
Err(e) => {
term::disable_raw_mode().unwrap();
std::mem::drop(terminal);
result = Err(e);
break 'outer;
let (tx, rx_key) = mpsc::channel();
thread::spawn(move || {
let mut is_paused = false;
loop {
if let Some(paused) = rx_key.try_recv().ok() {
is_paused = paused;
};
if !is_paused {
if event::poll(std::time::Duration::from_millis(1)).unwrap() {
if let Event::Key(key) = event::read().unwrap() {
let key = Key::from_event(key);
let msg = app::MsgIn::Internal(app::InternalMsg::HandleKey(key));
tx_key.send(app::Task::new(0, msg, Some(key))).unwrap();
}
}
}
}
});
let pipe_msg_in = app.config().pipes.msg_in.clone();
thread::spawn(move || loop {
let in_str = fs::read_to_string(&pipe_msg_in).unwrap_or_default();
if !in_str.is_empty() {
let msgs = in_str
.lines()
.filter_map(|s| serde_yaml::from_str::<app::ExternalMsg>(s.trim()).ok());
msgs.for_each(|msg| {
tx_pipe
.send(app::Task::new(2, app::MsgIn::External(msg), None))
.unwrap();
});
fs::write(&pipe_msg_in, "").unwrap();
};
thread::sleep(Duration::from_millis(10));
});
explorer::explore(pwd.clone(), focused_path, tx_init);
'outer: while result.is_ok() {
while let Some(msg) = app.pop_msg_out() {
match msg {
app::MsgOut::Debug(path) => {
fs::write(&path, serde_yaml::to_string(&app).unwrap_or_default())?;
}
app::MsgOut::PrintResultAndQuit => {
let out = if app.selected().is_empty() {
app.focused_node()
.map(|n| n.absolute_path.clone())
.unwrap_or_default()
} else {
app.selected()
.into_iter()
.map(|n| n.absolute_path.clone())
.collect::<Vec<String>>()
.join("\n")
};
output = Some(out);
break 'outer;
}
app::MsgOut::PrintAppStateAndQuit => {
let out = serde_yaml::to_string(&app)?;
output = Some(out);
break 'outer;
}
app::MsgOut::Refresh => {
if app.pwd() != &last_pwd {
explorer::explore(
app.pwd().clone(),
app.focused_node().map(|n| n.relative_path.clone()),
tx_explorer.clone(),
);
last_pwd = app.pwd().to_owned();
};
// UI
terminal.draw(|f| ui::draw(f, &app, &hb)).unwrap();
// Pipes
let focused = app
.focused_node()
.map(|n| n.absolute_path.clone())
.unwrap_or_default();
fs::write(&app.config().pipes.focus_out, focused).unwrap();
let selected = app
.selected()
.iter()
.map(|n| n.absolute_path.clone())
.collect::<Vec<String>>()
.join("\n");
fs::write(&app.config().pipes.selected_out, selected).unwrap();
fs::write(&app.config().pipes.mode_out, &app.mode().name).unwrap();
}
app::MsgOut::Call(cmd) => {
tx.send(true).unwrap();
terminal.clear()?;
term::disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), term::LeaveAlternateScreen).unwrap();
terminal.show_cursor()?;
let focus_path = app
.focused_node()
.map(|n| n.absolute_path.clone())
.unwrap_or_default();
let focus_index = app
.directory_buffer()
.map(|d| d.focus)
.unwrap_or_default()
.to_string();
let selected = app
.selected()
.iter()
.map(|n| n.absolute_path.clone())
.collect::<Vec<String>>()
.join(",");
let directory_nodes = app
.directory_buffer()
.map(|d| {
d.nodes
.iter()
.map(|n| n.absolute_path.clone())
.collect::<Vec<String>>()
.join(",")
})
.unwrap_or_default();
let pipe_msg_in = app.config().pipes.msg_in.clone();
let pipe_focus_out = app.config().pipes.focus_out.clone();
let pipe_selected_out = app.config().pipes.selected_out.clone();
let app_yaml = serde_yaml::to_string(&app).unwrap_or_default();
let _ = std::process::Command::new(cmd.command.clone())
.current_dir(app.pwd())
.env("XPLR_FOCUS_PATH", focus_path)
.env("XPLR_FOCUS_INDEX", focus_index)
.env("XPLR_SELECTED", selected)
.env("XPLR_PIPE_MSG_IN", pipe_msg_in)
.env("XPLR_PIPE_SELECTED_OUT", pipe_selected_out)
.env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out)
.env("XPLR_APP_YAML", app_yaml)
.env("XPLR_DIRECTORY_NODES", directory_nodes)
.args(cmd.args.clone())
.status();
terminal.hide_cursor()?;
execute!(terminal.backend_mut(), term::EnterAlternateScreen).unwrap();
term::enable_raw_mode().unwrap();
tx.send(false).unwrap();
terminal.draw(|f| ui::draw(f, &app, &hb)).unwrap();
}
};
}
for task in rx.try_iter() {
app = app.enqueue(task);
}
let (new_app, new_result) = match app.clone().possibly_mutate() {
Ok(a) => (a, Ok(())),
Err(e) => (app, Err(e)),
};
app = new_app;
result = new_result;
// thread::sleep(Duration::from_millis(10));
}
term::disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), term::LeaveAlternateScreen).unwrap();
terminal.show_cursor()?;
if let Some(out) = output {
println!("{}", out);
}
result

@ -1,66 +1,211 @@
use crate::app;
use crate::app::Node;
use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
use tui::backend::Backend;
use tui::layout::Rect;
use tui::layout::{Constraint as TUIConstraint, Direction, Layout};
use tui::widgets::{
Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table, TableState,
};
use tui::Frame;
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());
const TOTAL_ROWS: usize = 50;
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)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NodeUIMetadata {
// From Node
pub parent: String,
pub relative_path: String,
pub absolute_path: String,
pub extension: String,
pub is_symlink: bool,
pub is_dir: bool,
pub is_file: bool,
pub is_readonly: bool,
pub mime_essence: String,
// Extra
pub index: usize,
pub relative_index: usize,
pub is_before_focus: bool,
pub is_after_focus: bool,
pub tree: String,
pub icon: String,
pub prefix: String,
pub suffix: String,
pub is_selected: bool,
pub is_focused: bool,
pub total: usize,
}
impl NodeUIMetadata {
fn new(
node: &Node,
index: usize,
relative_index: usize,
is_before_focus: bool,
is_after_focus: bool,
tree: String,
icon: String,
prefix: String,
suffix: String,
is_selected: bool,
is_focused: bool,
total: usize,
) -> Self {
Self {
parent: node.parent.clone(),
relative_path: node.relative_path.clone(),
absolute_path: node.absolute_path.clone(),
extension: node.extension.clone(),
is_symlink: node.is_symlink,
is_dir: node.is_dir,
is_file: node.is_file,
is_readonly: node.is_readonly,
mime_essence: node.mime_essence.clone(),
index,
relative_index,
is_before_focus,
is_after_focus,
tree,
icon,
prefix,
suffix,
is_selected,
is_focused,
total,
}
}
}
fn draw_table<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, hb: &Handlebars) {
let config = app.config().to_owned();
let rows = app
.directory_buffer()
.map(|dir| {
let offset = (
dir.focus.max(TOTAL_ROWS) - TOTAL_ROWS,
dir.focus.max(TOTAL_ROWS),
);
dir.nodes
.iter()
.enumerate()
.skip_while(|(i, _)| *i < offset.0)
.take_while(|(i, _)| *i <= offset.1)
.map(|(index, node)| {
let is_focused = dir.focus == index;
// TODO : Optimize
let is_selected = app.selected().contains(&node);
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 = index == 0;
let is_last = index == dir.total.max(1) - 1;
let tree = config
.general
.table
.tree
.clone()
.map(|t| {
if is_last {
t.2.format.clone()
} else if is_first {
t.0.format.clone()
} else {
t.1.format.clone()
}
})
.unwrap_or_default();
let filetype = config
.filetypes
.special
.get(&node.relative_path)
.or_else(|| config.filetypes.extension.get(&node.extension))
.or_else(|| config.filetypes.mime_essence.get(&node.mime_essence))
.unwrap_or_else(|| {
if node.is_symlink {
&config.filetypes.symlink
} else if node.is_dir {
&config.filetypes.directory
} else {
&config.filetypes.file
}
});
let (relative_index, is_before_focus, is_after_focus) = if dir.focus > index {
(dir.focus - index, true, false)
} else if dir.focus < index {
(index - dir.focus, false, true)
} else {
(0, false, false)
};
let meta = NodeUIMetadata::new(
&node,
index,
relative_index,
is_before_focus,
is_after_focus,
tree,
filetype.icon.clone(),
ui.prefix.clone(),
ui.suffix.clone(),
is_selected,
is_focused,
dir.total,
);
let cols = hb
.render(app::TEMPLATE_TABLE_ROW, &meta)
.ok()
.unwrap_or_else(|| app::UNSUPPORTED_STR.into())
.split("\t")
.map(|x| Cell::from(x.to_string()))
.collect::<Vec<Cell>>();
let style = if is_focused {
config.general.focused_ui.style
} else if is_selected {
config.general.selected_ui.style
} else {
config
.filetypes
.special
.get(&node.relative_path)
.or_else(|| config.filetypes.extension.get(&node.extension))
.or_else(|| config.filetypes.mime_essence.get(&node.mime_essence))
.unwrap_or_else(|| {
if node.is_symlink {
&config.filetypes.symlink
} else if node.is_dir {
&config.filetypes.directory
} else {
&config.filetypes.file
}
})
.style
};
Row::new(cols).style(style)
})
.collect::<Vec<Row>>()
})
.map(|(t, s)| Row::new(t).style(s))
.collect::<Vec<Row>>();
.unwrap_or_default();
let table_constraints: Vec<TUIConstraint> = app
.config
let table_constraints: Vec<TUIConstraint> = config
.general
.table
.col_widths
@ -69,18 +214,18 @@ pub fn draw<B: Backend>(
.map(|c| c.into())
.collect();
let table = Table::new(body)
let table = Table::new(rows)
.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.directory_buffer.pwd.to_str().unwrap_or("???")
)));
let table = app
.config
.style(config.general.table.style)
.highlight_style(config.general.focused_ui.style)
.column_spacing(config.general.table.col_spacing)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!(" {} ", app.pwd())),
);
let table = config
.general
.table
.header
@ -99,29 +244,17 @@ pub fn draw<B: Backend>(
})
.unwrap_or_else(|| table.clone());
table_state.select(
app.directory_buffer
.focus
.map(app::DirectoryBuffer::relative_focus),
);
let mut table_state = TableState::default();
table_state.select(app.directory_buffer().map(|dir| dir.focus));
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
TUIConstraint::Percentage(40),
TUIConstraint::Percentage(50),
TUIConstraint::Min(1),
]
.as_ref(),
)
.split(chunks[1]);
f.render_stateful_widget(table, rect, &mut table_state);
}
fn draw_selected<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
let selected: Vec<ListItem> = app
.selected_paths
.selected()
.iter()
.map(|p| p.to_str().unwrap_or(app::UNSUPPORTED_STR))
.map(String::from)
.map(|n| n.absolute_path.clone())
.map(ListItem::new)
.collect();
@ -134,28 +267,122 @@ pub fn draw<B: Backend>(
.title(format!(" Selected ({}) ", selected_count)),
);
let mut list_state = ListState::default();
if selected_count > 0 {
list_state.select(Some(selected_count.max(1) - 1));
}
f.render_stateful_widget(selected_list, rect, &mut list_state);
}
fn draw_help_menu<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
// Help menu
let help_menu_rows: Vec<Row> = app
.parsed_help_menu
let mode = app.mode();
let extra_help_lines = mode
.extra_help
.clone()
.iter()
.map(|(h, k)| Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())]))
.collect();
.map(|e| e.lines().map(|l| l.to_string()).collect::<Vec<String>>());
let help_menu_rows: Vec<Row> = mode
.help
.clone()
.map(|h| {
h.lines()
.map(|l| Row::new(vec![Cell::from(l.to_string())]))
.collect()
})
.unwrap_or_else(|| {
extra_help_lines
.unwrap_or_default()
.into_iter()
.map(|l| Row::new(vec![Cell::from(l)]))
.chain(mode.key_bindings.on_key.iter().filter_map(|(k, a)| {
a.help.clone().map(|h| {
Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())])
})
}))
.chain(
mode.key_bindings
.on_alphabet
.iter()
.map(|a| ("a-Z", a.help.clone()))
.filter_map(|(k, mh)| {
mh.map(|h| {
Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())])
})
}),
)
.chain(
mode.key_bindings
.on_number
.iter()
.map(|a| ("0-9", a.help.clone()))
.filter_map(|(k, mh)| {
mh.map(|h| {
Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())])
})
}),
)
.chain(
mode.key_bindings
.on_special_character
.iter()
.map(|a| ("spcl chars", a.help.clone()))
.filter_map(|(k, mh)| {
mh.map(|h| {
Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())])
})
}),
)
.chain(
mode.key_bindings
.default
.iter()
.map(|a| ("default", a.help.clone()))
.filter_map(|(k, mh)| {
mh.map(|h| {
Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())])
})
}),
)
.collect::<Vec<Row>>()
});
let help_menu = Table::new(help_menu_rows)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!(" Help [{}] ", &app.mode)),
.title(format!(" Help [{}] ", &mode.name)),
)
.widths(&[TUIConstraint::Percentage(40), TUIConstraint::Percentage(60)]);
f.render_widget(help_menu, rect);
}
// Input box
let input_box = Paragraph::new(format!("> {}", &app.number_input))
fn draw_input_buffer<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
let input_buf = Paragraph::new(format!("> {}", app.input_buffer().unwrap_or(&"".into())))
.block(Block::default().borders(Borders::ALL).title(" input "));
f.render_widget(input_buf, rect);
}
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &app::App, hb: &Handlebars) {
let rect = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([TUIConstraint::Max(rect.height - 5), TUIConstraint::Max(3)].as_ref())
.split(rect);
let upper_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([TUIConstraint::Percentage(70), TUIConstraint::Percentage(30)].as_ref())
.split(chunks[0]);
let upper_left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([TUIConstraint::Percentage(50), TUIConstraint::Percentage(50)].as_ref())
.split(upper_chunks[1]);
f.render_stateful_widget(table, chunks[0], table_state);
f.render_stateful_widget(selected_list, left_chunks[0], list_state);
f.render_widget(help_menu, left_chunks[1]);
f.render_widget(input_box, left_chunks[2]);
draw_input_buffer(f, chunks[1], app, hb);
draw_table(f, upper_chunks[0], app, hb);
draw_selected(f, upper_left_chunks[0], app, hb);
draw_help_menu(f, upper_left_chunks[1], app, hb);
}

Loading…
Cancel
Save