diff --git a/config/joshuto.toml b/config/joshuto.toml
index 9219ca0c9..455210d68 100644
--- a/config/joshuto.toml
+++ b/config/joshuto.toml
@@ -18,7 +18,6 @@ scroll_offset = 6
show_borders = true
show_hidden = false
show_icons = true
-tilde_in_titlebar = true
# none, absolute, relative
line_number_style = "none"
diff --git a/config/theme.toml b/config/theme.toml
index c26502fd9..fce3633d7 100644
--- a/config/theme.toml
+++ b/config/theme.toml
@@ -2,12 +2,30 @@
## Tabs
##########################################
-# Inactive tabs
-[tabs.inactive]
+[tabs]
+[tabs.styles]
-# Active tabs
-[tabs.active]
-invert=true
+# Style of active tab & current directory
+[tabs.styles.active]
+bg = "light_blue"
+fg = "black"
+bold = true
+
+# Style of inactive tabs
+[tabs.styles.inactive]
+
+# Style of the left/front tab scroll tag (when tabs overflow)
+[tabs.styles.scroll_front]
+fg = "yellow"
+bold = true
+
+# Style of the right/back tab scroll tag (when tabs overflow)
+[tabs.styles.scroll_back]
+fg = "yellow"
+bold = true
+
+# There are more style options and strings to configure
+# the tab-bar theme.
##########################################
## File List - Selections
diff --git a/docs/configuration/joshuto.toml.md b/docs/configuration/joshuto.toml.md
index 6a77f53fa..ee88c33de 100644
--- a/docs/configuration/joshuto.toml.md
+++ b/docs/configuration/joshuto.toml.md
@@ -119,14 +119,6 @@ fzf_case_sensitivity = "insensitive"
# ...
[tab]
-
-# Options include
-# - num
-# - dir
-# - all
-# also can be changed with the 'tab_bar_mode' command
-display_mode = "all"
-
# inherit, home, root
home_page = "home"
```
diff --git a/docs/configuration/tabbar/README.md b/docs/configuration/tabbar/README.md
new file mode 100644
index 000000000..9cdd5b4e4
--- /dev/null
+++ b/docs/configuration/tabbar/README.md
@@ -0,0 +1,264 @@
+# Theming the Tab-Bar
+The tab bar in the title row can be configured in various aspects.
+This page explains the tab-related configuration options and gives
+configuration examples at the end.
+
+## Elements of the Tab-Bar
+Of course, the tab-bar is composed from one or more tabs.
+Tabs are separated by a _divider_, and if there are more tabs than fit into the window,
+there will be _scroll tags_ at the beginning and at the end of the tab bar.
+The scroll tags indicate that there are more tabs and each end's scroll tag shows the number
+of tabs that did not fit into the tab-bar on that particular side.
+
+The right scroll tag is always right-aligned.
+The space between the right scroll tag and the right-most tab is filled by a "padding" segment.
+
+Furthermore, each tab, the padding segment, and the scroll tags, all have a _prefix_ and a _postfix_.
+Prefixes and postfixes are additional sub-elements that
+help to better visualize the tabs and
+allow for some more styling options.
+
+
+The following list shows an example sequence with four tabs
+that includes all the possible segments of a tab-bar.
+
+*
+ [pre]
+ [scroll-f]
+ [post] (left scroll tag)
+
+*
+ [pre]
+ [tab 1]
+ [post] (inactive tab)
+
+*
+ [ii] (divider between two inactive tabs)
+
+*
+ [pre]
+ [tab 2]
+ [post] (inactive tab)
+
+*
+ [ia] (divider before active tab)
+
+*
+ [pre]
+ [tab 3]
+ [post] (active tab)
+
+*
+ [ai] (divider after active tab)
+
+*
+ [pre]
+ tab 4
+ [post] (inactive tab)
+
+*
+ [pre]
+ [pad]
+ [post] (padding)
+
+*
+
[pre]
+
[scroll-b]
+
[post] (right scroll tag)
+
+
+
+## Configuring the Characters
+Characters for prefixes, postfixes and some other elements can be configured under a `[tabs.chars]`
+section.
+This is a complete example for the configuration of the default values:
+```toml
+[tabs.chars]
+active_prefix = " "
+active_postfix = " "
+inactive_prefix = "["
+inactive_postfix = "]"
+divider = " "
+scroll_front_prefix = ""
+scroll_front_postfix = ""
+scroll_front_prestring = "«"
+scroll_front_poststring = " "
+scroll_back_prefix = ""
+scroll_back_postfix = ""
+scroll_back_prestring = " "
+scroll_back_poststring = "»"
+padding_prefix = " "
+padding_postfix = " "
+padding_fill = " "
+```
+Be aware that all elements for the padding segment are chars and can't be of zero length
+or `None`, while the other pre- and postfixes are strings and can also be an empty string.
+
+Further note that the scroll tags also have a `prestring` and `poststring`,
+which are strings shown before and after the number in the scroll tag but are still part of the
+tag's "body". While the pre- and postfix can have a separate font style (e.g. color),
+the pre- and post-string are styled just as the number.
+
+## Configuring Styles (Colors and Font Styles)
+Each element of the tab-bar can be configured with a separate style.
+The divider can even be configured with three different styles,
+one for dividers between two inactive tabs (`divider_ii`),
+one for the divider before the active tab (`divider_ia`),
+and one for the divider after the active tab (`divider_ai`).
+
+All styles are sub-elements of `[tabs.styles]`.
+
+To ease configuration, most styles are derived from another style and one only
+has to specify the deviation.
+
+The following image visualizes all styles and how they are derived from others.
+
+```mermaid
+classDiagram
+ direction LR
+
+ active <|-- prefix_active
+ prefix_active <|-- postfix_active
+
+ inactive <|-- prefix_inactive
+ prefix_inactive <|-- postfix_inactive
+
+ divider_ii <|-- divider_ia
+ divider_ia <|-- divider_ai
+
+ scroll_front <|-- scroll_front_prefix
+ scroll_front_prefix <|-- scroll_front_postfix
+
+ scroll_back <|-- scroll_back_prefix
+ scroll_back_prefix <|-- scroll_back_postfix
+
+ padding_fill <|-- padding_prefix
+ padding_prefix <|-- padding_postfix
+```
+The left-most styles in the diagram are basic styles which are not
+derived from another style.
+The default configuration uses these:
+
+```toml
+[tabs.styles]
+
+[tabs.styles.active]
+bg = "light_blue"
+fg = "black"
+bold = true
+
+[tabs.styles.scroll_front]
+fg = "yellow"
+bold = true
+
+[tabs.styles.scroll_back]
+fg = "yellow"
+bold = true
+```
+
+The remaining base styles use the terminal default background and foreground.
+The derived styles are all empty by default and therewith equal to their "root-style".
+
+Hint: If one changes even only one attribute of one of the base styles, the default
+is not used anymore. For example, if only `[tabs.styles.active.bg]` is set in
+`theme.toml`, the two other defaults (`fg` and `bold`) are not used anymore
+from the default configuration and need to be specified explicitly if wanted.
+If they are specified, they will fall back to the terminal default style.
+
+## Examples
+
+### Rounded Tabs with Arrow-like Scroll Tags
+This styling requires a **nerdfont** being used by the terminal.
+
+![Fancy Tab-Bar with Nerdfont](nerdfont_bar_1.png)
+
+This is the configuration that can be copied to `theme.toml`:
+```toml
+[tabs]
+
+[tabs.chars]
+divider = " "
+active_prefix = ""
+active_postfix = ""
+inactive_prefix = ""
+inactive_postfix = ""
+scroll_front_prefix = ""
+scroll_front_postfix = ""
+scroll_front_prestring = ""
+scroll_front_poststring = " "
+scroll_back_prefix = ""
+scroll_back_postfix = ""
+scroll_back_prestring = " "
+scroll_back_poststring = ""
+
+[tabs.styles]
+
+[tabs.styles.active]
+fg = "black"
+bg = "light_blue"
+bold = true
+
+[tabs.styles.active_prefix]
+fg = "light_blue"
+bg = "reset"
+
+[tabs.styles.inactive]
+fg = "black"
+bg = "gray"
+
+[tabs.styles.inactive_prefix]
+fg = "gray"
+bg = "reset"
+
+[tabs.styles.scroll_front]
+fg = "black"
+bg = "yellow"
+bold = true
+
+[tabs.styles.scroll_front_prefix]
+fg = "yellow"
+bg = "reset"
+
+[tabs.styles.scroll_front_postfix]
+invert = true
+
+[tabs.styles.scroll_back]
+fg = "black"
+bg = "yellow"
+bold = true
+
+[tabs.styles.scroll_back_prefix]
+fg = "yellow"
+bg = "reset"
+invert = true
+
+[tabs.styles.scroll_back_postfix]
+invert = false
+```
diff --git a/docs/configuration/tabbar/nerdfont_bar_1.png b/docs/configuration/tabbar/nerdfont_bar_1.png
new file mode 100644
index 000000000..8c7fb77c2
Binary files /dev/null and b/docs/configuration/tabbar/nerdfont_bar_1.png differ
diff --git a/docs/configuration/theme.toml.md b/docs/configuration/theme.toml.md
index e946be6fd..f42803425 100644
--- a/docs/configuration/theme.toml.md
+++ b/docs/configuration/theme.toml.md
@@ -61,3 +61,5 @@ a specific style that overrides the former file-type-styles.
Last but not least, there are styles for _selected_ files which override all the former
styles.
+## Theming the Tab-Bar
+Theming of the tab-bar is described [here](tabbar/README.md).
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index fb45e7966..8b56f5a59 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -40,7 +40,6 @@ pub mod show_tasks;
pub mod sort;
pub mod sub_process;
pub mod subdir_fzf;
-pub mod tab_bar_mode;
pub mod tab_ops;
pub mod touch_file;
pub mod uimodes;
diff --git a/src/commands/tab_bar_mode.rs b/src/commands/tab_bar_mode.rs
deleted file mode 100644
index e292aaca5..000000000
--- a/src/commands/tab_bar_mode.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use crate::config::clean::app::tab::TabBarDisplayMode;
-use crate::context::AppContext;
-use crate::error::AppResult;
-
-pub fn set_tab_bar_display_mode(
- context: &mut AppContext,
- mode: &TabBarDisplayMode,
-) -> AppResult<()> {
- context.tab_context_mut().display.mode = *mode;
- Ok(())
-}
diff --git a/src/config/clean/app/display/config.rs b/src/config/clean/app/display/config.rs
index 4bf64de9d..1f8e51fb6 100644
--- a/src/config/clean/app/display/config.rs
+++ b/src/config/clean/app/display/config.rs
@@ -26,7 +26,6 @@ pub struct DisplayOption {
pub _show_borders: bool,
pub _show_hidden: bool,
pub _show_icons: bool,
- pub _tilde_in_titlebar: bool,
pub _line_nums: LineNumberStyle,
pub column_ratio: (usize, usize, usize),
pub default_layout: [Constraint; 3],
@@ -71,7 +70,6 @@ impl From
for DisplayOption {
_show_borders: raw.show_borders,
_show_hidden: raw.show_hidden,
_show_icons: raw.show_icons,
- _tilde_in_titlebar: raw.tilde_in_titlebar,
_line_nums,
column_ratio,
@@ -120,10 +118,6 @@ impl DisplayOption {
self._show_hidden = show_hidden;
}
- pub fn tilde_in_titlebar(&self) -> bool {
- self._tilde_in_titlebar
- }
-
pub fn line_nums(&self) -> LineNumberStyle {
self._line_nums
}
@@ -164,7 +158,6 @@ impl std::default::Default for DisplayOption {
_show_borders: true,
_show_hidden: false,
_show_icons: false,
- _tilde_in_titlebar: true,
_line_nums: LineNumberStyle::None,
default_layout,
no_preview_layout,
diff --git a/src/config/clean/app/tab/config.rs b/src/config/clean/app/tab/config.rs
index 86e156f9e..d724e9af6 100644
--- a/src/config/clean/app/tab/config.rs
+++ b/src/config/clean/app/tab/config.rs
@@ -1,28 +1,13 @@
-use std::str::FromStr;
-
-use serde::Deserialize;
-
-use crate::{
- config::raw::app::display::tab::TabOptionRaw,
- error::{AppError, AppErrorKind},
- tab::TabHomePage,
-};
+use crate::{config::raw::app::display::tab::TabOptionRaw, tab::TabHomePage};
#[derive(Clone, Debug)]
pub struct TabOption {
pub _home_page: TabHomePage,
- pub display: TabBarDisplayOption,
}
impl TabOption {
- pub fn new(_home_page: TabHomePage, display_mode: TabBarDisplayMode, max_len: usize) -> Self {
- Self {
- _home_page,
- display: TabBarDisplayOption {
- mode: display_mode,
- max_len,
- },
- }
+ pub fn new(_home_page: TabHomePage) -> Self {
+ Self { _home_page }
}
pub fn home_page(&self) -> TabHomePage {
self._home_page
@@ -33,7 +18,6 @@ impl std::default::Default for TabOption {
fn default() -> Self {
Self {
_home_page: TabHomePage::Home,
- display: TabBarDisplayOption::default(),
}
}
}
@@ -42,48 +26,6 @@ impl From for TabOption {
fn from(raw: TabOptionRaw) -> Self {
let home_page = TabHomePage::from_str(raw.home_page.as_str()).unwrap_or(TabHomePage::Home);
- Self::new(home_page, raw.display_mode, raw.max_len)
- }
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct TabBarDisplayOption {
- pub mode: TabBarDisplayMode,
- pub max_len: usize,
-}
-
-impl Default for TabBarDisplayOption {
- fn default() -> Self {
- Self {
- mode: Default::default(),
- max_len: 16,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Deserialize, Default)]
-pub enum TabBarDisplayMode {
- #[serde(rename = "num")]
- Number,
- #[default]
- #[serde(rename = "dir")]
- Directory,
- #[serde(rename = "all")]
- All,
-}
-
-impl FromStr for TabBarDisplayMode {
- type Err = AppError;
-
- fn from_str(s: &str) -> Result {
- match s {
- "num" => Ok(Self::Number),
- "dir" => Ok(Self::Directory),
- "all" => Ok(Self::All),
- s => Err(AppError::new(
- AppErrorKind::UnrecognizedArgument,
- format!("tab_bar_mode: `{}` unknown argument.", s),
- )),
- }
+ Self::new(home_page)
}
}
diff --git a/src/config/clean/theme/tab.rs b/src/config/clean/theme/tab.rs
index 6a4a9018e..f8cf256de 100644
--- a/src/config/clean/theme/tab.rs
+++ b/src/config/clean/theme/tab.rs
@@ -1,17 +1,198 @@
-use crate::config::raw::theme::tab::TabThemeRaw;
-
-use super::style::AppStyle;
+use crate::config::raw::theme::tab::{TabThemeCharsRaw, TabThemeColorRaw, TabThemeRaw};
+use crate::util::style::PathStyleIfSome;
+use ratatui::style::{Color, Modifier, Style};
+use unicode_width::UnicodeWidthStr;
#[derive(Clone, Debug)]
pub struct TabTheme {
- pub inactive: AppStyle,
- pub active: AppStyle,
+ pub styles: TabThemeColors,
+ pub chars: TabThemeChars,
+ pub inference: TabThemeCharsInference,
}
impl From for TabTheme {
fn from(crude: TabThemeRaw) -> Self {
- let inactive = crude.inactive.to_style_theme();
- let active = crude.active.to_style_theme();
- Self { inactive, active }
+ let chars = TabThemeChars::from(crude.chars);
+ Self {
+ styles: TabThemeColors::from(crude.styles),
+ inference: TabThemeCharsInference::from_chars(&chars),
+ chars,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct TabThemeChars {
+ pub divider: String,
+ pub prefix_i: String,
+ pub postfix_i: String,
+ pub prefix_a: String,
+ pub postfix_a: String,
+ pub scroll_front_prefix: String,
+ pub scroll_front_postfix: String,
+ pub scroll_front_prestring: String,
+ pub scroll_front_poststring: String,
+ pub scroll_back_prefix: String,
+ pub scroll_back_postfix: String,
+ pub scroll_back_prestring: String,
+ pub scroll_back_poststring: String,
+ pub padding_prefix: char,
+ pub padding_postfix: char,
+ pub padding_fill: char,
+}
+
+impl From for TabThemeChars {
+ fn from(crude: TabThemeCharsRaw) -> Self {
+ Self {
+ divider: crude.divider.unwrap_or(" ".to_string()),
+ prefix_i: crude.inactive_prefix.unwrap_or("[".to_string()),
+ postfix_i: crude.inactive_postfix.unwrap_or("]".to_string()),
+ prefix_a: crude.active_prefix.unwrap_or(" ".to_string()),
+ postfix_a: crude.active_postfix.unwrap_or(" ".to_string()),
+ scroll_front_prefix: crude.scroll_front_prefix.unwrap_or("".to_string()),
+ scroll_front_postfix: crude.scroll_front_postfix.unwrap_or("".to_string()),
+ scroll_front_prestring: crude.scroll_front_prestring.unwrap_or("«".to_string()),
+ scroll_front_poststring: crude.scroll_front_poststring.unwrap_or(" ".to_string()),
+ scroll_back_prefix: crude.scroll_back_prefix.unwrap_or("".to_string()),
+ scroll_back_postfix: crude.scroll_back_postfix.unwrap_or("".to_string()),
+ scroll_back_prestring: crude.scroll_back_prestring.unwrap_or(" ".to_string()),
+ scroll_back_poststring: crude.scroll_back_poststring.unwrap_or("»".to_string()),
+ padding_prefix: crude.padding_prefix.unwrap_or(' '),
+ padding_postfix: crude.padding_postfix.unwrap_or(' '),
+ padding_fill: crude.padding_fill.unwrap_or(' '),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct TabThemeCharsInference {
+ pub tab_divider_length: usize,
+ pub tab_prefix_i_length: usize,
+ pub tab_postfix_i_length: usize,
+ pub tab_prefix_a_length: usize,
+ pub tab_postfix_a_length: usize,
+ pub scroll_front_static_length: usize,
+ pub scroll_back_static_length: usize,
+ pub active_tab_extra_width: usize,
+ pub inactive_tab_extra_width: usize,
+}
+
+impl TabThemeCharsInference {
+ fn from_chars(chars: &TabThemeChars) -> Self {
+ Self {
+ tab_divider_length: chars.divider.width(),
+ tab_prefix_i_length: chars.prefix_i.width(),
+ tab_prefix_a_length: chars.prefix_a.width(),
+ tab_postfix_i_length: chars.postfix_i.width(),
+ tab_postfix_a_length: chars.postfix_a.width(),
+ scroll_front_static_length: chars.scroll_front_prefix.width()
+ + chars.scroll_front_postfix.width()
+ + chars.scroll_front_prestring.width()
+ + chars.scroll_front_poststring.width(),
+ scroll_back_static_length: chars.scroll_back_prefix.width()
+ + chars.scroll_back_postfix.width()
+ + chars.scroll_back_prestring.width()
+ + chars.scroll_back_poststring.width(),
+ active_tab_extra_width: chars.prefix_a.width() + chars.postfix_a.width(),
+ inactive_tab_extra_width: chars.prefix_i.width() + chars.postfix_i.width(),
+ }
+ }
+
+ pub fn calc_scroll_tags_width(&self, num_tabs: usize) -> usize {
+ let max_num_width = num_tabs.checked_ilog10().unwrap_or(0) as usize + 1;
+ 2 * max_num_width + self.scroll_front_static_length + self.scroll_back_static_length
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct TabThemeColors {
+ pub prefix_a: Style,
+ pub postfix_a: Style,
+ pub tab_a: Style,
+ pub prefix_i: Style,
+ pub postfix_i: Style,
+ pub tab_i: Style,
+ pub divider_ii: Style,
+ pub divider_ia: Style,
+ pub divider_ai: Style,
+ pub scroll_front_prefix: Style,
+ pub scroll_front_postfix: Style,
+ pub scroll_front: Style,
+ pub scroll_back_prefix: Style,
+ pub scroll_back_postfix: Style,
+ pub scroll_back: Style,
+ pub padding_prefix: Style,
+ pub padding_postfix: Style,
+ pub padding_fill: Style,
+}
+
+impl From for TabThemeColors {
+ fn from(crude: TabThemeColorRaw) -> Self {
+ let tab_a = crude.active.map(|s| s.as_style()).unwrap_or(
+ Style::new()
+ .bg(Color::LightBlue)
+ .fg(Color::Black)
+ .add_modifier(Modifier::BOLD),
+ );
+ let prefix_a = tab_a.patch_optionally(crude.active_prefix.map(|s| s.as_style()));
+ let postfix_a = prefix_a.patch_optionally(crude.active_postfix.map(|s| s.as_style()));
+
+ let tab_i = crude.inactive.map(|s| s.as_style()).unwrap_or(Style::new());
+ let prefix_i = tab_i.patch_optionally(crude.inactive_prefix.map(|s| s.as_style()));
+ let postfix_i = prefix_i.patch_optionally(crude.inactive_postfix.map(|s| s.as_style()));
+
+ let divider_ii = crude
+ .divider_ii
+ .map(|s| s.as_style())
+ .unwrap_or(Style::new());
+ let divider_ia = divider_ii.patch_optionally(crude.divider_ia.map(|s| s.as_style()));
+ let divider_ai = divider_ia.patch_optionally(crude.divider_ai.map(|s| s.as_style()));
+
+ let scroll_front = crude
+ .scroll_front
+ .map(|s| s.as_style())
+ .unwrap_or(Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD));
+ let scroll_front_prefix =
+ scroll_front.patch_optionally(crude.scroll_front_prefix.map(|s| s.as_style()));
+ let scroll_front_postfix =
+ scroll_front_prefix.patch_optionally(crude.scroll_front_postfix.map(|s| s.as_style()));
+
+ let scroll_back = crude
+ .scroll_back
+ .map(|s| s.as_style())
+ .unwrap_or(Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD));
+ let scroll_back_prefix =
+ scroll_back.patch_optionally(crude.scroll_back_prefix.map(|s| s.as_style()));
+ let scroll_back_postfix =
+ scroll_back_prefix.patch_optionally(crude.scroll_back_postfix.map(|s| s.as_style()));
+
+ let padding_fill = crude
+ .padding_fill
+ .map(|s| s.as_style())
+ .unwrap_or(Style::new());
+ let padding_prefix =
+ padding_fill.patch_optionally(crude.padding_prefix.map(|s| s.as_style()));
+ let padding_postfix =
+ padding_prefix.patch_optionally(crude.padding_postfix.map(|s| s.as_style()));
+ Self {
+ prefix_a,
+ postfix_a,
+ tab_a,
+ prefix_i,
+ postfix_i,
+ tab_i,
+ divider_ii,
+ divider_ia,
+ divider_ai,
+ scroll_front_prefix,
+ scroll_front_postfix,
+ scroll_front,
+ scroll_back_prefix,
+ scroll_back_postfix,
+ scroll_back,
+ padding_prefix,
+ padding_postfix,
+ padding_fill,
+ }
}
}
diff --git a/src/config/raw/app/display/tab.rs b/src/config/raw/app/display/tab.rs
index 7a3c853a6..f3ee50b5e 100644
--- a/src/config/raw/app/display/tab.rs
+++ b/src/config/raw/app/display/tab.rs
@@ -1,31 +1,19 @@
use serde::Deserialize;
-use crate::config::clean::app::tab::TabBarDisplayMode;
-
fn default_home_page() -> String {
"home".to_string()
}
-const fn default_max_len() -> usize {
- 16
-}
-
#[derive(Clone, Debug, Deserialize)]
pub struct TabOptionRaw {
#[serde(default = "default_home_page")]
pub home_page: String,
- #[serde(default)]
- pub display_mode: TabBarDisplayMode,
- #[serde(default = "default_max_len")]
- pub max_len: usize,
}
impl std::default::Default for TabOptionRaw {
fn default() -> Self {
Self {
home_page: default_home_page(),
- display_mode: TabBarDisplayMode::default(),
- max_len: 16,
}
}
}
diff --git a/src/config/raw/theme/style.rs b/src/config/raw/theme/style.rs
index a588bd590..fd037a75a 100644
--- a/src/config/raw/theme/style.rs
+++ b/src/config/raw/theme/style.rs
@@ -1,11 +1,96 @@
use colors_transform::{Color, Rgb};
-
+use ratatui::style::{self, Style};
use serde::Deserialize;
-use ratatui::style;
-
use crate::config::clean::theme::style::AppStyle;
+fn str_to_color(s: &str) -> style::Color {
+ match s {
+ "black" => style::Color::Black,
+ "red" => style::Color::Red,
+ "green" => style::Color::Green,
+ "yellow" => style::Color::Yellow,
+ "blue" => style::Color::Blue,
+ "magenta" => style::Color::Magenta,
+ "cyan" => style::Color::Cyan,
+ "gray" => style::Color::Gray,
+ "dark_gray" => style::Color::DarkGray,
+ "light_red" => style::Color::LightRed,
+ "light_green" => style::Color::LightGreen,
+ "light_yellow" => style::Color::LightYellow,
+ "light_blue" => style::Color::LightBlue,
+ "light_magenta" => style::Color::LightMagenta,
+ "light_cyan" => style::Color::LightCyan,
+ "white" => style::Color::White,
+ "reset" => style::Color::Reset,
+ s if s.starts_with('#') => {
+ let rgb = match Rgb::from_hex_str(s) {
+ Ok(s) => s,
+ _ => return style::Color::Reset,
+ };
+ let r = rgb.get_red() as u8;
+ let g = rgb.get_green() as u8;
+ let b = rgb.get_blue() as u8;
+ style::Color::Rgb(r, g, b)
+ }
+ s if s.is_empty() => style::Color::Reset,
+ s => match s.parse::() {
+ Ok(rgb) => {
+ let r = rgb.get_red() as u8;
+ let g = rgb.get_green() as u8;
+ let b = rgb.get_blue() as u8;
+ style::Color::Rgb(r, g, b)
+ }
+ Err(_) => style::Color::Reset,
+ },
+ }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+
+pub struct AppStyleOptionsRaw {
+ pub fg: Option,
+ pub bg: Option,
+ pub bold: Option,
+ pub underline: Option,
+ pub invert: Option,
+}
+
+impl AppStyleOptionsRaw {
+ pub fn as_style(&self) -> Style {
+ let mut add_modifier = style::Modifier::empty();
+ let mut sub_modifier = style::Modifier::empty();
+ if let Some(bold) = self.bold {
+ if bold {
+ add_modifier.insert(style::Modifier::BOLD);
+ } else {
+ sub_modifier.insert(style::Modifier::BOLD);
+ }
+ }
+ if let Some(underline) = self.underline {
+ if underline {
+ add_modifier.insert(style::Modifier::UNDERLINED);
+ } else {
+ sub_modifier.insert(style::Modifier::UNDERLINED);
+ }
+ }
+ if let Some(invert) = self.invert {
+ if invert {
+ add_modifier.insert(style::Modifier::REVERSED);
+ } else {
+ sub_modifier.insert(style::Modifier::REVERSED);
+ }
+ }
+ Style {
+ fg: self.fg.clone().map(|s| str_to_color(s.as_ref())),
+ bg: self.bg.clone().map(|s| str_to_color(s.as_ref())),
+ add_modifier,
+ sub_modifier,
+ //underline_color: None, # only when ratatui with crossterm
+ }
+ }
+}
+
#[derive(Clone, Debug, Deserialize)]
pub struct AppStyleRaw {
#[serde(default)]
@@ -40,45 +125,7 @@ impl AppStyleRaw {
}
pub fn str_to_color(s: &str) -> style::Color {
- match s {
- "black" => style::Color::Black,
- "red" => style::Color::Red,
- "green" => style::Color::Green,
- "yellow" => style::Color::Yellow,
- "blue" => style::Color::Blue,
- "magenta" => style::Color::Magenta,
- "cyan" => style::Color::Cyan,
- "gray" => style::Color::Gray,
- "dark_gray" => style::Color::DarkGray,
- "light_red" => style::Color::LightRed,
- "light_green" => style::Color::LightGreen,
- "light_yellow" => style::Color::LightYellow,
- "light_blue" => style::Color::LightBlue,
- "light_magenta" => style::Color::LightMagenta,
- "light_cyan" => style::Color::LightCyan,
- "white" => style::Color::White,
- "reset" => style::Color::Reset,
- s if s.starts_with('#') => {
- let rgb = match Rgb::from_hex_str(s) {
- Ok(s) => s,
- _ => return style::Color::Reset,
- };
- let r = rgb.get_red() as u8;
- let g = rgb.get_green() as u8;
- let b = rgb.get_blue() as u8;
- style::Color::Rgb(r, g, b)
- }
- s if s.is_empty() => style::Color::Reset,
- s => match s.parse::() {
- Ok(rgb) => {
- let r = rgb.get_red() as u8;
- let g = rgb.get_green() as u8;
- let b = rgb.get_blue() as u8;
- style::Color::Rgb(r, g, b)
- }
- Err(_) => style::Color::Reset,
- },
- }
+ str_to_color(s)
}
}
diff --git a/src/config/raw/theme/tab.rs b/src/config/raw/theme/tab.rs
index 1e1fef692..be711136f 100644
--- a/src/config/raw/theme/tab.rs
+++ b/src/config/raw/theme/tab.rs
@@ -1,11 +1,53 @@
use serde::Deserialize;
-use super::style::AppStyleRaw;
+use super::style::AppStyleOptionsRaw;
#[derive(Clone, Debug, Deserialize, Default)]
pub struct TabThemeRaw {
#[serde(default)]
- pub inactive: AppStyleRaw,
+ pub styles: TabThemeColorRaw,
#[serde(default)]
- pub active: AppStyleRaw,
+ pub chars: TabThemeCharsRaw,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct TabThemeColorRaw {
+ pub active_prefix: Option,
+ pub active_postfix: Option,
+ pub active: Option,
+ pub inactive_prefix: Option,
+ pub inactive_postfix: Option,
+ pub inactive: Option,
+ pub divider_ii: Option,
+ pub divider_ia: Option,
+ pub divider_ai: Option,
+ pub scroll_front_prefix: Option,
+ pub scroll_front_postfix: Option,
+ pub scroll_front: Option,
+ pub scroll_back_prefix: Option,
+ pub scroll_back_postfix: Option,
+ pub scroll_back: Option,
+ pub padding_prefix: Option,
+ pub padding_postfix: Option,
+ pub padding_fill: Option,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct TabThemeCharsRaw {
+ pub active_prefix: Option,
+ pub active_postfix: Option,
+ pub inactive_prefix: Option,
+ pub inactive_postfix: Option,
+ pub divider: Option,
+ pub scroll_front_prefix: Option,
+ pub scroll_front_postfix: Option,
+ pub scroll_front_prestring: Option,
+ pub scroll_front_poststring: Option,
+ pub scroll_back_prefix: Option,
+ pub scroll_back_postfix: Option,
+ pub scroll_back_prestring: Option,
+ pub scroll_back_poststring: Option,
+ pub padding_prefix: Option,
+ pub padding_postfix: Option,
+ pub padding_fill: Option,
}
diff --git a/src/context/app_context.rs b/src/context/app_context.rs
index 16401ca38..35af55330 100644
--- a/src/context/app_context.rs
+++ b/src/context/app_context.rs
@@ -70,7 +70,7 @@ impl AppContext {
quit: QuitAction::DoNot,
events,
args,
- tab_context: TabContext::new(config.tab_options_ref().display),
+ tab_context: TabContext::new(),
local_state: None,
search_context: None,
message_queue: MessageQueue::new(),
diff --git a/src/context/tab_context.rs b/src/context/tab_context.rs
index fa81c4a8d..1df729577 100644
--- a/src/context/tab_context.rs
+++ b/src/context/tab_context.rs
@@ -3,21 +3,18 @@ use std::collections::HashMap;
use uuid::Uuid;
-use crate::config::clean::app::tab::{TabBarDisplayMode, TabBarDisplayOption};
use crate::tab::JoshutoTab;
#[derive(Default)]
pub struct TabContext {
pub index: usize,
pub tab_order: Vec,
- pub display: TabBarDisplayOption,
tabs: HashMap,
}
impl TabContext {
- pub fn new(display: TabBarDisplayOption) -> Self {
+ pub fn new() -> Self {
Self {
- display,
..Default::default()
}
}
@@ -28,6 +25,17 @@ impl TabContext {
pub fn tab_ref(&self, id: &Uuid) -> Option<&JoshutoTab> {
self.tabs.get(id)
}
+
+ pub fn tab_refs_in_order(&self) -> Vec<&JoshutoTab> {
+ let mut tab_refs: Vec<&JoshutoTab> = vec![];
+ for tab_id in self.tab_order.iter() {
+ if let Some(tab_ref) = self.tab_ref(tab_id) {
+ tab_refs.push(tab_ref);
+ }
+ }
+ tab_refs
+ }
+
pub fn tab_mut(&mut self, id: &Uuid) -> Option<&mut JoshutoTab> {
self.tabs.get_mut(id)
}
@@ -61,32 +69,4 @@ impl TabContext {
pub fn iter_mut(&mut self) -> IterMut {
self.tabs.iter_mut()
}
-
- pub fn tab_title_width(&self) -> usize {
- self.tabs
- .values()
- .map(|tab| {
- let title_len = tab.tab_title().len();
- (title_len > self.display.max_len)
- .then(|| self.display.max_len)
- .unwrap_or(title_len)
- })
- .sum()
- }
-
- pub fn tab_area_width(&self) -> usize {
- let width_without_divider = match self.display.mode {
- TabBarDisplayMode::Number => (1..=self.len()).map(|n| n.to_string().len() + 2).sum(), // each number has a horizontal padding(1 char width)
- TabBarDisplayMode::Directory => self.tab_title_width(),
- TabBarDisplayMode::All => {
- // [number][: ](width = 2)[title]
- self.tab_title_width()
- + (1..=self.len())
- .map(|n| n.to_string().len() + 2)
- .sum::()
- }
- };
-
- width_without_divider + 3 * (self.len() - 1)
- }
}
diff --git a/src/key_command/command.rs b/src/key_command/command.rs
index ae2ad6604..d21863222 100644
--- a/src/key_command/command.rs
+++ b/src/key_command/command.rs
@@ -8,7 +8,6 @@ use crate::config::clean::app::display::line_number::LineNumberStyle;
use crate::config::clean::app::display::new_tab::NewTabMode;
use crate::config::clean::app::display::sort_type::SortType;
use crate::config::clean::app::search::CaseSensitivity;
-use crate::config::clean::app::tab::TabBarDisplayMode;
use crate::io::FileOperationOptions;
#[derive(Clone, Debug)]
@@ -165,7 +164,6 @@ pub enum Command {
pattern: String,
},
- SetTabBarDisplayMode(TabBarDisplayMode),
NewTab {
mode: NewTabMode,
},
diff --git a/src/key_command/constants.rs b/src/key_command/constants.rs
index 765d94bf3..1edfb428c 100644
--- a/src/key_command/constants.rs
+++ b/src/key_command/constants.rs
@@ -72,7 +72,6 @@ cmd_constants![
(CMD_SUBPROCESS_FOREGROUND, "shell"),
(CMD_SUBPROCESS_BACKGROUND, "spawn"),
(CMD_SHOW_TASKS, "show_tasks"),
- (CMD_SET_TAB_BAR_MODE, "tab_bar_mode"),
(CMD_TAB_SWITCH, "tab_switch"),
(CMD_TAB_SWITCH_INDEX, "tab_switch_index"),
(CMD_TOGGLE_HIDDEN, "toggle_hidden"),
diff --git a/src/key_command/impl_appcommand.rs b/src/key_command/impl_appcommand.rs
index 4822ca871..4b8660838 100644
--- a/src/key_command/impl_appcommand.rs
+++ b/src/key_command/impl_appcommand.rs
@@ -90,7 +90,6 @@ impl AppCommand for Command {
Self::SwitchLineNums(_) => CMD_SWITCH_LINE_NUMBERS,
Self::SetLineMode(_) => CMD_SET_LINEMODE,
- Self::SetTabBarDisplayMode(_) => CMD_SET_TAB_BAR_MODE,
Self::TabSwitch { .. } => CMD_TAB_SWITCH,
Self::TabSwitchIndex { .. } => CMD_TAB_SWITCH_INDEX,
Self::ToggleHiddenFiles => CMD_TOGGLE_HIDDEN,
diff --git a/src/key_command/impl_appexecute.rs b/src/key_command/impl_appexecute.rs
index 86200dab9..b4033c281 100644
--- a/src/key_command/impl_appexecute.rs
+++ b/src/key_command/impl_appexecute.rs
@@ -155,9 +155,6 @@ impl AppExecute for Command {
Self::ToggleHiddenFiles => show_hidden::toggle_hidden(context),
- Self::SetTabBarDisplayMode(mode) => {
- tab_bar_mode::set_tab_bar_display_mode(context, mode)
- }
Self::TabSwitch { offset } => {
tab_ops::tab_switch(context, *offset).map_err(|e| e.into())
}
diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs
index 39fe88653..3922b323e 100644
--- a/src/key_command/impl_comment.rs
+++ b/src/key_command/impl_comment.rs
@@ -1,8 +1,5 @@
use crate::{
- config::clean::app::{
- display::{line_mode::LineMode, sort_type::SortType},
- tab::TabBarDisplayMode,
- },
+ config::clean::app::display::{line_mode::LineMode, sort_type::SortType},
io::FileOperationOptions,
};
@@ -131,15 +128,6 @@ impl CommandComment for Command {
Self::FilterRegex { .. } => "Filter directory list with regex",
Self::FilterString { .. } => "Filter directory list",
- Self::SetTabBarDisplayMode(mode) => match mode {
- TabBarDisplayMode::Number => "TabBar only display with number ( 1 | 2 | 3 )",
- TabBarDisplayMode::Directory => {
- "TabBar only display with directory ( dir1 | dir2 | dir3 )"
- }
- TabBarDisplayMode::All => {
- "TabBar display with numbar and directory ( 1: dir1 | 2: dir2 )"
- }
- },
Self::TabSwitch { .. } => "Switch to the next tab",
Self::TabSwitchIndex { .. } => "Switch to a given tab",
Self::Help => "Open this help page",
diff --git a/src/key_command/impl_from_str.rs b/src/key_command/impl_from_str.rs
index b3b34505f..33fdfefd2 100644
--- a/src/key_command/impl_from_str.rs
+++ b/src/key_command/impl_from_str.rs
@@ -8,7 +8,6 @@ use crate::config::clean::app::display::line_number::LineNumberStyle;
use crate::config::clean::app::display::new_tab::NewTabMode;
use crate::config::clean::app::display::sort_type::SortType;
use crate::config::clean::app::search::CaseSensitivity;
-use crate::config::clean::app::tab::TabBarDisplayMode;
use crate::error::{AppError, AppErrorKind};
use crate::io::FileOperationOptions;
use crate::util::unix;
@@ -489,10 +488,6 @@ impl std::str::FromStr for Command {
}
} else if command == CMD_SET_LINEMODE {
Ok(Self::SetLineMode(LineMode::from_string(arg)?))
- } else if command == CMD_SET_TAB_BAR_MODE {
- Ok(Self::SetTabBarDisplayMode(TabBarDisplayMode::from_str(
- arg,
- )?))
} else if command == CMD_TAB_SWITCH {
match arg.parse::() {
Ok(s) => Ok(Self::TabSwitch { offset: s }),
diff --git a/src/tab/tab_struct.rs b/src/tab/tab_struct.rs
index 9b64b09fe..09e999900 100644
--- a/src/tab/tab_struct.rs
+++ b/src/tab/tab_struct.rs
@@ -1,6 +1,4 @@
-use std::borrow::Cow;
use std::collections::HashMap;
-use std::ffi::OsStr;
use std::path;
use crate::config::clean::app::display::tab::TabDisplayOption;
@@ -111,11 +109,4 @@ impl JoshutoTab {
self.history.get_mut(child_path.as_path())
}
-
- pub fn tab_title(&self) -> Cow<'_, str> {
- self.cwd()
- .file_name()
- .unwrap_or_else(|| OsStr::new("/"))
- .to_string_lossy()
- }
}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 709e9a45c..a76b29aa7 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,6 +1,7 @@
mod backend;
mod preview_area;
mod rect;
+mod tab_list_builder;
pub mod views;
pub mod widgets;
@@ -8,3 +9,4 @@ pub mod widgets;
pub use backend::*;
pub use preview_area::*;
pub use rect::*;
+pub use tab_list_builder::*;
diff --git a/src/ui/tab_list_builder.rs b/src/ui/tab_list_builder.rs
new file mode 100644
index 000000000..2fda9268c
--- /dev/null
+++ b/src/ui/tab_list_builder.rs
@@ -0,0 +1,1040 @@
+use ratatui::text::Span;
+use std::isize;
+use std::path::Path;
+
+use crate::config::clean::theme::tab::TabTheme;
+use crate::util::string::UnicodeTruncate;
+use crate::HOME_DIR;
+use unicode_width::UnicodeWidthStr;
+
+// This file provides stuff to factor a tab-line from a list of paths.
+//
+// The tab-line logic uses these basic cases:
+//
+// * Case 0: Not enough space for anything useful. ⇒ Empty tab line
+// * Case 1: All [1..n] tabs fit in long form
+// * If only one tab exists, then...
+// * Case 2: The only tab did _not_ fit in long form (would have been Case 1)...
+// * Case 2a: Single tab fits in short form
+// * Case 2b: Single tab fits only in short form, without pre- and postfix
+// * Case 2c: Single tab fits only without pre- and postfix, and further
+// shortened with an ellipsis
+// * If more than one tab exist, then...
+// * Case 3: The active tab fits in long form together with the others in short form
+// * Case 4: Not all tabs can be shown; Scrolling needed...
+// * Case 4a: The active tab fits between the scroll tags in long form
+// * Case 4b: The active tab fits between the scroll tags only in short form
+// * Case 4c: The active tab fits only without scroll tags and pre- and postfix
+// * Case 4d: The active tab fits only without scroll tags and pre- and postfix
+// and further shortened with ellipsis
+
+pub struct TabLabel {
+ long: String,
+ short: String,
+}
+
+impl TabLabel {
+ fn from_path(path: &Path) -> TabLabel {
+ let mut full_path_str = path.as_os_str().to_str().unwrap().to_string();
+ if let Some(home_dir) = HOME_DIR.as_ref() {
+ let home_dir_str = home_dir.to_string_lossy().into_owned();
+ full_path_str = full_path_str.replace(&home_dir_str, "~");
+ }
+ let last = Path::new(&full_path_str)
+ .file_name()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .to_string();
+ TabLabel {
+ long: full_path_str,
+ short: last,
+ }
+ }
+}
+
+#[derive(PartialEq, Debug)]
+enum TabBarElement {
+ // Note: The Tab-Elements also store the index, eventhough it's not used
+ // for the tab-bar rendering at all. The reason is that this would allow
+ // an easier implementation for other features later on, like changing
+ // tabs by mouse clicks.
+ // It's a valuable information that comes for free here.
+ DividerII,
+ DividerIA,
+ DividerAI,
+ PrefixI,
+ PostfixI,
+ PrefixA,
+ PostfixA,
+ TabI(usize, String), // index, label-string
+ TabA(usize, String), // index, label-string
+ ScrollFrontPrefix,
+ ScrollFrontPostfix,
+ ScrollFront(usize), // number of hidden tabs
+ ScrollBackPrefix,
+ ScrollBackPostfix,
+ ScrollBack(usize), // number of hidden tab
+ PaddingPrefix,
+ PaddingPostfix,
+ PaddingFill(usize), // width of padding
+}
+
+fn check_fit_and_build_sequence(
+ labels: &[String],
+ available_width: usize,
+ extra_width: usize,
+ current_index: usize,
+) -> Option> {
+ if labels.iter().map(|l| l.width()).sum::() + extra_width <= available_width {
+ let r = labels
+ .iter()
+ .enumerate()
+ .flat_map(|(ix, l)| {
+ let mut section = Vec::with_capacity(4);
+ if ix > 0 {
+ if ix == current_index {
+ section.push(TabBarElement::DividerIA);
+ } else if ix - 1 == current_index {
+ section.push(TabBarElement::DividerAI);
+ } else {
+ section.push(TabBarElement::DividerII);
+ }
+ };
+ if ix == current_index {
+ section.push(TabBarElement::PrefixA);
+ section.push(TabBarElement::TabA(ix, l.to_string()));
+ section.push(TabBarElement::PostfixA);
+ } else {
+ section.push(TabBarElement::PrefixI);
+ section.push(TabBarElement::TabI(ix, l.to_string()));
+ section.push(TabBarElement::PostfixI);
+ }
+ section
+ })
+ .collect();
+ Some(r)
+ } else {
+ None
+ }
+}
+
+fn factor_tab_bar_sequence(
+ available_width: usize,
+ tab_records: &Vec<&TabLabel>,
+ current_index: usize,
+ config: &TabTheme,
+) -> Vec {
+ //##################################################################
+ //## Case 0: less available width than 2 units -> just show nothing
+ //##################################################################
+ if available_width < 2 {
+ return vec![];
+ }
+ //## Not case 0. Let's continue...
+
+ let tab_num = tab_records.len();
+ let labels_are_indexed = tab_num > 1;
+ let extra_width = config.inference.tab_divider_length * (tab_num - 1)
+ + config.inference.active_tab_extra_width
+ + config.inference.inactive_tab_extra_width * (tab_num - 1);
+
+ let all_labels_as_long: Vec<_> = tab_records
+ .iter()
+ .enumerate()
+ .map(|(ix, &r)| {
+ if labels_are_indexed {
+ format!("{}: {}", ix + 1, &r.long)
+ } else {
+ String::from(&r.long)
+ }
+ })
+ .collect();
+
+ //##############################################################
+ //## Case 1: all tabs fit with long form label
+ //##############################################################
+ if let Some(result) = check_fit_and_build_sequence(
+ &all_labels_as_long,
+ available_width,
+ extra_width,
+ current_index,
+ ) {
+ return result;
+ }
+ //## Not case 1. Let's continue...
+
+ //##############################################################
+ //## Case 2: single tab that does _not_ fit in long form
+ //##############################################################
+ if tab_num == 1 {
+ let label = String::from(&tab_records[0].short);
+ if label.width() as isize
+ <= available_width as isize - config.inference.active_tab_extra_width as isize
+ {
+ return vec![
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, label),
+ TabBarElement::PostfixA,
+ ];
+ }
+ if label.width() <= available_width {
+ return vec![TabBarElement::TabA(0, label)];
+ }
+ return vec![TabBarElement::TabA(
+ 0,
+ format!("{}…", label.trunc(available_width - 1)),
+ )];
+ }
+ //## Not case 2. Let's continue...
+
+ let all_labels_as_short: Vec<_> = tab_records
+ .iter()
+ .enumerate()
+ .map(|(ix, &r)| {
+ if labels_are_indexed {
+ format!("{}: {}", ix + 1, &r.short)
+ } else {
+ String::from(&r.short)
+ }
+ })
+ .collect();
+
+ let all_labels_as_short_except_current: Vec = all_labels_as_short
+ .iter()
+ .zip(all_labels_as_long.iter())
+ .enumerate()
+ .map(|(ix, (short, long))| {
+ if ix == current_index {
+ long.to_string()
+ } else {
+ short.to_string()
+ }
+ })
+ .collect();
+
+ //####################################################################
+ //## Case 3: active tab fits in long form and all others in short form
+ //#####################################################################
+ if let Some(result) = check_fit_and_build_sequence(
+ &all_labels_as_short_except_current,
+ available_width,
+ extra_width,
+ current_index,
+ ) {
+ return result;
+ }
+ //## Not case 3. Let's continue...
+
+ //####################################################################
+ //## Case 4: more than one tab and the tabs don't fit at once
+ //## (=> we need scrolling)
+ //####################################################################
+ let scroll_tags_width = config.inference.calc_scroll_tags_width(tab_num);
+ let scrollable_width = if scroll_tags_width > available_width {
+ 0
+ } else {
+ available_width - scroll_tags_width
+ };
+
+ let carousel_labels = if all_labels_as_long[current_index].width()
+ + config.inference.active_tab_extra_width
+ <= scrollable_width
+ {
+ //## Case 4a: active tab fits in long form between scroll tags
+ &all_labels_as_short_except_current
+ } else if all_labels_as_short[current_index].width() + config.inference.active_tab_extra_width
+ <= scrollable_width
+ {
+ //## Case 4b: active tab fits only in short form between scroll tags
+ &all_labels_as_short
+ } else if all_labels_as_short[current_index].width() <= available_width {
+ //## Case 4c: active tab fits only without scroll tags and pre- and postfix
+ return vec![TabBarElement::TabA(
+ current_index,
+ String::from(&all_labels_as_short[current_index]),
+ )];
+ } else {
+ //## Case 4d: active only fits when shortened with ellipsis
+ return vec![TabBarElement::TabA(
+ current_index,
+ format!(
+ "{}…",
+ &all_labels_as_short[current_index].trunc(available_width - 1)
+ ),
+ )];
+ };
+
+ //####################################################################
+ //## Case 4a/b: Sub-set of tabs shown with scroll tags
+ //####################################################################
+ // Preparing mutable variables for the following looping alogorithm.
+ // If we end up here, we know that at least the scroll tags and the label of the
+ // current tab fit into the available space.
+ let mut remaining_width = scrollable_width as isize;
+ let mut tab_bar_elements: Vec = Vec::new();
+ let mut ix_front = if current_index >= 1 {
+ Some(current_index - 1)
+ } else {
+ None
+ };
+ let mut ix_back = Some(current_index);
+ let mut try_to_take_next_from_front = false;
+ let mut remaining_left: Option = None;
+ let mut remaining_right: Option = None;
+
+ // Loop until the visible space of the tab-bar is full or until we run out of tabs
+ loop {
+ // Pick the next tab alternating from before and after the current tab as long as
+ // another tab is available from the respective side.
+ let (taking_from_front, ix_to_take) = match (ix_front, ix_back) {
+ (Some(ixf), Some(ixb)) => (
+ try_to_take_next_from_front,
+ if try_to_take_next_from_front {
+ ixf
+ } else {
+ ixb
+ },
+ ),
+ (Some(ixf), None) => (true, ixf), //only tabs before current to be added
+ (None, Some(ixb)) => (false, ixb), //only tabs after current to be added
+ (None, None) => break, //for both sides hold, either there are no more
+ //tabs or the next tab does not fit anymore
+ };
+ try_to_take_next_from_front = !try_to_take_next_from_front;
+ let is_active_tab = ix_to_take == current_index;
+
+ // Pick this loop's tab that shall be added to the tab-bar if it still fits
+ if let Some(label_to_add) = carousel_labels.get(ix_to_take) {
+ let needed_width = (if is_active_tab {
+ config.inference.active_tab_extra_width
+ } else {
+ config.inference.inactive_tab_extra_width + config.inference.tab_divider_length
+ } + label_to_add.width()) as isize;
+ if needed_width <= remaining_width {
+ // This next tab still fits into the available space; let's add it...
+ remaining_width -= needed_width;
+ if taking_from_front {
+ // Next tab is added to the front.
+ // This cannot be the active tab because we start adding from the back side.
+ assert!(!is_active_tab);
+ // Add the tab-bar elements...
+ tab_bar_elements.insert(
+ 0,
+ if ix_to_take + 1 == current_index {
+ TabBarElement::DividerIA
+ } else {
+ TabBarElement::DividerII
+ },
+ );
+ tab_bar_elements.insert(0, TabBarElement::PostfixI);
+ tab_bar_elements
+ .insert(0, TabBarElement::TabI(ix_to_take, label_to_add.to_string()));
+ tab_bar_elements.insert(0, TabBarElement::PrefixI);
+ // Prepare the next loop...
+ if ix_to_take > 0 {
+ ix_front = Some(ix_to_take - 1);
+ } else {
+ ix_front = None;
+ }
+ } else {
+ // Next tab is added to the back.
+ // Add the tab-bar elements...
+ if !is_active_tab {
+ tab_bar_elements.push(
+ if ix_to_take > 0 && ix_to_take - 1 == current_index {
+ TabBarElement::DividerAI
+ } else {
+ TabBarElement::DividerII
+ },
+ );
+ }
+ if is_active_tab {
+ tab_bar_elements.push(TabBarElement::PrefixA);
+ tab_bar_elements
+ .push(TabBarElement::TabA(ix_to_take, label_to_add.to_string()));
+ tab_bar_elements.push(TabBarElement::PostfixA);
+ } else {
+ tab_bar_elements.push(TabBarElement::PrefixI);
+ tab_bar_elements
+ .push(TabBarElement::TabI(ix_to_take, label_to_add.to_string()));
+ tab_bar_elements.push(TabBarElement::PostfixI);
+ }
+ // Prepare the next loop...
+ if ix_to_take < tab_num - 1 {
+ ix_back = Some(ix_to_take + 1);
+ } else {
+ ix_back = None;
+ }
+ }
+ } else {
+ // This next tab does NOT fit anymore.
+ // This must not be the active tab though, as this case was handeled before.
+ assert!(!is_active_tab);
+ // Break the processing of the current side,
+ // and store the number of remaining tabs for the respective scroll tag.
+ if taking_from_front {
+ remaining_left = Some(ix_to_take + 1);
+ ix_front = None;
+ } else {
+ remaining_right = Some(tab_num - ix_to_take);
+ ix_back = None;
+ }
+ }
+ } else {
+ // The index for the label to pick must always exists.
+ // Otherwise, there would be a logical bug in this algorithm.
+ unreachable!();
+ }
+ } // End of loop building up the scrollable tab-bar.
+ // The visible elements for the scrollable bar are composed.
+ // Only the scroll-tags on each end of the bar and the padding are missing.
+ // Let's add them...
+ // left scroll tag...
+ tab_bar_elements.insert(0, TabBarElement::ScrollFrontPostfix);
+ tab_bar_elements.insert(
+ 0,
+ TabBarElement::ScrollFront(if let Some(remains) = remaining_left {
+ remains
+ } else {
+ 0
+ }),
+ );
+ tab_bar_elements.insert(0, TabBarElement::ScrollFrontPrefix);
+ // padding...
+ if remaining_width > 0 {
+ tab_bar_elements.push(TabBarElement::PaddingPrefix);
+ if remaining_width > 2 {
+ tab_bar_elements.push(TabBarElement::PaddingFill(remaining_width as usize - 2));
+ }
+ if remaining_width > 1 {
+ tab_bar_elements.push(TabBarElement::PaddingPostfix);
+ }
+ }
+ // right scroll tag...
+ tab_bar_elements.push(TabBarElement::ScrollBackPrefix);
+ tab_bar_elements.push(TabBarElement::ScrollBack(
+ if let Some(remains) = remaining_right {
+ remains
+ } else {
+ 0
+ },
+ ));
+ tab_bar_elements.push(TabBarElement::ScrollBackPostfix);
+
+ // That was it...
+ tab_bar_elements
+}
+
+fn factor_tab_bar_spans_from_sequence<'a>(
+ tab_bar_elements: Vec,
+ config: &TabTheme,
+) -> Vec> {
+ tab_bar_elements
+ .into_iter()
+ .map(|e| match e {
+ TabBarElement::PrefixA => {
+ Span::styled(String::from(&config.chars.prefix_a), config.styles.prefix_a)
+ }
+ TabBarElement::PostfixA => Span::styled(
+ String::from(&config.chars.postfix_a),
+ config.styles.postfix_a,
+ ),
+ TabBarElement::TabA(_ix, s) => Span::styled(s, config.styles.tab_a),
+ TabBarElement::PrefixI => {
+ Span::styled(String::from(&config.chars.prefix_i), config.styles.prefix_i)
+ }
+ TabBarElement::PostfixI => Span::styled(
+ String::from(&config.chars.postfix_i),
+ config.styles.postfix_i,
+ ),
+ TabBarElement::TabI(_ix, s) => Span::styled(s, config.styles.tab_i),
+ TabBarElement::DividerII => Span::styled(
+ String::from(&config.chars.divider),
+ config.styles.divider_ii,
+ ),
+ TabBarElement::DividerAI => Span::styled(
+ String::from(&config.chars.divider),
+ config.styles.divider_ai,
+ ),
+ TabBarElement::DividerIA => Span::styled(
+ String::from(&config.chars.divider),
+ config.styles.divider_ia,
+ ),
+ TabBarElement::ScrollFront(s) => Span::styled(
+ format!(
+ "{}{}{}",
+ &config.chars.scroll_front_prestring, s, &config.chars.scroll_front_poststring,
+ ),
+ config.styles.scroll_front,
+ ),
+ TabBarElement::ScrollFrontPrefix => Span::styled(
+ String::from(&config.chars.scroll_front_prefix),
+ config.styles.scroll_front_prefix,
+ ),
+ TabBarElement::ScrollFrontPostfix => Span::styled(
+ String::from(&config.chars.scroll_front_postfix),
+ config.styles.scroll_front_postfix,
+ ),
+ TabBarElement::ScrollBack(s) => Span::styled(
+ format!(
+ "{}{}{}",
+ &config.chars.scroll_back_prestring, s, &config.chars.scroll_back_poststring,
+ ),
+ config.styles.scroll_back,
+ ),
+ TabBarElement::ScrollBackPrefix => Span::styled(
+ String::from(&config.chars.scroll_back_prefix),
+ config.styles.scroll_back_prefix,
+ ),
+ TabBarElement::ScrollBackPostfix => Span::styled(
+ String::from(&config.chars.scroll_back_postfix),
+ config.styles.scroll_back_postfix,
+ ),
+ TabBarElement::PaddingPrefix => Span::styled(
+ String::from(config.chars.padding_prefix),
+ config.styles.padding_prefix,
+ ),
+ TabBarElement::PaddingPostfix => Span::styled(
+ String::from(config.chars.padding_postfix),
+ config.styles.padding_postfix,
+ ),
+ TabBarElement::PaddingFill(n) => Span::styled(
+ String::from(config.chars.padding_fill).repeat(n),
+ config.styles.padding_fill,
+ ),
+ })
+ .collect()
+}
+
+pub fn factor_tab_bar_spans<'a>(
+ available_width: usize,
+ tab_paths: &[&'a Path],
+ current_index: usize,
+ config: &TabTheme,
+) -> Vec> {
+ let reps: Vec = tab_paths.iter().map(|p| TabLabel::from_path(p)).collect();
+ let rep_refs: Vec<&TabLabel> = reps.iter().collect();
+ let tab_bar_elements =
+ factor_tab_bar_sequence(available_width, &rep_refs, current_index, config);
+ let tab_bar = factor_tab_bar_spans_from_sequence(tab_bar_elements, config);
+ tab_bar
+}
+
+#[cfg(test)]
+mod tests_facator_tab_bar_sequence {
+ use crate::config::clean::theme::tab::TabTheme;
+ use crate::config::raw::theme::tab::TabThemeRaw;
+
+ use super::{factor_tab_bar_sequence, TabBarElement, TabLabel};
+
+ fn test_config() -> TabTheme {
+ let raw = TabThemeRaw {
+ ..Default::default()
+ };
+ TabTheme::from(raw)
+ }
+
+ #[test]
+ /// If there is less available width than 2,
+ /// an empty tab line is returned.
+ fn too_little_available_width_for_anything() {
+ // Given
+ let tabs = vec![TabLabel {
+ long: "/foo/a".to_string(),
+ short: "a".to_string(),
+ }];
+ // When
+ let elements = factor_tab_bar_sequence(1, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(Vec::::new(), elements)
+ }
+
+ #[test]
+ /// All tabs - even a single one - has a prefix and a postfix.
+ /// In this test case, a single tab just fits exactly to the last character
+ /// (`[/foo/a]` is exactly a width of 8).
+ fn one_tab_that_fits() {
+ // Given
+ let tabs = vec![TabLabel {
+ long: "/foo/a".to_string(),
+ short: "a".to_string(),
+ }];
+ // When
+ let elements = factor_tab_bar_sequence(8, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "/foo/a".to_string()),
+ TabBarElement::PostfixA,
+ ],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there's only a single tab, and the long label does not fit, the short version
+ /// is used instead.
+ /// In this test case, the available width is too short by one unit, so the short
+ /// version of the label will be shown
+ /// (`[a]`).
+ fn one_tab_that_fits_only_in_short_form() {
+ // Given
+ let tabs = vec![TabLabel {
+ long: "/foo/a".to_string(),
+ short: "a".to_string(),
+ }];
+ // When
+ let elements = factor_tab_bar_sequence(7, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "a".to_string()),
+ TabBarElement::PostfixA,
+ ],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there's only a single tab, and the long label does not fit,
+ /// and the short version does not fit with the prefix and postfix,
+ /// the prefix and postfix are omitted.
+ /// In this test case, the available width is too short by one unit
+ /// to use the short form with pre- and post-fix.
+ /// (`aaaaaaa`).
+ fn one_tab_that_fits_only_in_short_form_without_prepostfixes() {
+ // Given
+ let tabs = vec![TabLabel {
+ long: "/foo/a".to_string(),
+ short: "aaaaaaa".to_string(),
+ }];
+ // When
+ let elements = factor_tab_bar_sequence(7, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![TabBarElement::TabA(0, "aaaaaaa".to_string()),],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there's only a single tab, and the long label does not fit,
+ /// and the short version does not fit even without the prefix and postfix,
+ /// the prefix and postfix are omitted and the label is shortened with an ellipsis.
+ /// In this test case, the available width is too short by one unit
+ /// to use the short form without pre- and post-fix.
+ /// (`aaaaa…`).
+ fn case_2c_one_tab_that_does_not_fit_unless_further_shortened() {
+ // Given
+ let tabs = vec![TabLabel {
+ long: "/foo/a".to_string(),
+ short: "aaaaaaa".to_string(),
+ }];
+ // When
+ let elements = factor_tab_bar_sequence(6, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![TabBarElement::TabA(0, "aaaaa…".to_string()),],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there are multiple tabs, a delimiter is put between each two
+ /// adjacent tabs and the label of each tab is prefixed with the tab's index,
+ /// starting counting at 1.
+ /// In this test case, the tab line just fits exactly into the available width
+ /// (`[1: /foo/a]| 2: /foo/b ` has exactly a width of 23 )
+ fn case_1_two_tabs_that_fit() {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/a".to_string(),
+ short: "a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/b".to_string(),
+ short: "b".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(23, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: /foo/b".to_string()),
+ TabBarElement::PostfixI,
+ ],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there are multiple tabs and not all fit with their long-form labels,
+ /// the inactive tabs are shown with their short-form labels.
+ /// In this test case, the tab line's available width is just one unit to short for the long
+ /// form, so the inactive tab will be shown with its shortened label
+ /// (`[1: /foo/a]| 2: b `).
+ fn case_3_two_tabs_fit_shortened() {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/a".to_string(),
+ short: "a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/b".to_string(),
+ short: "b".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(22, &tabs.iter().collect(), 0, &test_config());
+ // Then
+ assert_eq!(
+ vec![
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: b".to_string()),
+ TabBarElement::PostfixI,
+ ],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there are multiple tabs and scrolling is needed,
+ /// when the long form of the active tab does not fit in between the scroll tags,
+ /// the short form is used also for the current tab.
+ ///
+ /// This test would result in this tab-bar (dots are padding):
+ /// `«0 [1: long_name_a]···· 1»`
+ fn multiple_tabs_but_active_one_does_only_fit_in_short_form_with_scroll_tags() {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/long_name_a".to_string(),
+ short: "long_name_a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/long_name_b".to_string(),
+ short: "long_name_b".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(
+ 16 + 3 + 2 + 6 - 1, //label + tab-index + pre/postfix + scroll tags - 1 to make it not fit
+ &tabs.iter().collect(),
+ 0,
+ &test_config(),
+ );
+ // Then
+ assert_eq!(
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: long_name_a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::PaddingPrefix,
+ TabBarElement::PaddingFill(2),
+ TabBarElement::PaddingPostfix,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there are multiple tabs and scrolling is needed,
+ /// when even the short form of the active tab does not fit in between the scroll tags,
+ /// the scroll tags and the tab's pre- and postfix are omitted.
+ ///
+ /// This test would result in this tab-bar (dots are padding):
+ /// `1: long_name_a`
+ fn multiple_tabs_but_active_one_does_not_fit_in_short_form_with_scroll_tags() {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/long_name_a".to_string(),
+ short: "long_name_a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/long_name_b".to_string(),
+ short: "long_name_b".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(
+ 21, //label + tab-index + pre/postfix + scroll tags - 1 to make it not fit
+ &tabs.iter().collect(),
+ 0,
+ &test_config(),
+ );
+ // Then
+ assert_eq!(
+ vec![TabBarElement::TabA(0, "1: long_name_a".to_string()),],
+ elements
+ )
+ }
+
+ #[test]
+ /// If there are multiple tabs and scrolling is needed,
+ /// when even the short form alone does not fit in the available width,
+ /// only the fitting part of the short form with an ellipsis is shown.
+ ///
+ /// This test would result in this tab-bar (dots are padding):
+ /// `1: long…`
+ fn multiple_tabs_but_active_one_does_not_fit_in_short_even_without_scroll_tags() {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/long_name_a".to_string(),
+ short: "long_name_a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/long_name_b".to_string(),
+ short: "long_name_b".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(
+ 8, //label + tab-index + pre/postfix + scroll tags - 1 to make it not fit
+ &tabs.iter().collect(),
+ 0,
+ &test_config(),
+ );
+ // Then
+ assert_eq!(
+ vec![TabBarElement::TabA(0, "1: long…".to_string()),],
+ elements
+ )
+ }
+
+ /// Scenario, used for the next four test cases.
+ fn _scenario_three_tabs(
+ available_width: usize,
+ current_index: usize,
+ expected: Vec,
+ ) {
+ // Given
+ let tabs = vec![
+ TabLabel {
+ long: "/foo/long_name_a".to_string(),
+ short: "long_name_a".to_string(),
+ },
+ TabLabel {
+ long: "/foo/long_name_b".to_string(),
+ short: "long_name_b".to_string(),
+ },
+ TabLabel {
+ long: "/foo/long_name_c".to_string(),
+ short: "long_name_c".to_string(),
+ },
+ ];
+ // When
+ let elements = factor_tab_bar_sequence(
+ available_width,
+ &tabs.iter().collect(),
+ current_index,
+ &test_config(),
+ );
+ // Then
+ assert_eq!(expected, elements)
+ }
+
+ #[test]
+ /// When one or more tabs don't fit anymore into the bar, “scroll tags” are added to both ends,
+ /// showing the number of hidden tabs for each side.
+ ///
+ /// The bar starts with the current tab, and then adds more tabs one by one to each
+ /// side alternating. The first inacive tab added is added to the front.
+ ///
+ /// In this test case, there are three tabs with the first on active which would produce a
+ /// tab-line like this if there is enough space:
+ /// `[1: /foo/long_name_a]| 2: long_name_b | 3: long_name_c ` (width 55)
+ ///
+ /// But we choose a available width which is to short.
+ /// It's just long enough to fit the two tabs and the scroll tags.
+ /// So, this example results in a tab-bar like this:
+ /// `«0·[1: /foo/long_name_a]|·2: long_name_b··1»` (width 44)
+ fn three_tabs_with_overflow_on_the_right_w_no_padding() {
+ _scenario_three_tabs(
+ 44,
+ 0,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/long_name_a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: long_name_b".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+
+ // When scroll tags are shown, the right scroll tag stays right-aligned.
+ // The gap between the rightmost tab and the right scroll tab is filled with a padding section.
+ //
+ // In this test case, we use the same tab-situation as before, but the available
+ // width is only one unit to short for all three tabs to be shown (54 units).
+ // So, there will be a gap of 10 units to be filled with padding.
+ // Like the other elements, also the padding has a prefix and postfix.
+ // The middle (“fill”) character is repeated to fill the gap.
+ #[test]
+ fn three_tabs_with_overflow_on_the_right_w_long_padding() {
+ _scenario_three_tabs(
+ 54,
+ 0,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/long_name_a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: long_name_b".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::PaddingPrefix,
+ TabBarElement::PaddingFill(8),
+ TabBarElement::PaddingPostfix,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+
+ // The padding section has a prefix, a postfix, and a fill-sequence in the middle.
+ // However, this is only possible when the padding has at least a width of 3 chars.
+ // If the padding is only one char wide, only the prefix is shown.
+ // If the padding is only two chars wide, only the prefix and postfix are shown.
+ //
+ // In the next two test cases, we use the same tab-situation as before, but the available
+ // width has a value so that the padding is only one or two characters long.
+ #[test]
+ fn three_tabs_with_overflow_on_the_right_w_one_char_padding() {
+ _scenario_three_tabs(
+ 45,
+ 0,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/long_name_a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: long_name_b".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::PaddingPrefix,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+
+ #[test]
+ fn three_tabs_with_overflow_on_the_right_w_two_char_padding() {
+ _scenario_three_tabs(
+ 46,
+ 0,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(0, "1: /foo/long_name_a".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::DividerAI,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: long_name_b".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::PaddingPrefix,
+ TabBarElement::PaddingPostfix,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+
+ #[test]
+ /// When scrolling is needed, tabs are added alternating to the left and to the right of the
+ /// current tab. The algorithm starts on the left. So, if there are three tabs, and the middle
+ /// one is the current one, and there is only enough space for two tabs, the first and the
+ /// second (current) tab will be shown. The tab right of the current tab will be the one that
+ /// is hidden.
+ fn adding_tabs_to_front_has_precedence_over_adding_to_right() {
+ _scenario_three_tabs(
+ 44,
+ 1,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(0),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(0, "1: long_name_a".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::DividerIA,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(1, "2: /foo/long_name_b".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(1),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+
+ #[test]
+ /// This tests uses the same three-tab screnario as the ones before, but uses the last tab as
+ /// the current one. The available space is again just enough for two tabs.
+ /// As a result, the first tab will now be hidden and the front scroll tag will indicate that
+ /// hidden tab.
+ fn overflowing_on_the_left_side() {
+ _scenario_three_tabs(
+ 44,
+ 2,
+ vec![
+ TabBarElement::ScrollFrontPrefix,
+ TabBarElement::ScrollFront(1),
+ TabBarElement::ScrollFrontPostfix,
+ TabBarElement::PrefixI,
+ TabBarElement::TabI(1, "2: long_name_b".to_string()),
+ TabBarElement::PostfixI,
+ TabBarElement::DividerIA,
+ TabBarElement::PrefixA,
+ TabBarElement::TabA(2, "3: /foo/long_name_c".to_string()),
+ TabBarElement::PostfixA,
+ TabBarElement::ScrollBackPrefix,
+ TabBarElement::ScrollBack(0),
+ TabBarElement::ScrollBackPostfix,
+ ],
+ )
+ }
+}
diff --git a/src/ui/views/tui_folder_view.rs b/src/ui/views/tui_folder_view.rs
index c9ffebac1..68b94ad02 100644
--- a/src/ui/views/tui_folder_view.rs
+++ b/src/ui/views/tui_folder_view.rs
@@ -11,7 +11,7 @@ use crate::preview::preview_file::PreviewFileState;
use crate::ui;
use crate::ui::widgets::{
TuiDirList, TuiDirListDetailed, TuiDirListLoading, TuiFilePreview, TuiFooter, TuiMessage,
- TuiTabBar, TuiTopBar,
+ TuiTopBar,
};
use crate::ui::PreviewArea;
@@ -109,24 +109,6 @@ impl<'a> TuiFolderView<'a> {
height: 1,
}
}
-
- pub fn tab_area(&self, area: &Rect, tabs_width: usize) -> Rect {
- // render tabs
- let tabs_width = tabs_width as u16;
- let tab_width = if tabs_width > area.width {
- area.width
- } else {
- tabs_width
- };
- let topbar_x = area.width.saturating_sub(tab_width);
-
- Rect {
- x: topbar_x,
- y: area.top(),
- width: tab_width,
- height: 1,
- }
- }
}
impl<'a> Widget for TuiFolderView<'a> {
@@ -236,11 +218,7 @@ impl<'a> Widget for TuiFolderView<'a> {
}
let topbar_area = Self::header_area(&area);
- TuiTopBar::new(self.context, curr_tab_cwd).render(topbar_area, buf);
-
- // render tabs
- let tab_area = self.tab_area(&area, self.context.tab_context_ref().tab_area_width());
- TuiTabBar::new(self.context.tab_context_ref()).render(tab_area, buf);
+ TuiTopBar::new(self.context).render(topbar_area, buf);
}
}
diff --git a/src/ui/views/tui_hsplit_view.rs b/src/ui/views/tui_hsplit_view.rs
index 57614efc3..6244e0912 100644
--- a/src/ui/views/tui_hsplit_view.rs
+++ b/src/ui/views/tui_hsplit_view.rs
@@ -5,9 +5,7 @@ use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Paragraph, Widget, Wrap};
use crate::context::AppContext;
-use crate::ui::widgets::{TuiDirListDetailed, TuiFooter, TuiTabBar, TuiTopBar};
-
-const TAB_VIEW_WIDTH: u16 = 15;
+use crate::ui::widgets::{TuiDirListDetailed, TuiFooter, TuiTopBar};
pub struct TuiHSplitView<'a> {
pub context: &'a AppContext,
@@ -113,20 +111,7 @@ impl<'a> Widget for TuiHSplitView<'a> {
width: topbar_width,
height: 1,
};
- TuiTopBar::new(self.context, curr_tab.cwd()).render(rect, buf);
-
- // render tabs
- if self.context.tab_context_ref().len() > 1 {
- let topbar_width = area.width.saturating_sub(TAB_VIEW_WIDTH);
-
- let rect = Rect {
- x: topbar_width,
- y: 0,
- width: TAB_VIEW_WIDTH,
- height: 1,
- };
- TuiTabBar::new(self.context.tab_context_ref()).render(rect, buf);
- }
+ TuiTopBar::new(self.context).render(rect, buf);
}
let other_tab_index = if tab_index % 2 == 0 {
diff --git a/src/ui/views/tui_worker_view.rs b/src/ui/views/tui_worker_view.rs
index 3140d89b4..8ed324961 100644
--- a/src/ui/views/tui_worker_view.rs
+++ b/src/ui/views/tui_worker_view.rs
@@ -22,8 +22,7 @@ impl<'a> Widget for TuiWorkerView<'a> {
}
let rect = Rect { height: 1, ..area };
- let curr_tab = self.context.tab_context_ref().curr_tab_ref();
- TuiTopBar::new(self.context, curr_tab.cwd()).render(rect, buf);
+ TuiTopBar::new(self.context).render(rect, buf);
let rect = Rect {
x: 0,
diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs
index 1871da6df..5c86b1dde 100644
--- a/src/ui/widgets/mod.rs
+++ b/src/ui/widgets/mod.rs
@@ -7,7 +7,6 @@ mod tui_help;
mod tui_menu;
mod tui_message;
mod tui_prompt;
-mod tui_tab;
mod tui_text;
mod tui_topbar;
mod tui_worker;
@@ -21,7 +20,6 @@ pub use self::tui_help::{get_keymap_table, TuiHelp};
pub use self::tui_menu::TuiMenu;
pub use self::tui_message::TuiMessage;
pub use self::tui_prompt::TuiPrompt;
-pub use self::tui_tab::TuiTabBar;
pub use self::tui_text::TuiMultilineText;
pub use self::tui_topbar::TuiTopBar;
pub use self::tui_worker::TuiWorker;
diff --git a/src/ui/widgets/tui_tab.rs b/src/ui/widgets/tui_tab.rs
deleted file mode 100644
index f3315fe88..000000000
--- a/src/ui/widgets/tui_tab.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use ratatui::buffer::Buffer;
-use ratatui::layout::Rect;
-use ratatui::text::{Line, Span};
-use ratatui::widgets::{Paragraph, Widget, Wrap};
-
-use crate::config::clean::app::tab::TabBarDisplayMode;
-use crate::context::TabContext;
-use crate::util::format::format_tab_bar_title_string;
-use crate::THEME_T;
-
-pub struct TuiTabBar<'a> {
- context: &'a TabContext,
-}
-
-impl<'a> TuiTabBar<'a> {
- pub fn new(context: &'a TabContext) -> Self {
- Self { context }
- }
-}
-
-impl<'a> Widget for TuiTabBar<'a> {
- fn render(self, area: Rect, buf: &mut Buffer) {
- let regular_style = THEME_T.tabs.inactive.as_style();
- let selected_style = THEME_T.tabs.active.as_style();
-
- let index = self.context.index;
- let tab_order = self.context.tab_order.as_slice();
-
- let mut spans_vec = vec![];
- for (i, tab_id) in tab_order.iter().enumerate() {
- let curr_style = if i == index {
- selected_style
- } else {
- regular_style
- };
- if let Some(curr_tab) = self.context.tab_ref(tab_id) {
- let preview_text = match self.context.display.mode {
- TabBarDisplayMode::Number => format!(" {} ", i + 1),
- TabBarDisplayMode::Directory => format_tab_bar_title_string(
- self.context.display.max_len,
- None,
- curr_tab.tab_title(),
- ),
- TabBarDisplayMode::All => format_tab_bar_title_string(
- self.context.display.max_len,
- Some(i),
- curr_tab.tab_title(),
- ),
- };
-
- spans_vec.push(Span::styled(preview_text, curr_style));
- spans_vec.push(Span::styled(" | ", regular_style));
- }
- }
-
- spans_vec.pop();
-
- Paragraph::new(Line::from(spans_vec))
- .wrap(Wrap { trim: true })
- .render(area, buf);
- }
-}
diff --git a/src/ui/widgets/tui_topbar.rs b/src/ui/widgets/tui_topbar.rs
index 10ed13157..bcf11da8f 100644
--- a/src/ui/widgets/tui_topbar.rs
+++ b/src/ui/widgets/tui_topbar.rs
@@ -1,75 +1,30 @@
-use std::path::Component;
-use std::path::Path;
-
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Widget};
-use unicode_width::UnicodeWidthStr;
+use tab_list_builder::factor_tab_bar_spans;
use crate::context::AppContext;
-use crate::{HOME_DIR, HOSTNAME, USERNAME};
+use crate::ui::tab_list_builder;
+use crate::THEME_T;
+use crate::{HOSTNAME, USERNAME};
pub struct TuiTopBar<'a> {
pub context: &'a AppContext,
- path: &'a Path,
}
impl<'a> TuiTopBar<'a> {
- pub fn new(context: &'a AppContext, path: &'a Path) -> Self {
- Self { context, path }
+ pub fn new(context: &'a AppContext) -> Self {
+ Self { context }
}
}
impl<'a> Widget for TuiTopBar<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
- let path_style = Style::default()
- .fg(Color::LightBlue)
- .add_modifier(Modifier::BOLD);
-
- let mut ellipses = None;
- let mut curr_path_str = self.path.to_string_lossy().into_owned();
-
- let tab_width = self.context.tab_context_ref().tab_area_width();
let name_width = USERNAME.as_str().len() + HOSTNAME.as_str().len() + 2;
- if tab_width + name_width > area.width as usize {
- curr_path_str = "".to_owned();
- } else if curr_path_str.width() > area.width as usize - tab_width - name_width {
- if let Some(s) = self.path.file_name() {
- let mut short_path = String::new();
- let mut components: Vec = self.path.components().collect();
- components.pop();
-
- for component in components {
- match component {
- Component::RootDir => short_path.push('/'),
- Component::Normal(s) => {
- let ch = s.to_string_lossy().chars().next().unwrap();
- short_path.push(ch);
- short_path.push('/');
- }
- _ => {}
- }
- }
- ellipses = Some(Span::styled(short_path, path_style));
- curr_path_str = s.to_string_lossy().into_owned();
- }
- }
- if self
- .context
- .config_ref()
- .display_options_ref()
- .tilde_in_titlebar()
- {
- if let Some(home_dir) = HOME_DIR.as_ref() {
- let home_dir_str = home_dir.to_string_lossy().into_owned();
- curr_path_str = curr_path_str.replace(&home_dir_str, "~");
- }
- }
-
let username_style = if USERNAME.as_str() == "root" {
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
} else {
@@ -78,19 +33,26 @@ impl<'a> Widget for TuiTopBar<'a> {
.add_modifier(Modifier::BOLD)
};
- let mut text = vec![
+ let mut top_bar_spans = vec![
Span::styled(USERNAME.as_str(), username_style),
Span::styled("@", username_style),
Span::styled(HOSTNAME.as_str(), username_style),
Span::styled(" ", username_style),
];
- if let Some(s) = ellipses {
- text.push(s);
+ let available_tab_width = area.width as usize - name_width;
+ let mut paths = Vec::new();
+ let tabs = self.context.tab_context_ref().tab_refs_in_order();
+ for tab in tabs {
+ paths.push(tab.cwd());
}
-
- text.extend([Span::styled(curr_path_str, path_style)]);
-
- Paragraph::new(Line::from(text)).render(area, buf);
+ let tab_bar_spans = factor_tab_bar_spans(
+ available_tab_width,
+ &paths,
+ self.context.tab_context_ref().index,
+ &THEME_T.tabs,
+ );
+ top_bar_spans.extend(tab_bar_spans);
+ Paragraph::new(Line::from(top_bar_spans)).render(area, buf);
}
}
diff --git a/src/util/format.rs b/src/util/format.rs
index 3a9bc541f..3079840f8 100644
--- a/src/util/format.rs
+++ b/src/util/format.rs
@@ -26,27 +26,3 @@ pub fn mtime_to_string(mtime: time::SystemTime) -> String {
let datetime: chrono::DateTime = mtime.into();
datetime.format(MTIME_FORMATTING).to_string()
}
-
-pub fn format_tab_bar_title_string(
- max_len: usize,
- number: Option,
- title: impl Into,
-) -> String {
- let title: String = title.into();
-
- if let Some(number) = number {
- if title.len() > max_len {
- format!(
- "{}: {}…",
- number + 1,
- title.chars().take(max_len - 1).collect::()
- )
- } else {
- format!("{}: {}", number + 1, title)
- }
- } else if title.len() > max_len {
- format!("{}…", title.chars().take(max_len - 1).collect::())
- } else {
- title.to_string()
- }
-}
diff --git a/src/util/style.rs b/src/util/style.rs
index 2f87a9bb9..96d111c71 100644
--- a/src/util/style.rs
+++ b/src/util/style.rs
@@ -5,6 +5,23 @@ use crate::util::unix;
use crate::THEME_T;
+/// Allows patching a ratatui style if there is `Some` style, otherwise returns a clone.
+pub trait PathStyleIfSome {
+ fn patch_optionally(&self, other: Option