Support defining custom layout for different modes

Closes: https://github.com/sayanarijit/xplr/issues/335
pull/341/head
Arijit Basu 3 years ago committed by Arijit Basu
parent 85cc956792
commit 5eab3c6033

@ -1,5 +1,28 @@
Layouts
=======
# Layouts
#### Example: Defining Custom Layout
[![layout.png][23]][24]
```lua
xplr.config.layouts.builtin.default = {
Horizontal = {
config = {
margin = 1,
horizontal_margin = 2,
vertical_margin = 3,
constraints = {
{ Percentage = 50 },
{ Percentage = 50 },
}
},
splits = {
"Table",
"HelpMenu",
}
}
}
```
xplr layouts define the structure of the UI, i.e. how many panel we see,
placement and size of the panels, how they look etc.
@ -12,9 +35,7 @@ the following fields:
The users can switch between these layouts at run-time.
builtin
-------
## builtin
Type: mapping of string and [Layout][3]
@ -51,9 +72,7 @@ Type: [Layout][3]
This layout hides both the help menu and the selection panel.
custom
------
## custom
Type: mapping of string and [Layout][3]
@ -70,8 +89,7 @@ xplr.config.general.initial_layout = "example"
-- when you load xplr, you should see a blank screen
```
Layout
------
## Layout
A layout can be one of the following:
@ -81,8 +99,9 @@ A layout can be one of the following:
- ["Selection"][11]
- ["HelpMenu"][12]
- ["SortAndFilter"][13]
- { [Horizontal][14] = { config = [Layout Config][15], splits = { [Layout][3], ... } }
- { [Vertical][16] = { config = [Layout Config][15], splits = { [Layout][3], ... } }
- { [CustomContent][25] = { [title][33], [body][34] }
- { [Horizontal][14] = { [config][15], [splits][17] }
- { [Vertical][16] = { [config][15], [splits][17] }
### Nothing
@ -129,9 +148,7 @@ It contains the following information:
- [config][15]
- [splits][17]
Layout Config
-------------
## Layout Config
A layout config contains the following information:
@ -164,9 +181,7 @@ Type: nullable list of [Constraint][22]
The constraints applied on the layout.
Constraint
----------
## Constraint
A constraint can be one of the following:
@ -190,62 +205,257 @@ A constraint can be one of the following:
TODO: document each constraint.
splits
------
## splits
Type: list of [Layout][3]
The list of child layouts to fit into the parent layout.
## Custom Content
Example
-------
Custom content is a special layout to render something custom.
It contains the following information:
[![layout.png][23]][24]
- [title][33]
- [body][34]
### title
Type: nullable string
The title of the panel.
### body
Type: [Content Body][26]
The body of the panel.
## Content Body
Content body can be one of the following:
- [StaticParagraph][27]
- [DynamicParagraph][28]
- [StaticList][29]
- [DynamicList][30]
- [StaticTable][31]
- [DynamicTable][32]
### Static Paragraph
A paragraph to render. It contains the following fields:
- **render** (string): The string to render.
#### Example: Render a custom static paragraph
```lua
xplr.config.layouts.builtin.default = {
Horizontal = {
config = {
margin = 1,
horizontal_margin = 2,
vertical_margin = 3,
constraints = {
{ Percentage = 50 },
{ Percentage = 50 },
}
CustomContent = {
title = "custom title",
body = {
StaticParagraph = { render = "custom body" },
},
splits = {
"Table",
"HelpMenu",
}
}
},
}
```
### Dynamic Paragraph
A [Lua function][35] to render a custom paragraph.
It contains the following fields:
- **render** (string): The [lua function][35] that returns the paragraph to
render.
#### Example: Render a custom dynamic paragraph
```lua
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = { DynamicParagraph = { render = "fn.custom.render_layout" } },
},
}
xplr.fn.custom.render_layout = function(ctx)
return ctx.app.pwd
end
```
### Static List
A list to render. It contains the following fields:
- **render** (list of string): The list to render.
#### Example: Render a custom static list
```lua
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = {
StaticList = { render = { "1", "2", "3" } },
},
},
}
```
### Dynamic List
A [Lua function][35] to render a custom list.
It contains the following fields:
- **render** (string): The [lua function][35] that returns the list to render.
#### Example: Render a custom dynamic list
```lua
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = { DynamicList = { render = "custom.render_layout" } },
},
}
xplr.fn.custom.render_layout = function(ctx)
return ctx.app.history.paths
end
```
### Static Table
A table to render. It contains the following fields:
- **widths** (list of [Constraint][22]): Width of the columns.
- **col_spacing** (nullable int): Spacing between columns. Defaults to 1.
- **render** (list of list of string): The rows and columns to render.
#### Example: Render a custom static table
```lua
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = {
StaticTable = {
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
col_spacing = 1,
render = {
{ "a", "b" },
{ "c", "d" },
},
},
},
},
}
```
### Dynamic Table
A [Lua function][35] to render a custom table.
It contains the following fields:
- **widths** (list of [Constraint][22]): Width of the columns.
- **col_spacing** (nullable int): Spacing between columns. Defaults to 1.
- **render** (string): The [lua function][35] that returns the table to render.
#### Example: Render a custom dynamic table
```lua
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = {
DynamicTable = {
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
col_spacing = 1,
render = "custom.render_layout",
},
},
},
}
xplr.fn.custom.render_layout = function(ctx)
return {
{ "", "" },
{ "Layout height", tostring(ctx.layout_size.height) },
{ "Layout width", tostring(ctx.layout_size.width) },
{ "", "" },
{ "Screen height", tostring(ctx.screen_size.height) },
{ "Screen width", tostring(ctx.screen_size.width) },
}
end
```
## Content Renderer
It is a Lua function that receives [a special argument][36] as input and
returns some output that can be rendered in the UI. It is used to render
content body for the custom dynamic layouts.
## Content Renderer Argument
It contains the following information:
- [layout_size][37]
- [screen_size][37]
- [app][38]
## Size
It contains the following information:
[1]:#builtin
[2]:#custom
[3]:#layout
[4]:#default
[5]:#no_help
[6]:#no_selection
[7]:#no_help_no_selection
[8]:#nothing
[9]:#table
[10]:#inputandlogs
[11]:#selection
[12]:#helpmenu
[13]:#sortandfilter
[14]:#horizontal
[15]:#layout-config
[16]:#vertical
[17]:#splits
[18]:#margin
[19]:#horizontal_margin
[20]:#vertical_margin
[21]:#constraints
[22]:#constraint
[23]:https://s6.gifyu.com/images/layout.png
[24]:https://gifyu.com/image/1X38
- x
- y
- height
- width
Every field is of integer type.
[1]: #builtin
[2]: #custom
[3]: #layout
[4]: #default
[5]: #no_help
[6]: #no_selection
[7]: #no_help_no_selection
[8]: #nothing
[9]: #table
[10]: #inputandlogs
[11]: #selection
[12]: #helpmenu
[13]: #sortandfilter
[14]: #horizontal
[15]: #layout-config
[16]: #vertical
[17]: #splits
[18]: #margin
[19]: #horizontal_margin
[20]: #vertical_margin
[21]: #constraints
[22]: #constraint
[23]: https://s6.gifyu.com/images/layout.png
[24]: https://gifyu.com/image/1X38
[25]: #custom-content
[26]: #content-body
[27]: #static-paragraph
[28]: #dynamic-paragraph
[29]: #static-list
[30]: #dynamic-list
[31]: #static-table
[32]: #dynamic-table
[33]: #title
[34]: #body
[35]: #content-renderer
[36]: #content-renderer-argument
[37]: #size
[38]: message.md#calllua-argument

@ -88,6 +88,7 @@ A mode contains the following information:
- [help][6]
- [extra_help][7]
- [key_bindings][8]
- [layout][29]
### name
@ -115,6 +116,12 @@ Type: [Key Bindings][9]
The key bindings available in that mode.
### layout
Type: nullable [Layout][30]
If specified, this layout will be used to render the UI.
Key Bindings
------------
@ -322,3 +329,5 @@ Visit [Awesome Plugins][27] for more [integration][28] options.
[26]:https://gifyu.com/image/tW86
[27]:awesome-plugins.md
[28]:awesome-plugins.md#integration
[29]:#layout
[30]:layout.md#Layout

@ -1830,12 +1830,7 @@ impl App {
.or_else(|| default.map(|a| a.messages().clone()))
.unwrap_or_else(|| {
if self.config().general().enable_recover_mode() {
vec![
ExternalMsg::SwitchModeBuiltin("recover".into()),
ExternalMsg::LogWarning(
"Key map not found. Let's calm down, escape, and try again.".into(),
),
]
vec![ExternalMsg::SwitchModeBuiltin("recover".into())]
} else {
vec![ExternalMsg::LogWarning("Key map not found.".into())]
}

@ -161,7 +161,7 @@ impl UiConfig {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct UiElement {
#[serde(default)]
@ -667,6 +667,9 @@ pub struct Mode {
#[serde(default)]
pub key_bindings: KeyBindings,
#[serde(default)]
pub layout: Option<Layout>,
}
impl Mode {
@ -789,6 +792,11 @@ impl Mode {
pub fn key_bindings(&self) -> &KeyBindings {
&self.key_bindings
}
/// Get a reference to the mode's layout.
pub fn layout(&self) -> &Option<Layout> {
&self.layout
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -1006,7 +1014,7 @@ impl ModesConfig {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct PanelUiConfig {
#[serde(default)]

@ -997,8 +997,20 @@ xplr.config.modes.builtin.default.key_bindings.on_key["l"] =
------ Recover
xplr.config.modes.builtin.recover = {
name = "recover",
help = nil,
extra_help = nil,
layout = {
CustomContent = {
title = " recover ",
body = {
StaticParagraph = {
render = [[
You pressed an invalid key and went into "recover" mode.
Let's calm down, press `escape`, and try again.
]],
},
},
},
},
key_bindings = {
on_key = {
["ctrl-c"] = {
@ -1010,11 +1022,7 @@ xplr.config.modes.builtin.recover = {
messages = { "PopMode" },
},
},
on_alphabet = nil,
on_number = nil,
on_special_character = nil,
default = {
help = nil,
messages = {},
},
},

@ -15,7 +15,7 @@ use std::cmp::Ordering;
use std::collections::HashMap;
use std::env;
use tui::backend::Backend;
use tui::layout::Rect;
use tui::layout::Rect as TuiRect;
use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout};
use tui::style::{Color, Modifier as TuiModifier, Style as TuiStyle};
use tui::text::{Span, Spans, Text};
@ -35,7 +35,7 @@ fn read_only_indicator(app: &app::App) -> &str {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct LayoutOptions {
#[serde(default)]
@ -81,7 +81,37 @@ impl LayoutOptions {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub enum ContentBody {
/// A paragraph to render
StaticParagraph { render: String },
/// A Lua function that returns a paragraph to render
DynamicParagraph { render: String },
/// List to render
StaticList { render: Vec<String> },
/// A Lua function that returns lines to render
DynamicList { render: String },
/// A table to render
StaticTable {
widths: Vec<Constraint>,
col_spacing: Option<u16>,
render: Vec<Vec<String>>,
},
/// A Lua function that returns a table to render
DynamicTable {
widths: Vec<Constraint>,
col_spacing: Option<u16>,
render: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub enum Layout {
Nothing,
@ -90,6 +120,10 @@ pub enum Layout {
Selection,
HelpMenu,
SortAndFilter,
CustomContent {
title: Option<String>,
body: ContentBody,
},
Horizontal {
config: LayoutOptions,
splits: Vec<Layout>,
@ -248,7 +282,7 @@ impl Into<TuiStyle> for Style {
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub enum Constraint {
Percentage(u16),
@ -271,7 +305,7 @@ pub enum Constraint {
}
impl Constraint {
pub fn to_tui(self, screen_size: Rect, layout_size: Rect) -> TuiConstraint {
pub fn to_tui(self, screen_size: TuiRect, layout_size: TuiRect) -> TuiConstraint {
match self {
Self::Percentage(n) => TuiConstraint::Percentage(n),
Self::Ratio(x, y) => TuiConstraint::Ratio(x, y),
@ -428,8 +462,8 @@ fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> {
fn draw_table<B: Backend>(
f: &mut Frame<B>,
screen_size: Rect,
layout_size: Rect,
screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
lua: &Lua,
) {
@ -651,8 +685,8 @@ fn draw_table<B: Backend>(
fn draw_selection<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_: &Lua,
) {
@ -683,8 +717,8 @@ fn draw_selection<B: Backend>(
fn draw_help_menu<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_: &Lua,
) {
@ -724,8 +758,8 @@ fn draw_help_menu<B: Backend>(
fn draw_input_buffer<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_: &Lua,
) {
@ -768,8 +802,8 @@ fn draw_input_buffer<B: Backend>(
fn draw_sort_n_filter<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_: &Lua,
) {
@ -845,8 +879,8 @@ fn draw_sort_n_filter<B: Backend>(
fn draw_logs<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_: &Lua,
) {
@ -926,8 +960,8 @@ fn draw_logs<B: Backend>(
pub fn draw_nothing<B: Backend>(
f: &mut Frame<B>,
_screen_size: Rect,
layout_size: Rect,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
_lua: &Lua,
) {
@ -937,11 +971,191 @@ pub fn draw_nothing<B: Backend>(
f.render_widget(nothing, layout_size);
}
pub fn draw_layout<B: Backend>(
layout: Layout,
pub fn draw_custom_content<B: Backend>(
f: &mut Frame<B>,
screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
title: Option<String>,
body: ContentBody,
lua: &Lua,
) {
let config = app.config().general().panel_ui().default().clone();
match body {
ContentBody::StaticParagraph { render } => {
let content = Paragraph::new(render).block(block(config, title.unwrap_or_default()));
f.render_widget(content, layout_size);
}
ContentBody::DynamicParagraph { render } => {
let ctx = ContentRendererArg {
app: app.to_lua_arg(),
layout_size: layout_size.into(),
screen_size: screen_size.into(),
};
let render = lua
.to_value(&ctx)
.map(|arg| lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{:?}", e)))
.unwrap_or_else(|e| e.to_string());
let content = Paragraph::new(render).block(block(config, title.unwrap_or_default()));
f.render_widget(content, layout_size);
}
ContentBody::StaticList { render } => {
let items = render
.into_iter()
.map(ListItem::new)
.collect::<Vec<ListItem>>();
let content = List::new(items).block(block(config, title.unwrap_or_default()));
f.render_widget(content, layout_size);
}
ContentBody::DynamicList { render } => {
let ctx = ContentRendererArg {
app: app.to_lua_arg(),
layout_size: layout_size.into(),
screen_size: screen_size.into(),
};
let items = lua
.to_value(&ctx)
.map(|arg| {
lua::call(lua, &render, arg).unwrap_or_else(|e| vec![format!("{:?}", e)])
})
.unwrap_or_else(|e| vec![e.to_string()])
.into_iter()
.map(ListItem::new)
.collect::<Vec<ListItem>>();
let content = List::new(items).block(block(config, title.unwrap_or_default()));
f.render_widget(content, layout_size);
}
ContentBody::StaticTable {
widths,
col_spacing,
render,
} => {
let rows = render
.into_iter()
.map(|cols| Row::new(cols.into_iter().map(Cell::from).collect::<Vec<Cell>>()))
.collect::<Vec<Row>>();
let widths = widths
.into_iter()
.map(|w| w.to_tui(screen_size, layout_size))
.collect::<Vec<TuiConstraint>>();
let content = Table::new(rows)
.widths(&widths)
.column_spacing(col_spacing.unwrap_or(1))
.block(block(config, title.unwrap_or_default()));
f.render_widget(content, layout_size);
}
ContentBody::DynamicTable {
widths,
col_spacing,
render,
} => {
let ctx = ContentRendererArg {
app: app.to_lua_arg(),
layout_size: layout_size.into(),
screen_size: screen_size.into(),
};
let rows = lua
.to_value(&ctx)
.map(|arg| {
lua::call(lua, &render, arg).unwrap_or_else(|e| vec![vec![format!("{:?}", e)]])
})
.unwrap_or_else(|e| vec![vec![e.to_string()]])
.into_iter()
.map(|cols| Row::new(cols.into_iter().map(Cell::from).collect::<Vec<Cell>>()))
.collect::<Vec<Row>>();
let widths = widths
.into_iter()
.map(|w| w.to_tui(screen_size, layout_size))
.collect::<Vec<TuiConstraint>>();
let mut content = Table::new(rows)
.widths(&widths)
.block(block(config, title.unwrap_or_default()));
if let Some(col_spacing) = col_spacing {
content = content.column_spacing(col_spacing);
};
f.render_widget(content, layout_size);
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct Rect {
x: u16,
y: u16,
height: u16,
width: u16,
}
impl From<TuiRect> for Rect {
fn from(tui: TuiRect) -> Self {
Self {
x: tui.x,
y: tui.y,
height: tui.height,
width: tui.width,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentRendererArg {
app: app::CallLuaArg,
screen_size: Rect,
layout_size: Rect,
}
pub fn draw_dynamic_content<B: Backend>(
f: &mut Frame<B>,
_screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
config: Option<PanelUiConfig>,
func: String,
lua: &Lua,
) {
let panel_config = app.config().general().panel_ui();
let config = config.unwrap_or_else(|| panel_config.default().to_owned());
let lines: Vec<ListItem> = lua
.to_value(&app.to_lua_arg())
.map(|arg| lua::call(lua, &func, arg).unwrap_or_else(|e| format!("{:?}", e)))
.unwrap_or_else(|e| e.to_string())
.lines()
.into_iter()
.map(|l| {
let line = ansi_to_text(l.bytes()).unwrap_or_else(|e| Text::raw(e.to_string()));
ListItem::new(line)
})
.collect();
let content = List::new(lines).block(block(config, "".into()));
f.render_widget(content, layout_size);
}
pub fn draw_layout<B: Backend>(
layout: Layout,
f: &mut Frame<B>,
screen_size: TuiRect,
layout_size: TuiRect,
app: &app::App,
lua: &Lua,
) {
@ -958,6 +1172,9 @@ pub fn draw_layout<B: Backend>(
draw_logs(f, screen_size, layout_size, app, lua);
};
}
Layout::CustomContent { title, body } => {
draw_custom_content(f, screen_size, layout_size, app, title, body, lua)
}
Layout::Horizontal { config, splits } => {
let chunks = TuiLayout::default()
.direction(Direction::Horizontal)
@ -1026,7 +1243,12 @@ pub fn draw_layout<B: Backend>(
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &app::App, lua: &Lua) {
let screen_size = f.size();
let layout = app.layout().to_owned();
let layout = app
.mode()
.layout()
.as_ref()
.unwrap_or_else(|| app.layout())
.to_owned();
draw_layout(layout, f, screen_size, screen_size, app, lua);
}

Loading…
Cancel
Save