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
This commit is contained in:
a-kenji 2022-06-03 11:14:38 +02:00 committed by GitHub
parent ad9ba8ab24
commit d62e6fb57e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 779 additions and 199 deletions

460
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@ members = [
"zellij-utils",
"zellij-tile",
"zellij-tile-utils",
"default-plugins/compact-bar",
"default-plugins/status-bar",
"default-plugins/strider",
"default-plugins/tab-bar",

View File

@ -109,6 +109,7 @@ end
[tasks.build-plugins-release]
env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = [
"default-plugins/compact-bar",
"default-plugins/status-bar",
"default-plugins/strider",
"default-plugins/tab-bar",
@ -117,6 +118,7 @@ run_task = { name = "build-release", fork = true }
[tasks.build-plugins]
env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = [
"default-plugins/compact-bar",
"default-plugins/status-bar",
"default-plugins/strider",
"default-plugins/tab-bar",

BIN
assets/plugins/compact-bar.wasm Executable file

Binary file not shown.

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-wasi"

View File

@ -0,0 +1,13 @@
[package]
name = "compact-bar"
version = "0.1.0"
authors = ["Alexander Kenji Berthold <aks.kenji@protonmail.com>" ]
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" }

View File

@ -0,0 +1 @@
../../LICENSE.md

View File

@ -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<LinePart>,
tabs_after_active: &mut Vec<LinePart>,
tabs_to_render: &mut Vec<LinePart>,
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<LinePart> {
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<LinePart>,
active_tab_index: usize,
cols: usize,
palette: Palette,
capabilities: PluginCapabilities,
mode: InputMode,
) -> Vec<LinePart> {
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
}

View File

@ -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<TabInfo>,
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<LinePart> = 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::<u32>::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;
}
}

View File

@ -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<ANSIString>, 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)
}

View File

@ -69,6 +69,7 @@ flake-utils.lib.eachSystem [
];
defaultPlugins = [
plugins.compact-bar
plugins.status-bar
plugins.tab-bar
plugins.strider
@ -123,6 +124,7 @@ flake-utils.lib.eachSystem [
cp ${plugins.tab-bar}/bin/tab-bar.wasm assets/plugins/tab-bar.wasm
cp ${plugins.status-bar}/bin/status-bar.wasm assets/plugins/status-bar.wasm
cp ${plugins.strider}/bin/strider.wasm assets/plugins/strider.wasm
cp ${plugins.compact-bar}/bin/compact-bar.wasm assets/plugins/compact-bar.wasm
'';
desktopItems = [

View File

@ -50,4 +50,5 @@ in {
status-bar = makeDefaultPlugin "status-bar";
tab-bar = makeDefaultPlugin "tab-bar";
strider = makeDefaultPlugin "strider";
compact-bar = makeDefaultPlugin "compact-bar";
}

View File

@ -21,6 +21,7 @@ macro_rules! asset_map {
pub(crate) fn populate_data_dir(data_dir: &Path) {
// First run installation of default plugins & layouts
let mut assets = asset_map! {
"assets/plugins/compact-bar.wasm" => "plugins/compact-bar.wasm",
"assets/plugins/status-bar.wasm" => "plugins/status-bar.wasm",
"assets/plugins/tab-bar.wasm" => "plugins/tab-bar.wasm",
"assets/plugins/strider.wasm" => "plugins/strider.wasm",

View File

@ -479,6 +479,8 @@ plugins:
tag: status-bar
- path: strider
tag: strider
- path: compact-bar
tag: compact-bar
# Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP
# eg. when terminal window with an active zellij session is closed

View File

@ -0,0 +1,13 @@
---
template:
direction: Horizontal
parts:
- direction: Vertical
body: true
- direction: Vertical
borderless: true
split_size:
Fixed: 1
run:
plugin:
location: "zellij:compact-bar"

View File

@ -288,6 +288,7 @@ impl LayoutFromYamlIntermediate {
Some("default") => Self::default_from_assets(),
Some("strider") => Self::strider_from_assets(),
Some("disable-status-bar") => Self::disable_status_from_assets(),
Some("compact") => Self::compact_from_assets(),
None | Some(_) => Err(ConfigError::IoPath(
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
path.into(),
@ -314,6 +315,12 @@ impl LayoutFromYamlIntermediate {
serde_yaml::from_str(&String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?)?;
Ok(layout)
}
pub fn compact_from_assets() -> LayoutFromYamlIntermediateResult {
let layout: LayoutFromYamlIntermediate =
serde_yaml::from_str(&String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?)?;
Ok(layout)
}
}
type LayoutFromYamlResult = Result<LayoutFromYaml, ConfigError>;

View File

@ -271,7 +271,7 @@ mod tests {
)?;
let plugins = PluginsConfig::get_plugins_with_default(plugins.try_into()?);
assert_eq!(plugins.iter().count(), 4);
assert_eq!(plugins.iter().count(), 5);
Ok(())
}

View File

@ -109,6 +109,12 @@ pub const NO_STATUS_LAYOUT: &[u8] = include_bytes!(concat!(
"assets/layouts/disable-status-bar.yaml"
));
pub const COMPACT_BAR_LAYOUT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/layouts/compact.yaml"
));
pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
@ -141,6 +147,7 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> {
match layout {
"strider" => dump_asset(STRIDER_LAYOUT),
"default" => dump_asset(DEFAULT_LAYOUT),
"compact" => dump_asset(COMPACT_BAR_LAYOUT),
"disable-status" => dump_asset(NO_STATUS_LAYOUT),
not_found => Err(std::io::Error::new(
std::io::ErrorKind::Other,