From d62e6fb57e7c35f3b2f165c5a63c809ba21e2c2e Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 3 Jun 2022 11:14:38 +0200 Subject: add(plugin): `compact-bar` & `compact` layout (#1450) * add(plugin): `compact-bar` & `compact` layout * add(nix): `compact-bar` plugin * add(config): `compact-bar` to the config * add(workspace): `compact-bar` to workspace members * add(assets): `compact-bar` * chore(fmt): rustfmt * add(nix): add `compact-bar` * add: compact layout to dump command * nix(build): fix destination of copy command * add(makefile): add `compact-bar` to `plugin-build` * add(layout): `compact-bar` to layout * add: install `compact-bar` plugin * fix(test): update input plugin test * fix(plugin): default colors for compact-bar --- default-plugins/compact-bar/.cargo/config.toml | 2 + default-plugins/compact-bar/Cargo.toml | 13 ++ default-plugins/compact-bar/LICENSE.md | 1 + default-plugins/compact-bar/src/line.rs | 243 +++++++++++++++++++++++++ default-plugins/compact-bar/src/main.rs | 137 ++++++++++++++ default-plugins/compact-bar/src/tab.rs | 84 +++++++++ 6 files changed, 480 insertions(+) create mode 100644 default-plugins/compact-bar/.cargo/config.toml create mode 100644 default-plugins/compact-bar/Cargo.toml create mode 120000 default-plugins/compact-bar/LICENSE.md create mode 100644 default-plugins/compact-bar/src/line.rs create mode 100644 default-plugins/compact-bar/src/main.rs create mode 100644 default-plugins/compact-bar/src/tab.rs (limited to 'default-plugins') diff --git a/default-plugins/compact-bar/.cargo/config.toml b/default-plugins/compact-bar/.cargo/config.toml new file mode 100644 index 000000000..6b77899cb --- /dev/null +++ b/default-plugins/compact-bar/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/default-plugins/compact-bar/Cargo.toml b/default-plugins/compact-bar/Cargo.toml new file mode 100644 index 000000000..6f45e2c40 --- /dev/null +++ b/default-plugins/compact-bar/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "compact-bar" +version = "0.1.0" +authors = ["Alexander Kenji Berthold " ] +edition = "2021" +license = "MIT" + +[dependencies] +colored = "2" +ansi_term = "0.12" +unicode-width = "0.1.8" +zellij-tile = { path = "../../zellij-tile" } +zellij-tile-utils = { path = "../../zellij-tile-utils" } diff --git a/default-plugins/compact-bar/LICENSE.md b/default-plugins/compact-bar/LICENSE.md new file mode 120000 index 000000000..f0608a63a --- /dev/null +++ b/default-plugins/compact-bar/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/default-plugins/compact-bar/src/line.rs b/default-plugins/compact-bar/src/line.rs new file mode 100644 index 000000000..5cd9c6873 --- /dev/null +++ b/default-plugins/compact-bar/src/line.rs @@ -0,0 +1,243 @@ +use ansi_term::ANSIStrings; +use unicode_width::UnicodeWidthStr; + +use crate::{LinePart, ARROW_SEPARATOR}; +use zellij_tile::prelude::*; +use zellij_tile_utils::style; + +fn get_current_title_len(current_title: &[LinePart]) -> usize { + current_title.iter().map(|p| p.len).sum() +} + +// move elements from before_active and after_active into tabs_to_render while they fit in cols +// adds collapsed_tabs to the left and right if there's left over tabs that don't fit +fn populate_tabs_in_tab_line( + tabs_before_active: &mut Vec, + tabs_after_active: &mut Vec, + tabs_to_render: &mut Vec, + cols: usize, + palette: Palette, + capabilities: PluginCapabilities, +) { + let mut middle_size = get_current_title_len(tabs_to_render); + + let mut total_left = 0; + let mut total_right = 0; + loop { + let left_count = tabs_before_active.len(); + let right_count = tabs_after_active.len(); + let collapsed_left = left_more_message(left_count, palette, tab_separator(capabilities)); + let collapsed_right = right_more_message(right_count, palette, tab_separator(capabilities)); + + let total_size = collapsed_left.len + middle_size + collapsed_right.len; + + if total_size > cols { + // break and dont add collapsed tabs to tabs_to_render, they will not fit + break; + } + + let left = if let Some(tab) = tabs_before_active.last() { + tab.len + } else { + usize::MAX + }; + + let right = if let Some(tab) = tabs_after_active.first() { + tab.len + } else { + usize::MAX + }; + + // total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab + let size_by_adding_left = + left.saturating_add(total_size) + .saturating_sub(if left_count == 1 { + collapsed_left.len + } else { + 0 + }); + let size_by_adding_right = + right + .saturating_add(total_size) + .saturating_sub(if right_count == 1 { + collapsed_right.len + } else { + 0 + }); + + let left_fits = size_by_adding_left <= cols; + let right_fits = size_by_adding_right <= cols; + // active tab is kept in the middle by adding to the side that + // has less width, or if the tab on the other side doesn' fit + if (total_left <= total_right || !right_fits) && left_fits { + // add left tab + let tab = tabs_before_active.pop().unwrap(); + middle_size += tab.len; + total_left += tab.len; + tabs_to_render.insert(0, tab); + } else if right_fits { + // add right tab + let tab = tabs_after_active.remove(0); + middle_size += tab.len; + total_right += tab.len; + tabs_to_render.push(tab); + } else { + // there's either no space to add more tabs or no more tabs to add, so we're done + tabs_to_render.insert(0, collapsed_left); + tabs_to_render.push(collapsed_right); + break; + } + } +} + +fn left_more_message(tab_count_to_the_left: usize, palette: Palette, separator: &str) -> LinePart { + if tab_count_to_the_left == 0 { + return LinePart::default(); + } + let more_text = if tab_count_to_the_left < 10000 { + format!(" ← +{} ", tab_count_to_the_left) + } else { + " ← +many ".to_string() + }; + // 238 + // chars length plus separator length on both sides + let more_text_len = more_text.width() + 2 * separator.width(); + let text_color = match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }; + let left_separator = style!(text_color, palette.orange).paint(separator); + let more_styled_text = style!(text_color, palette.orange).bold().paint(more_text); + let right_separator = style!(palette.orange, text_color).paint(separator); + let more_styled_text = + ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string(); + LinePart { + part: more_styled_text, + len: more_text_len, + } +} + +fn right_more_message( + tab_count_to_the_right: usize, + palette: Palette, + separator: &str, +) -> LinePart { + if tab_count_to_the_right == 0 { + return LinePart::default(); + }; + let more_text = if tab_count_to_the_right < 10000 { + format!(" +{} → ", tab_count_to_the_right) + } else { + " +many → ".to_string() + }; + // chars length plus separator length on both sides + let more_text_len = more_text.width() + 2 * separator.width(); + let text_color = match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }; + let left_separator = style!(text_color, palette.orange).paint(separator); + let more_styled_text = style!(text_color, palette.orange).bold().paint(more_text); + let right_separator = style!(palette.orange, text_color).paint(separator); + let more_styled_text = + ANSIStrings(&[left_separator, more_styled_text, right_separator]).to_string(); + LinePart { + part: more_styled_text, + len: more_text_len, + } +} + +fn tab_line_prefix( + session_name: Option<&str>, + mode: InputMode, + palette: Palette, + cols: usize, +) -> Vec { + let prefix_text = " Zellij ".to_string(); + + let prefix_text_len = prefix_text.chars().count(); + let text_color = match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }; + let bg_color = match palette.theme_hue { + ThemeHue::Dark => palette.black, + ThemeHue::Light => palette.white, + }; + let prefix_styled_text = style!(text_color, bg_color).bold().paint(prefix_text); + let mut parts = vec![LinePart { + part: prefix_styled_text.to_string(), + len: prefix_text_len, + }]; + if let Some(name) = session_name { + let name_part = format!("({}) ", name); + let name_part_len = name_part.width(); + let text_color = match palette.theme_hue { + ThemeHue::Dark => palette.white, + ThemeHue::Light => palette.black, + }; + let name_part_styled_text = style!(text_color, bg_color).bold().paint(name_part); + if cols.saturating_sub(prefix_text_len) >= name_part_len { + parts.push(LinePart { + part: name_part_styled_text.to_string(), + len: name_part_len, + }) + } + } + let mode_part = format!("({:?})", mode); + let mode_part_len = mode_part.width(); + let mode_part_styled_text = style!(text_color, bg_color).bold().paint(mode_part); + if cols.saturating_sub(prefix_text_len) >= mode_part_len { + parts.push(LinePart { + part: format!("({:^6})", mode_part_styled_text), + len: mode_part_len, + }) + } + parts +} + +pub fn tab_separator(capabilities: PluginCapabilities) -> &'static str { + if !capabilities.arrow_fonts { + ARROW_SEPARATOR + } else { + "" + } +} + +pub fn tab_line( + session_name: Option<&str>, + mut all_tabs: Vec, + active_tab_index: usize, + cols: usize, + palette: Palette, + capabilities: PluginCapabilities, + mode: InputMode, +) -> Vec { + let mut tabs_after_active = all_tabs.split_off(active_tab_index); + let mut tabs_before_active = all_tabs; + let active_tab = if !tabs_after_active.is_empty() { + tabs_after_active.remove(0) + } else { + tabs_before_active.pop().unwrap() + }; + let mut prefix = tab_line_prefix(session_name, mode, palette, cols); + let prefix_len = get_current_title_len(&prefix); + + // if active tab alone won't fit in cols, don't draw any tabs + if prefix_len + active_tab.len > cols { + return prefix; + } + + let mut tabs_to_render = vec![active_tab]; + + populate_tabs_in_tab_line( + &mut tabs_before_active, + &mut tabs_after_active, + &mut tabs_to_render, + cols.saturating_sub(prefix_len), + palette, + capabilities, + ); + prefix.append(&mut tabs_to_render); + prefix +} diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs new file mode 100644 index 000000000..3e5e4a184 --- /dev/null +++ b/default-plugins/compact-bar/src/main.rs @@ -0,0 +1,137 @@ +mod line; +mod tab; + +use std::cmp::{max, min}; +use std::convert::TryInto; + +use zellij_tile::prelude::*; + +use crate::line::tab_line; +use crate::tab::tab_style; + +#[derive(Debug, Default)] +pub struct LinePart { + part: String, + len: usize, +} + +#[derive(Default)] +struct State { + tabs: Vec, + active_tab_idx: usize, + mode_info: ModeInfo, + mouse_click_pos: usize, + should_render: bool, +} + +static ARROW_SEPARATOR: &str = ""; + +register_plugin!(State); + +impl ZellijPlugin for State { + fn load(&mut self) { + set_selectable(false); + subscribe(&[ + EventType::TabUpdate, + EventType::ModeUpdate, + EventType::Mouse, + ]); + } + + fn update(&mut self, event: Event) { + match event { + Event::ModeUpdate(mode_info) => self.mode_info = mode_info, + Event::TabUpdate(tabs) => { + if let Some(active_tab_index) = tabs.iter().position(|t| t.active) { + // tabs are indexed starting from 1 so we need to add 1 + self.active_tab_idx = active_tab_index + 1; + self.tabs = tabs; + } else { + eprintln!("Could not find active tab."); + } + } + Event::Mouse(me) => match me { + Mouse::LeftClick(_, col) => { + self.mouse_click_pos = col; + self.should_render = true; + } + Mouse::ScrollUp(_) => { + switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32); + } + Mouse::ScrollDown(_) => { + switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32); + } + _ => {} + }, + _ => { + eprintln!("Got unrecognized event: {:?}", event); + } + } + } + + fn render(&mut self, _rows: usize, cols: usize) { + if self.tabs.is_empty() { + return; + } + let mut all_tabs: Vec = vec![]; + let mut active_tab_index = 0; + for t in &mut self.tabs { + let mut tabname = t.name.clone(); + if t.active && self.mode_info.mode == InputMode::RenameTab { + if tabname.is_empty() { + tabname = String::from("Enter name..."); + } + active_tab_index = t.position; + } else if t.active { + active_tab_index = t.position; + } + let tab = tab_style( + tabname, + t.active, + t.is_sync_panes_active, + self.mode_info.style.colors, + self.mode_info.capabilities, + t.other_focused_clients.as_slice(), + ); + all_tabs.push(tab); + } + let tab_line = tab_line( + self.mode_info.session_name.as_deref(), + all_tabs, + active_tab_index, + cols.saturating_sub(1), + self.mode_info.style.colors, + self.mode_info.capabilities, + self.mode_info.mode, + ); + let mut s = String::new(); + let mut len_cnt = 0; + for (idx, bar_part) in tab_line.iter().enumerate() { + s = format!("{}{}", s, &bar_part.part); + + if self.should_render + && self.mouse_click_pos > len_cnt + && self.mouse_click_pos <= len_cnt + bar_part.len + && idx > 2 + { + // First three elements of tab_line are "Zellij", session name and empty thing, hence the idx > 2 condition. + // Tabs are indexed starting from 1, therefore we need subtract 2 below. + switch_tab_to(TryInto::::try_into(idx).unwrap() - 2); + } + len_cnt += bar_part.len; + } + let background = match self.mode_info.style.colors.theme_hue { + ThemeHue::Dark => self.mode_info.style.colors.black, + ThemeHue::Light => self.mode_info.style.colors.white, + }; + match background { + PaletteColor::Rgb((r, g, b)) => { + println!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", s, r, g, b); + } + PaletteColor::EightBit(color) => { + println!("{}\u{1b}[48;5;{}m\u{1b}[0K", s, color); + } + } + self.should_render = false; + } +} diff --git a/default-plugins/compact-bar/src/tab.rs b/default-plugins/compact-bar/src/tab.rs new file mode 100644 index 000000000..993489620 --- /dev/null +++ b/default-plugins/compact-bar/src/tab.rs @@ -0,0 +1,84 @@ +use crate::{line::tab_separator, LinePart}; +use ansi_term::{ANSIString, ANSIStrings}; +use unicode_width::UnicodeWidthStr; +use zellij_tile::prelude::*; +use zellij_tile_utils::style; + +fn cursors(focused_clients: &[ClientId], palette: Palette) -> (Vec, usize) { + // cursor section, text length + let mut len = 0; + let mut cursors = vec![]; + for client_id in focused_clients.iter() { + if let Some(color) = client_id_to_colors(*client_id, palette) { + cursors.push(style!(color.1, color.0).paint(" ")); + len += 1; + } + } + (cursors, len) +} + +pub fn render_tab( + text: String, + palette: Palette, + separator: &str, + focused_clients: &[ClientId], + active: bool, +) -> LinePart { + let background_color = if active { palette.green } else { palette.fg }; + let foreground_color = match palette.theme_hue { + ThemeHue::Dark => palette.black, + ThemeHue::Light => palette.white, + }; + let left_separator = style!(foreground_color, background_color).paint(separator); + let mut tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding + + let tab_styled_text = style!(foreground_color, background_color) + .bold() + .paint(format!(" {} ", text)); + + let right_separator = style!(background_color, foreground_color).paint(separator); + let tab_styled_text = if !focused_clients.is_empty() { + let (cursor_section, extra_length) = cursors(focused_clients, palette); + tab_text_len += extra_length; + let mut s = String::new(); + let cursor_beginning = style!(foreground_color, background_color) + .bold() + .paint("[") + .to_string(); + let cursor_section = ANSIStrings(&cursor_section).to_string(); + let cursor_end = style!(foreground_color, background_color) + .bold() + .paint("]") + .to_string(); + s.push_str(&left_separator.to_string()); + s.push_str(&tab_styled_text.to_string()); + s.push_str(&cursor_beginning); + s.push_str(&cursor_section); + s.push_str(&cursor_end); + s.push_str(&right_separator.to_string()); + s + } else { + ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string() + }; + + LinePart { + part: tab_styled_text, + len: tab_text_len, + } +} + +pub fn tab_style( + text: String, + is_active_tab: bool, + is_sync_panes_active: bool, + palette: Palette, + capabilities: PluginCapabilities, + focused_clients: &[ClientId], +) -> LinePart { + let separator = tab_separator(capabilities); + let mut tab_text = text; + if is_sync_panes_active { + tab_text.push_str(" (Sync)"); + } + render_tab(tab_text, palette, separator, focused_clients, is_active_tab) +} -- cgit v1.2.3