Improve config inheritance for layout UI

With this commit, users will be able to define the common configuration
as super config and inherit from them in each layout.
This commit is contained in:
Arijit Basu 2021-05-08 17:01:40 +05:30 committed by Arijit Basu
parent 0270fecec9
commit 38812e733b
5 changed files with 244 additions and 93 deletions

2
Cargo.lock generated
View File

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

View File

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

View File

@ -196,7 +196,7 @@ pub struct UiElement {
} }
impl UiElement { impl UiElement {
fn extend(mut self, other: Self) -> Self { pub fn extend(mut self, other: Self) -> Self {
self.format = other.format.or(self.format); self.format = other.format.or(self.format);
self.style = self.style.extend(other.style); self.style = self.style.extend(other.style);
self self
@ -439,6 +439,9 @@ pub struct SortAndFilterUi {
#[serde(default)] #[serde(default)]
separator: UiElement, separator: UiElement,
#[serde(default)]
default_identifier: UiElement,
#[serde(default)] #[serde(default)]
sort_direction_identifiers: SortDirectionIdentifiersUi, sort_direction_identifiers: SortDirectionIdentifiersUi,
@ -452,6 +455,7 @@ pub struct SortAndFilterUi {
impl SortAndFilterUi { impl SortAndFilterUi {
pub fn extend(mut self, other: Self) -> Self { pub fn extend(mut self, other: Self) -> Self {
self.separator = self.separator.extend(other.separator); self.separator = self.separator.extend(other.separator);
self.default_identifier = self.default_identifier.extend(other.default_identifier);
self.sort_direction_identifiers = self self.sort_direction_identifiers = self
.sort_direction_identifiers .sort_direction_identifiers
.extend(other.sort_direction_identifiers); .extend(other.sort_direction_identifiers);
@ -479,6 +483,75 @@ impl SortAndFilterUi {
pub fn filter_identifiers(&self) -> &HashMap<NodeFilter, UiElement> { pub fn filter_identifiers(&self) -> &HashMap<NodeFilter, UiElement> {
&self.filter_identifiers &self.filter_identifiers
} }
/// Get a reference to the sort and filter ui's default identifier.
pub fn default_identifier(&self) -> &UiElement {
&self.default_identifier
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PanelUi {
#[serde(default)]
default: PanelUiConfig,
#[serde(default)]
table: PanelUiConfig,
#[serde(default)]
sort_and_filter: PanelUiConfig,
#[serde(default)]
selection: PanelUiConfig,
#[serde(default)]
input_and_logs: PanelUiConfig,
#[serde(default)]
help_menu: PanelUiConfig,
}
impl PanelUi {
fn extend(mut self, other: Self) -> Self {
self.default = self.default.extend(other.default);
self.table = self.table.extend(other.table);
self.sort_and_filter = self.sort_and_filter.extend(other.sort_and_filter);
self.selection = self.selection.extend(other.selection);
self.input_and_logs = self.input_and_logs.extend(other.input_and_logs);
self.help_menu = self.help_menu.extend(other.help_menu);
self
}
/// Get a reference to the panel ui's default.
pub fn default(&self) -> &PanelUiConfig {
&self.default
}
/// Get a reference to the panel ui's table.
pub fn table(&self) -> &PanelUiConfig {
&self.table
}
/// Get a reference to the panel ui's sort and filter.
pub fn sort_and_filter(&self) -> &PanelUiConfig {
&self.sort_and_filter
}
/// Get a reference to the panel ui's selection.
pub fn selection(&self) -> &PanelUiConfig {
&self.selection
}
/// Get a reference to the panel ui's input and log.
pub fn input_and_logs(&self) -> &PanelUiConfig {
&self.input_and_logs
}
/// Get a reference to the panel ui's help menu.
pub fn help_menu(&self) -> &PanelUiConfig {
&self.help_menu
}
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -514,6 +587,9 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
sort_and_filter_ui: SortAndFilterUi, sort_and_filter_ui: SortAndFilterUi,
#[serde(default)]
panel_ui: PanelUi,
#[serde(default)] #[serde(default)]
initial_sorting: Option<IndexSet<NodeSorterApplicable>>, initial_sorting: Option<IndexSet<NodeSorterApplicable>>,
@ -536,6 +612,7 @@ impl GeneralConfig {
self.focus_ui = self.focus_ui.extend(other.focus_ui); self.focus_ui = self.focus_ui.extend(other.focus_ui);
self.selection_ui = self.selection_ui.extend(other.selection_ui); 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.sort_and_filter_ui = self.sort_and_filter_ui.extend(other.sort_and_filter_ui);
self.panel_ui = self.panel_ui.extend(other.panel_ui);
self.initial_sorting = other.initial_sorting.or(self.initial_sorting); self.initial_sorting = other.initial_sorting.or(self.initial_sorting);
self.initial_layout = other.initial_layout.or(self.initial_layout); self.initial_layout = other.initial_layout.or(self.initial_layout);
self.initial_mode = other.initial_mode.or(self.initial_mode); self.initial_mode = other.initial_mode.or(self.initial_mode);
@ -606,6 +683,11 @@ impl GeneralConfig {
pub fn initial_layout(&self) -> &Option<String> { pub fn initial_layout(&self) -> &Option<String> {
&self.initial_layout &self.initial_layout
} }
/// Get a reference to the general config's panel ui.
pub fn panel_ui(&self) -> &PanelUi {
&self.panel_ui
}
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -1072,7 +1154,7 @@ impl LayoutOptions {
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct BlockConfig { pub struct PanelUiConfig {
#[serde(default)] #[serde(default)]
title: UiElement, title: UiElement,
@ -1083,7 +1165,13 @@ pub struct BlockConfig {
style: Style, style: Style,
} }
impl BlockConfig { impl PanelUiConfig {
pub fn extend(mut self, other: Self) -> Self {
self.title = self.title.extend(other.title);
self.borders = other.borders.or(self.borders);
self.style = self.style.extend(other.style);
self
}
/// Get a reference to the block config's borders. /// Get a reference to the block config's borders.
pub fn borders(&self) -> &Option<IndexSet<Border>> { pub fn borders(&self) -> &Option<IndexSet<Border>> {
&self.borders &self.borders
@ -1103,12 +1191,12 @@ impl BlockConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub enum Layout { pub enum Layout {
Nothing(BlockConfig), Nothing(Option<PanelUiConfig>),
Table(BlockConfig), Table(Option<PanelUiConfig>),
InputAndLogs(BlockConfig), InputAndLogs(Option<PanelUiConfig>),
Selection(BlockConfig), Selection(Option<PanelUiConfig>),
HelpMenu(BlockConfig), HelpMenu(Option<PanelUiConfig>),
SortAndFilter(BlockConfig), SortAndFilter(Option<PanelUiConfig>),
Horizontal { Horizontal {
config: LayoutOptions, config: LayoutOptions,
splits: Vec<Layout>, splits: Vec<Layout>,
@ -1129,6 +1217,38 @@ impl Layout {
pub fn extend(self, other: Self) -> Self { pub fn extend(self, other: Self) -> Self {
match (self, other) { match (self, other) {
(s, Self::Nothing(_)) => s, (s, Self::Nothing(_)) => s,
(Self::Table(s), Self::Table(o)) => Self::Table(o.or(s)),
(Self::InputAndLogs(s), Self::InputAndLogs(o)) => Self::InputAndLogs(o.or(s)),
(Self::Selection(s), Self::Selection(o)) => Self::Selection(o.or(s)),
(Self::HelpMenu(s), Self::HelpMenu(o)) => Self::HelpMenu(o.or(s)),
(Self::SortAndFilter(s), Self::SortAndFilter(o)) => Self::SortAndFilter(o.or(s)),
(
Self::Horizontal {
config: sconfig,
splits: _,
},
Self::Horizontal {
config: oconfig,
splits: osplits,
},
) => Self::Horizontal {
config: sconfig.extend(oconfig),
splits: osplits,
},
(
Self::Vertical {
config: sconfig,
splits: _,
},
Self::Vertical {
config: oconfig,
splits: osplits,
},
) => Self::Vertical {
config: sconfig.extend(oconfig),
splits: osplits,
},
(_, other) => other, (_, other) => other,
} }
} }
@ -1274,7 +1394,7 @@ impl Config {
pub fn is_compatible(&self) -> Result<bool> { pub fn is_compatible(&self) -> Result<bool> {
let result = match self.parsed_version()? { let result = match self.parsed_version()? {
(0, 6, 0) => true, (0, 7, 0) => true,
(_, _, _) => false, (_, _, _) => false,
}; };

View File

@ -1,4 +1,4 @@
version: v0.6.0 version: v0.7.0
layouts: layouts:
custom: {} custom: {}
builtin: builtin:
@ -19,22 +19,17 @@ layouts:
- Min: 1 - Min: 1
- Length: 3 - Length: 3
splits: splits:
- SortAndFilter: - SortAndFilter: ~
borders: [Top, Right, Bottom, Left] - Table: ~
- Table: - InputAndLogs: ~
borders: [Top, Right, Bottom, Left]
- InputAndLogs:
borders: [Top, Right, Bottom, Left]
- Vertical: - Vertical:
config: config:
constraints: constraints:
- Percentage: 50 - Percentage: 50
- Percentage: 50 - Percentage: 50
splits: splits:
- Selection: - Selection: ~
borders: [Top, Right, Bottom, Left] - HelpMenu: ~
- HelpMenu:
borders: [Top, Right, Bottom, Left]
no_help: no_help:
Horizontal: Horizontal:
config: config:
@ -49,14 +44,10 @@ layouts:
- Min: 1 - Min: 1
- Length: 3 - Length: 3
splits: splits:
- SortAndFilter: - SortAndFilter: ~
borders: [Top, Right, Bottom, Left] - Table: ~
- Table: - InputAndLogs: ~
borders: [Top, Right, Bottom, Left] - Selection: ~
- InputAndLogs:
borders: [Top, Right, Bottom, Left]
- Selection:
borders: [Top, Right, Bottom, Left]
no_selection: no_selection:
Horizontal: Horizontal:
@ -72,14 +63,10 @@ layouts:
- Min: 1 - Min: 1
- Length: 3 - Length: 3
splits: splits:
- SortAndFilter: - SortAndFilter: ~
borders: [Top, Right, Bottom, Left] - Table: ~
- Table: - InputAndLogs: ~
borders: [Top, Right, Bottom, Left] - HelpMenu: ~
- InputAndLogs:
borders: [Top, Right, Bottom, Left]
- HelpMenu:
borders: [Top, Right, Bottom, Left]
no_help_no_selection: no_help_no_selection:
Vertical: Vertical:
@ -89,12 +76,9 @@ layouts:
- Min: 1 - Min: 1
- Length: 3 - Length: 3
splits: splits:
- SortAndFilter: - SortAndFilter: ~
borders: [Top, Right, Bottom, Left] - Table: ~
- Table: - InputAndLogs: ~
borders: [Top, Right, Bottom, Left]
- InputAndLogs:
borders: [Top, Right, Bottom, Left]
general: general:
show_hidden: false show_hidden: false
@ -161,27 +145,38 @@ general:
default_ui: default_ui:
prefix: ' ' prefix: ' '
suffix: '' suffix: ''
focus_ui: focus_ui:
prefix: ▸[ prefix: ▸[
suffix: ']' suffix: ']'
style: style:
fg: Blue fg: Blue
add_modifiers: [Bold] add_modifiers: [Bold]
selection_ui: selection_ui:
prefix: ' {' prefix: ' {'
suffix: '}' suffix: '}'
style: style:
fg: LightGreen fg: LightGreen
add_modifiers: [Bold] add_modifiers: [Bold]
panel_ui:
default:
borders: [Top, Right, Bottom, Left]
sort_and_filter_ui: sort_and_filter_ui:
separator: separator:
format: " " format: " "
style:
add_modifiers: [Dim]
default_identifier:
style:
add_modifiers: [Bold]
sort_direction_identifiers: sort_direction_identifiers:
forward: forward:
format: "↓" format: "↓"
reverse: reverse:
format: "↑" format: "↑"
sorter_identifiers: sorter_identifiers:
ByRelativePath: ByRelativePath:
format: "rel" format: "rel"

130
src/ui.rs
View File

@ -1,8 +1,8 @@
use crate::app; use crate::app;
use crate::app::HelpMenuLine; use crate::app::HelpMenuLine;
use crate::app::{Node, ResolvedNode}; use crate::app::{Node, ResolvedNode};
use crate::config::BlockConfig;
use crate::config::Layout; use crate::config::Layout;
use crate::config::PanelUiConfig;
use handlebars::Handlebars; use handlebars::Handlebars;
use indexmap::IndexSet; use indexmap::IndexSet;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -233,7 +233,7 @@ impl NodeUiMetadata {
} }
} }
fn block<'a>(config: BlockConfig, default_title: String) -> Block<'a> { fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> {
Block::default() Block::default()
.borders(TuiBorders::from_bits_truncate( .borders(TuiBorders::from_bits_truncate(
config config
@ -253,13 +253,21 @@ fn block<'a>(config: BlockConfig, default_title: String) -> Block<'a> {
} }
fn draw_table<B: Backend>( fn draw_table<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
screen_size: Rect, screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
hb: &Handlebars, hb: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.table().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let app_config = app.config().to_owned(); let app_config = app.config().to_owned();
let header_height = app_config.general().table().header().height().unwrap_or(1); let header_height = app_config.general().table().header().height().unwrap_or(1);
let height: usize = (layout_size.height.max(header_height + 2) - (header_height + 2)).into(); let height: usize = (layout_size.height.max(header_height + 2) - (header_height + 2)).into();
@ -430,13 +438,21 @@ fn draw_table<B: Backend>(
} }
fn draw_selection<B: Backend>( fn draw_selection<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
_: &Handlebars, _: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.selection().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let selection: Vec<ListItem> = app let selection: Vec<ListItem> = app
.selection() .selection()
.iter() .iter()
@ -457,13 +473,21 @@ fn draw_selection<B: Backend>(
} }
fn draw_help_menu<B: Backend>( fn draw_help_menu<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
_: &Handlebars, _: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.help_menu().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let help_menu_rows = app let help_menu_rows = app
.mode() .mode()
.help_menu() .help_menu()
@ -505,13 +529,21 @@ fn draw_help_menu<B: Backend>(
} }
fn draw_input_buffer<B: Backend>( fn draw_input_buffer<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
_: &Handlebars, _: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.input_and_logs().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let input_buf = Paragraph::new(Spans::from(vec![ let input_buf = Paragraph::new(Spans::from(vec![
Span::styled( Span::styled(
app.config() app.config()
@ -538,41 +570,31 @@ fn draw_input_buffer<B: Backend>(
} }
fn draw_sort_n_filter<B: Backend>( fn draw_sort_n_filter<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
_: &Handlebars, _: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.sort_and_filter().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let ui = app.config().general().sort_and_filter_ui().clone(); let ui = app.config().general().sort_and_filter_ui().clone();
let filter_by = app.explorer_config().filters(); let filter_by = app.explorer_config().filters();
let sort_by = app.explorer_config().sorters(); let sort_by = app.explorer_config().sorters();
let forward = Span::styled( let defaultui = ui.default_identifier();
ui.sort_direction_identifiers() let forwardui = defaultui
.forward() .clone()
.format() .extend(ui.sort_direction_identifiers().forward().clone());
.to_owned() let reverseui = defaultui
.unwrap_or_default(), .clone()
ui.sort_direction_identifiers() .extend(ui.sort_direction_identifiers().reverse().clone());
.forward()
.style()
.clone()
.into(),
);
let reverse = Span::styled(
ui.sort_direction_identifiers()
.reverse()
.format()
.to_owned()
.unwrap_or_default(),
ui.sort_direction_identifiers()
.reverse()
.style()
.clone()
.into(),
);
let mut spans = filter_by let mut spans = filter_by
.iter() .iter()
@ -580,35 +602,36 @@ fn draw_sort_n_filter<B: Backend>(
ui.filter_identifiers() ui.filter_identifiers()
.get(&f.filter()) .get(&f.filter())
.map(|u| { .map(|u| {
let ui = defaultui.clone().extend(u.clone());
( (
Span::styled( Span::styled(
u.format().to_owned().unwrap_or_default(), ui.format().to_owned().unwrap_or_default(),
u.style().clone().into(), ui.style().clone().into(),
), ),
Span::raw(f.input().clone()), Span::styled(f.input().clone(), ui.style().clone().into()),
) )
}) })
.unwrap_or_else(|| (Span::raw("f"), Span::raw(""))) .unwrap_or_else(|| (Span::raw("f"), Span::raw("")))
}) })
.chain(sort_by.iter().map(|s| { .chain(sort_by.iter().map(|s| {
let direction = if s.reverse() { let direction = if s.reverse() { &reverseui } else { &forwardui };
reverse.clone()
} else {
forward.clone()
};
ui.sorter_identifiers() ui.sorter_identifiers()
.get(&s.sorter()) .get(&s.sorter())
.map(|u| { .map(|u| {
let ui = defaultui.clone().extend(u.clone());
( (
Span::styled( Span::styled(
u.format().to_owned().unwrap_or_default(), ui.format().to_owned().unwrap_or_default(),
u.style().clone().into(), ui.style().clone().into(),
),
Span::styled(
direction.format().to_owned().unwrap_or_default(),
direction.style().clone().into(),
), ),
direction.clone(),
) )
}) })
.unwrap_or_else(|| (Span::raw("s"), direction.clone())) .unwrap_or_else(|| (Span::raw("s"), Span::raw("")))
})) }))
.zip(std::iter::repeat(Span::styled( .zip(std::iter::repeat(Span::styled(
ui.separator().format().to_owned().unwrap_or_default(), ui.separator().format().to_owned().unwrap_or_default(),
@ -628,13 +651,21 @@ fn draw_sort_n_filter<B: Backend>(
} }
fn draw_logs<B: Backend>( fn draw_logs<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
app: &app::App, app: &app::App,
_: &Handlebars, _: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config
.default()
.clone()
.extend(panel_config.input_and_logs().clone());
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let logs_config = app.config().general().logs().clone(); let logs_config = app.config().general().logs().clone();
let logs = app let logs = app
.logs() .logs()
@ -680,13 +711,18 @@ fn draw_logs<B: Backend>(
} }
pub fn draw_nothing<B: Backend>( pub fn draw_nothing<B: Backend>(
config: BlockConfig, config: Option<PanelUiConfig>,
f: &mut Frame<B>, f: &mut Frame<B>,
_screen_size: Rect, _screen_size: Rect,
layout_size: Rect, layout_size: Rect,
_app: &app::App, app: &app::App,
_hb: &Handlebars, _hb: &Handlebars,
) { ) {
let panel_config = app.config().general().panel_ui();
let default_panel_config = panel_config.default().clone();
let config = config
.map(|c| default_panel_config.clone().extend(c))
.unwrap_or(default_panel_config);
let nothing = Paragraph::new("").block(block(config, "".into())); let nothing = Paragraph::new("").block(block(config, "".into()));
f.render_widget(nothing, layout_size); f.render_widget(nothing, layout_size);
} }