Add feature dynamic UI

Now, users can change the UI layout via the `SwitchLayout{Builtin|Custom}`
message, or by using key `ctrl-w`.

There are 3 default layout options -

- default
- no_help
- no_selection
- no_help_no_selection

Also, the initial mode and the initial layout can be specified in the
config.

Closes: https://github.com/sayanarijit/xplr/issues/107
pull/126/head
Arijit Basu 3 years ago committed by Arijit Basu
parent 474c17b493
commit b3e6679b50

2
Cargo.lock generated

@ -1630,7 +1630,7 @@ dependencies = [
[[package]]
name = "xplr"
version = "0.5.13"
version = "0.6.0"
dependencies = [
"anyhow",
"chrono",

@ -1,6 +1,6 @@
[package]
name = "xplr"
version = "0.5.13" # Update config.yml, config.rs and default.nix
version = "0.6.0" # Update config.yml, config.rs and default.nix
authors = ["Arijit Basu <sayanarijit@gmail.com>"]
edition = "2018"
description = "A hackable, minimal, fast TUI file explorer"

@ -1,4 +1,5 @@
use crate::config::Config;
use crate::config::Layout;
use crate::config::Mode;
use crate::input::Key;
use anyhow::{bail, Result};
@ -1078,6 +1079,27 @@ pub enum ExternalMsg {
/// **Example:** `SwitchModeCustom: my_custom_mode`
SwitchModeCustom(String),
/// Switch layout.
/// This will call `Refresh` automatically.
///
/// > **NOTE:** To be specific about which layout to switch to, use `SwitchLayoutBuiltin` or
/// `SwitchLayoutCustom` instead.
///
/// **Example:** `SwitchLayout: default`
SwitchLayout(String),
/// Switch to a builtin layout.
/// This will call `Refresh` automatically.
///
/// **Example:** `SwitchLayoutBuiltin: default`
SwitchLayoutBuiltin(String),
/// Switch to a custom layout.
/// This will call `Refresh` automatically.
///
/// **Example:** `SwitchLayoutCustom: my_custom_layout`
SwitchLayoutCustom(String),
/// Call a shell command with the given arguments.
/// Note that the arguments will be shell-escaped.
/// So to read the variables, the `-c` option of the shell
@ -1396,6 +1418,7 @@ pub struct App {
selection: IndexSet<Node>,
msg_out: VecDeque<MsgOut>,
mode: Mode,
layout: Layout,
input_buffer: Option<String>,
pid: u32,
session_path: String,
@ -1436,7 +1459,13 @@ impl App {
)
};
let mode = match config.modes().builtin().get(&"default".to_string()) {
let mode = match config.modes().get(
&config
.general()
.initial_mode()
.to_owned()
.unwrap_or_else(|| "default".into()),
) {
Some(m) => m
.clone()
.sanitized(config.general().read_only().unwrap_or_default()),
@ -1445,6 +1474,19 @@ impl App {
}
};
let layout = match config.layouts().get(
&config
.general()
.initial_layout()
.to_owned()
.unwrap_or_else(|| "default".into()),
) {
Some(l) => l.clone(),
None => {
bail!("'default' layout is missing")
}
};
let pid = std::process::id();
let session_path = dirs::runtime_dir()
.unwrap_or_else(|| "/tmp".into())
@ -1476,6 +1518,7 @@ impl App {
selection: Default::default(),
msg_out: Default::default(),
mode,
layout,
input_buffer: Default::default(),
pid,
session_path: session_path.clone(),
@ -1577,6 +1620,9 @@ impl App {
ExternalMsg::SwitchMode(mode) => self.switch_mode(&mode),
ExternalMsg::SwitchModeBuiltin(mode) => self.switch_mode_builtin(&mode),
ExternalMsg::SwitchModeCustom(mode) => self.switch_mode_custom(&mode),
ExternalMsg::SwitchLayout(mode) => self.switch_layout(&mode),
ExternalMsg::SwitchLayoutBuiltin(mode) => self.switch_layout_builtin(&mode),
ExternalMsg::SwitchLayoutCustom(mode) => self.switch_layout_custom(&mode),
ExternalMsg::Call(cmd) => self.call(cmd),
ExternalMsg::CallSilently(cmd) => self.call_silently(cmd),
ExternalMsg::BashExec(cmd) => self.bash_exec(cmd),
@ -1949,6 +1995,36 @@ impl App {
}
}
fn switch_layout(mut self, layout: &str) -> Result<Self> {
if let Some(l) = self.config().layouts().get(layout) {
self.layout = l.to_owned();
self.msg_out.push_back(MsgOut::Refresh);
Ok(self)
} else {
self.log_error(format!("Layout not found: {}", layout))
}
}
fn switch_layout_builtin(mut self, layout: &str) -> Result<Self> {
if let Some(l) = self.config().layouts().get_builtin(layout) {
self.layout = l.to_owned();
self.msg_out.push_back(MsgOut::Refresh);
Ok(self)
} else {
self.log_error(format!("Builtin layout not found: {}", layout))
}
}
fn switch_layout_custom(mut self, layout: &str) -> Result<Self> {
if let Some(l) = self.config().layouts().get_custom(layout) {
self.layout = l.to_owned();
self.msg_out.push_back(MsgOut::Refresh);
Ok(self)
} else {
self.log_error(format!("Custom layout not found: {}", layout))
}
}
fn call(mut self, command: Command) -> Result<Self> {
self.msg_out.push_back(MsgOut::Call(command));
Ok(self)
@ -2479,4 +2555,9 @@ impl App {
// };
Ok(self)
}
/// Get a reference to the app's layout.
pub fn layout(&self) -> &Layout {
&self.layout
}
}

@ -249,7 +249,7 @@ impl TableRowConfig {
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
#[serde(deny_unknown_fields)]
pub enum Constraint {
Percentage(u16),
Ratio(u32, u32),
@ -488,6 +488,12 @@ pub struct GeneralConfig {
#[serde(default)]
initial_sorting: Option<IndexSet<NodeSorterApplicable>>,
#[serde(default)]
initial_mode: Option<String>,
#[serde(default)]
initial_layout: Option<String>,
}
impl GeneralConfig {
@ -503,6 +509,8 @@ impl GeneralConfig {
self.selection_ui = self.selection_ui.extend(other.selection_ui);
self.sort_and_filter_ui = self.sort_and_filter_ui.extend(other.sort_and_filter_ui);
self.initial_sorting = other.initial_sorting.or(self.initial_sorting);
self.initial_layout = other.initial_layout.or(self.initial_layout);
self.initial_mode = other.initial_mode.or(self.initial_mode);
self
}
@ -560,6 +568,16 @@ impl GeneralConfig {
pub fn initial_sorting(&self) -> &Option<IndexSet<NodeSorterApplicable>> {
&self.initial_sorting
}
/// Get a reference to the general config's initial mode.
pub fn initial_mode(&self) -> &Option<String> {
&self.initial_mode
}
/// Get a reference to the general config's initial layout.
pub fn initial_layout(&self) -> &Option<String> {
&self.initial_layout
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -605,10 +623,8 @@ impl KeyBindings {
.into_iter()
.filter(|(_, v)| self.on_key.contains_key(v))
.collect();
self
} else {
self
}
};
self
}
fn extend(mut self, other: Self) -> Self {
@ -808,6 +824,9 @@ pub struct BuiltinModesConfig {
#[serde(default)]
sort: Mode,
#[serde(default)]
switch_layout: Mode,
}
impl BuiltinModesConfig {
@ -831,6 +850,7 @@ impl BuiltinModesConfig {
.relative_path_does_not_contain
.extend(other.relative_path_does_not_contain);
self.sort = self.sort.extend(other.sort);
self.switch_layout = self.switch_layout.extend(other.switch_layout);
self
}
@ -857,6 +877,8 @@ impl BuiltinModesConfig {
"relative path does contain" => Some(&self.relative_path_does_contain),
"relative_path_does_not_contain" => Some(&self.relative_path_does_not_contain),
"relative path does not contain" => Some(&self.relative_path_does_not_contain),
"switch layout" => Some(&self.switch_layout),
"switch_layout" => Some(&self.switch_layout),
_ => None,
}
}
@ -977,11 +999,200 @@ impl ModesConfig {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LayoutOptions {
#[serde(default)]
margin: Option<u16>,
#[serde(default)]
horizontal_margin: Option<u16>,
#[serde(default)]
vertical_margin: Option<u16>,
#[serde(default)]
constraints: Vec<Constraint>,
}
impl LayoutOptions {
pub fn extend(mut self, other: Self) -> Self {
self.margin = other.margin.or(self.margin);
self.horizontal_margin = other.horizontal_margin.or(self.horizontal_margin);
self.vertical_margin = other.vertical_margin.or(self.vertical_margin);
self.constraints = other.constraints;
self
}
/// Get a reference to the layout options's constraints.
pub fn constraints(&self) -> &Vec<Constraint> {
&self.constraints
}
/// Get a reference to the layout options's margin.
pub fn margin(&self) -> Option<u16> {
self.margin
}
/// Get a reference to the layout options's horizontal margin.
pub fn horizontal_margin(&self) -> Option<u16> {
self.horizontal_margin
}
/// Get a reference to the layout options's vertical margin.
pub fn vertical_margin(&self) -> Option<u16> {
self.vertical_margin
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Layout {
Table,
InputAndLogs,
Selection,
HelpMenu,
SortAndFilter,
Horizontal {
config: LayoutOptions,
splits: Vec<Layout>,
},
Vertical {
config: LayoutOptions,
splits: Vec<Layout>,
},
}
impl Default for Layout {
fn default() -> Self {
Self::Table
}
}
impl Layout {
pub fn extend(self, other: Self) -> Self {
match (self, other) {
(
Self::Horizontal {
config: sc,
splits: _,
},
Self::Horizontal {
config: oc,
splits: os,
},
) => Self::Horizontal {
config: sc.extend(oc),
splits: os,
},
(
Self::Vertical {
config: sc,
splits: _,
},
Self::Vertical {
config: oc,
splits: os,
},
) => Self::Vertical {
config: sc.extend(oc),
splits: os,
},
(_, other) => other,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BuiltinLayoutsConfig {
#[serde(default)]
default: Layout,
#[serde(default)]
no_help: Layout,
#[serde(default)]
no_selection: Layout,
#[serde(default)]
no_help_no_selection: Layout,
}
impl BuiltinLayoutsConfig {
pub fn extend(mut self, other: Self) -> Self {
self.default = self.default.extend(other.default);
self.no_help = self.no_help.extend(other.no_help);
self.no_selection = self.no_selection.extend(other.no_selection);
self.no_help_no_selection = self.no_help_no_selection.extend(other.no_help_no_selection);
self
}
pub fn get(&self, name: &str) -> Option<&Layout> {
match name {
"default" => Some(&self.default),
"no_help" => Some(&self.no_help),
"no help" => Some(&self.no_help),
"no_selection" => Some(&self.no_selection),
"no selection" => Some(&self.no_selection),
"no_help_no_selection" => Some(&self.no_help_no_selection),
"no help no selection" => Some(&self.no_help_no_selection),
_ => None,
}
}
/// Get a reference to the builtin layouts config's default.
pub fn default(&self) -> &Layout {
&self.default
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LayoutsConfig {
#[serde(default)]
builtin: BuiltinLayoutsConfig,
#[serde(default)]
custom: HashMap<String, Layout>,
}
impl LayoutsConfig {
pub fn get_builtin(&self, name: &str) -> Option<&Layout> {
self.builtin.get(name)
}
pub fn get_custom(&self, name: &str) -> Option<&Layout> {
self.custom.get(name)
}
pub fn get(&self, name: &str) -> Option<&Layout> {
self.get_builtin(name).or_else(|| self.get_custom(name))
}
pub fn extend(mut self, other: Self) -> Self {
self.builtin = self.builtin.extend(other.builtin);
self.custom.extend(other.custom);
self
}
/// Get a reference to the layouts config's builtin.
pub fn builtin(&self) -> &BuiltinLayoutsConfig {
&self.builtin
}
/// Get a reference to the layouts config's custom.
pub fn custom(&self) -> &HashMap<String, Layout> {
&self.custom
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
version: String,
#[serde(default)]
layouts: LayoutsConfig,
#[serde(default)]
general: GeneralConfig,
@ -996,6 +1207,7 @@ impl Default for Config {
fn default() -> Self {
Self {
version: default_config::version(),
layouts: default_config::layouts(),
general: default_config::general(),
node_types: default_config::node_types(),
modes: default_config::modes(),
@ -1006,6 +1218,7 @@ impl Default for Config {
impl Config {
pub fn extended(mut self) -> Self {
let default = Self::default();
self.layouts = default.layouts.extend(self.layouts);
self.general = default.general.extend(self.general);
self.node_types = default.node_types.extend(self.node_types);
self.modes = default.modes.extend(self.modes);
@ -1028,20 +1241,7 @@ impl Config {
pub fn is_compatible(&self) -> Result<bool> {
let result = match self.parsed_version()? {
(0, 5, 13) => true,
(0, 5, 12) => true,
(0, 5, 11) => true,
(0, 5, 10) => true,
(0, 5, 9) => true,
(0, 5, 8) => true,
(0, 5, 7) => true,
(0, 5, 6) => true,
(0, 5, 5) => true,
(0, 5, 4) => true,
(0, 5, 3) => true,
(0, 5, 2) => true,
(0, 5, 1) => true,
(0, 5, 0) => true,
(0, 6, 0) => true,
(_, _, _) => false,
};
@ -1050,20 +1250,8 @@ impl Config {
pub fn upgrade_notification(&self) -> Result<Option<&str>> {
let result = match self.parsed_version()? {
(0, 5, 13) => None,
(0, 5, 12) => Some("App version updated. Added new messages for switching mode."),
(0, 5, 11) => Some("App version updated. Fixed changing directory on focus using argument"),
(0, 5, 10) => Some("App version updated. Added xplr desktop icon and search navigation"),
(0, 5, 9) => Some("App version updated. Fixed pipes not updating properly"),
(0, 5, 8) => Some("App version updated. Fixed support for filenames starting with - (hiphen)"),
(0, 5, 7) => Some("App version updated. Fixed distorted screen when opening files in GUI"),
(0, 5, 6) => Some("App version updated. Fixed piping and in-built terminal support"),
(0, 5, 5) => Some("App version updated. Significant reduction in CPU usage"),
(0, 5, 4) => Some("App version updated. Significant reduction in CPU usage"),
(0, 5, 3) => Some("App version updated. Fixed exit on permission denied"),
(0, 5, 2) => Some("App version updated. Now pwd is synced with your terminal session"),
(0, 5, 1) => Some("App version updated. Now follow symlinks using 'gf'"),
(_, _, _) => Some("App version updated. New: added sort and filter support and some hacks: https://github.com/sayanarijit/xplr/wiki/Hacks"),
(_, _, _) => None,
// (_, _, _) => Some("App version updated. New: added sort and filter support and some hacks: https://github.com/sayanarijit/xplr/wiki/Hacks"),
};
Ok(result)
@ -1074,6 +1262,11 @@ impl Config {
&self.version
}
/// Get a reference to the config's layouts.
pub fn layouts(&self) -> &LayoutsConfig {
&self.layouts
}
/// Get a reference to the config's general.
pub fn general(&self) -> &GeneralConfig {
&self.general

@ -1,7 +1,89 @@
version: v0.5.13
version: v0.6.0
layouts:
builtin:
default:
Horizontal:
config:
horizontal_margin: 0
vertical_margin: 0
constraints:
- Percentage: 70
- Percentage: 30
splits:
- Vertical:
config:
margin: 0
constraints:
- Length: 3
- Min: 1
- Length: 3
splits:
- SortAndFilter
- Table
- InputAndLogs
- Vertical:
config:
constraints:
- Percentage: 50
- Percentage: 50
splits:
- Selection
- HelpMenu
no_help:
Horizontal:
config:
constraints:
- Percentage: 70
- Percentage: 30
splits:
- Vertical:
config:
constraints:
- Length: 3
- Min: 1
- Length: 3
splits:
- SortAndFilter
- Table
- InputAndLogs
- Selection
no_selection:
Horizontal:
config:
constraints:
- Percentage: 70
- Percentage: 30
splits:
- Vertical:
config:
constraints:
- Length: 3
- Min: 1
- Length: 3
splits:
- SortAndFilter
- Table
- InputAndLogs
- HelpMenu
no_help_no_selection:
Vertical:
config:
constraints:
- Length: 3
- Min: 1
- Length: 3
splits:
- SortAndFilter
- Table
- InputAndLogs
general:
show_hidden: false
read_only: false
initial_layout: default
initial_mode: default
initial_sorting:
- sorter: ByCanonicalIsDir
reverse: true
@ -89,10 +171,10 @@ general:
- format: ╰─
col_spacing: 1
col_widths:
- percentage: 10
- percentage: 50
- percentage: 20
- percentage: 20
- Percentage: 10
- Percentage: 50
- Percentage: 20
- Percentage: 20
default_ui:
prefix: ' '
suffix: ''
@ -823,6 +905,10 @@ modes:
- SwitchModeBuiltin: search
- SetInputBuffer: ''
- Explore
ctrl-w:
help: switch layout
messages:
- SwitchModeBuiltin: switch_layout
d:
help: delete
messages:
@ -1095,7 +1181,7 @@ modes:
s:
help: selection operations
messages:
- SwitchModeBuiltin: selection ops
- SwitchModeBuiltin: selection_ops
l:
help: logs
@ -1192,4 +1278,38 @@ modes:
- AddNodeFilterFromInput: IRelativePathDoesContain
- Explore
switch_layout:
name: switch layout
help: null
extra_help: null
key_bindings:
on_key:
1:
help: default
messages:
- SwitchLayoutBuiltin: default
- SwitchMode: default
2:
help: no help menu
messages:
- SwitchLayoutBuiltin: no_help
- SwitchMode: default
3:
help: no selection panel
messages:
- SwitchLayoutBuiltin: no_selection
- SwitchMode: default
4:
help: no help or selection
messages:
- SwitchLayoutBuiltin: no_help_no_selection
- SwitchMode: default
ctrl-c:
help: terminate
messages:
- Terminate
default:
messages:
- SwitchMode: default
custom: {}

@ -11,6 +11,10 @@ pub fn version() -> String {
DEFAULT_CONFIG.version().clone()
}
pub fn layouts() -> config::LayoutsConfig {
DEFAULT_CONFIG.layouts().clone()
}
pub fn general() -> config::GeneralConfig {
DEFAULT_CONFIG.general().clone()
}

@ -384,7 +384,7 @@ impl Key {
matches!(self, Self::Special(_))
}
pub fn to_char(&self) -> Option<char> {
pub fn to_char(self) -> Option<char> {
match self {
Self::Num0 => Some('0'),
Self::Num1 => Some('1'),

@ -1,6 +1,7 @@
use crate::app;
use crate::app::HelpMenuLine;
use crate::app::{Node, ResolvedNode};
use crate::config::Layout;
use handlebars::Handlebars;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
@ -9,7 +10,7 @@ use std::collections::HashMap;
use std::env;
use tui::backend::Backend;
use tui::layout::Rect;
use tui::layout::{Constraint as TuiConstraint, Direction, Layout};
use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout};
use tui::style::{Color, Modifier, Style as TuiStyle};
use tui::text::{Span, Spans};
use tui::widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table};
@ -412,7 +413,7 @@ fn draw_input_buffer<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _
f.render_widget(input_buf, rect);
}
fn draw_sort_n_filter_by<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
fn draw_sort_n_filter<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
let ui = app.config().general().sort_and_filter_ui().clone();
let filter_by = app.explorer_config().filters();
let sort_by = app.explorer_config().sorters();
@ -525,42 +526,72 @@ fn draw_logs<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handl
f.render_widget(logs_list, 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::Horizontal)
.constraints([TuiConstraint::Percentage(70), TuiConstraint::Percentage(30)].as_ref())
.split(rect);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
TuiConstraint::Length(3),
TuiConstraint::Length(rect.height - 6),
TuiConstraint::Length(3),
]
.as_ref(),
)
.split(chunks[0]);
draw_sort_n_filter_by(f, left_chunks[0], app, hb);
draw_table(f, left_chunks[1], app, hb);
pub fn draw_layout<B: Backend>(
layout: Layout,
f: &mut Frame<B>,
rect: Rect,
app: &app::App,
hb: &Handlebars,
) {
match layout {
Layout::Table => draw_table(f, rect, app, hb),
Layout::SortAndFilter => draw_sort_n_filter(f, rect, app, hb),
Layout::HelpMenu => draw_help_menu(f, rect, app, hb),
Layout::Selection => draw_selection(f, rect, app, hb),
Layout::InputAndLogs => {
if app.input_buffer().is_some() {
draw_input_buffer(f, rect, app, hb);
} else {
draw_logs(f, rect, app, hb);
};
}
Layout::Horizontal { config, splits } => {
let chunks = TuiLayout::default()
.direction(Direction::Horizontal)
.constraints(
config
.constraints()
.iter()
.map(|c| (*c).into())
.collect::<Vec<TuiConstraint>>(),
)
.margin(config.margin().unwrap_or_default())
.horizontal_margin(config.horizontal_margin().unwrap_or_default())
.vertical_margin(config.vertical_margin().unwrap_or_default())
.split(rect);
splits
.into_iter()
.enumerate()
.for_each(|(i, s)| draw_layout(s, f, chunks[i], app, hb));
}
if app.input_buffer().is_some() {
draw_input_buffer(f, left_chunks[2], app, hb);
} else {
draw_logs(f, left_chunks[2], app, hb);
};
Layout::Vertical { config, splits } => {
let chunks = TuiLayout::default()
.direction(Direction::Vertical)
.constraints(
config
.constraints()
.iter()
.map(|c| (*c).into())
.collect::<Vec<TuiConstraint>>(),
)
.margin(config.margin().unwrap_or_default())
.horizontal_margin(config.horizontal_margin().unwrap_or_default())
.vertical_margin(config.vertical_margin().unwrap_or_default())
.split(rect);
splits
.into_iter()
.enumerate()
.for_each(|(i, s)| draw_layout(s, f, chunks[i], app, hb));
}
}
}
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([TuiConstraint::Percentage(50), TuiConstraint::Percentage(50)].as_ref())
.split(chunks[1]);
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &app::App, hb: &Handlebars) {
let rect = f.size();
let layout = app.layout().clone();
draw_selection(f, right_chunks[0], app, hb);
draw_help_menu(f, right_chunks[1], app, hb);
draw_layout(layout, f, rect, app, hb);
}
#[cfg(test)]

Loading…
Cancel
Save