diff --git a/Cargo.lock b/Cargo.lock index 2210d2bd81..e454c20c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2924,6 +2924,7 @@ dependencies = [ "url", "wasmer", "wasmer-wasi", + "zellij-tile", "zellij-utils", ] diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index f1b2197e49..1ecd87ab14 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -76,61 +76,55 @@ pub struct ColoredElements { // that can be defined in the config perhaps fn color_elements(palette: Palette) -> ColoredElements { match palette.source { - // "cyan" here is used as a background as a dirty hack - // this is because the Palette struct doesn't have a "gray" section - // and we can't use its "bg" because that is now dynamically taken from the terminal - // and might often not actually fit the rest of the colorscheme - // - // to fix this, we need to restructure the Palette struct PaletteSource::Default => ColoredElements { - selected_prefix_separator: style!(palette.cyan, palette.green), + selected_prefix_separator: style!(palette.gray, palette.green), selected_char_left_separator: style!(palette.black, palette.green).bold(), selected_char_shortcut: style!(palette.red, palette.green).bold(), selected_char_right_separator: style!(palette.black, palette.green).bold(), selected_styled_text: style!(palette.black, palette.green).bold(), - selected_suffix_separator: style!(palette.green, palette.cyan).bold(), - unselected_prefix_separator: style!(palette.cyan, palette.fg), + selected_suffix_separator: style!(palette.green, palette.gray).bold(), + unselected_prefix_separator: style!(palette.gray, palette.fg), unselected_char_left_separator: style!(palette.black, palette.fg).bold(), unselected_char_shortcut: style!(palette.red, palette.fg).bold(), unselected_char_right_separator: style!(palette.black, palette.fg).bold(), unselected_styled_text: style!(palette.black, palette.fg).bold(), - unselected_suffix_separator: style!(palette.fg, palette.cyan), - disabled_prefix_separator: style!(palette.cyan, palette.fg), - disabled_styled_text: style!(palette.cyan, palette.fg).dimmed(), - disabled_suffix_separator: style!(palette.fg, palette.cyan), - selected_single_letter_prefix_separator: style!(palette.cyan, palette.green), + unselected_suffix_separator: style!(palette.fg, palette.gray), + disabled_prefix_separator: style!(palette.gray, palette.fg), + disabled_styled_text: style!(palette.gray, palette.fg).dimmed(), + disabled_suffix_separator: style!(palette.fg, palette.gray), + selected_single_letter_prefix_separator: style!(palette.gray, palette.green), selected_single_letter_char_shortcut: style!(palette.red, palette.green).bold(), - selected_single_letter_suffix_separator: style!(palette.green, palette.cyan), - unselected_single_letter_prefix_separator: style!(palette.cyan, palette.fg), + selected_single_letter_suffix_separator: style!(palette.green, palette.gray), + unselected_single_letter_prefix_separator: style!(palette.gray, palette.fg), unselected_single_letter_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_single_letter_suffix_separator: style!(palette.fg, palette.cyan), - superkey_prefix: style!(palette.white, palette.cyan).bold(), - superkey_suffix_separator: style!(palette.cyan, palette.cyan), + unselected_single_letter_suffix_separator: style!(palette.fg, palette.gray), + superkey_prefix: style!(palette.white, palette.gray).bold(), + superkey_suffix_separator: style!(palette.gray, palette.gray), }, PaletteSource::Xresources => ColoredElements { - selected_prefix_separator: style!(palette.cyan, palette.green), + selected_prefix_separator: style!(palette.gray, palette.green), selected_char_left_separator: style!(palette.fg, palette.green).bold(), selected_char_shortcut: style!(palette.red, palette.green).bold(), selected_char_right_separator: style!(palette.fg, palette.green).bold(), - selected_styled_text: style!(palette.cyan, palette.green).bold(), - selected_suffix_separator: style!(palette.green, palette.cyan).bold(), - unselected_prefix_separator: style!(palette.cyan, palette.fg), - unselected_char_left_separator: style!(palette.cyan, palette.fg).bold(), + selected_styled_text: style!(palette.gray, palette.green).bold(), + selected_suffix_separator: style!(palette.green, palette.gray).bold(), + unselected_prefix_separator: style!(palette.gray, palette.fg), + unselected_char_left_separator: style!(palette.gray, palette.fg).bold(), unselected_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_char_right_separator: style!(palette.cyan, palette.fg).bold(), - unselected_styled_text: style!(palette.cyan, palette.fg).bold(), - unselected_suffix_separator: style!(palette.fg, palette.cyan), - disabled_prefix_separator: style!(palette.cyan, palette.fg), - disabled_styled_text: style!(palette.cyan, palette.fg).dimmed(), - disabled_suffix_separator: style!(palette.fg, palette.cyan), + unselected_char_right_separator: style!(palette.gray, palette.fg).bold(), + unselected_styled_text: style!(palette.gray, palette.fg).bold(), + unselected_suffix_separator: style!(palette.fg, palette.gray), + disabled_prefix_separator: style!(palette.gray, palette.fg), + disabled_styled_text: style!(palette.gray, palette.fg).dimmed(), + disabled_suffix_separator: style!(palette.fg, palette.gray), selected_single_letter_prefix_separator: style!(palette.fg, palette.green), selected_single_letter_char_shortcut: style!(palette.red, palette.green).bold(), selected_single_letter_suffix_separator: style!(palette.green, palette.fg), - unselected_single_letter_prefix_separator: style!(palette.fg, palette.cyan), + unselected_single_letter_prefix_separator: style!(palette.fg, palette.gray), unselected_single_letter_char_shortcut: style!(palette.red, palette.fg).bold(), - unselected_single_letter_suffix_separator: style!(palette.fg, palette.cyan), - superkey_prefix: style!(palette.cyan, palette.fg).bold(), - superkey_suffix_separator: style!(palette.fg, palette.cyan), + unselected_single_letter_suffix_separator: style!(palette.fg, palette.gray), + superkey_prefix: style!(palette.gray, palette.fg).bold(), + superkey_suffix_separator: style!(palette.fg, palette.gray), }, } } @@ -231,7 +225,7 @@ impl ZellijPlugin for State { // [48;5;238m is gray background, [0K is so that it fills the rest of the line // [m is background reset, [0K is so that it clears the rest of the line - match self.mode_info.palette.cyan { + match self.mode_info.palette.gray { PaletteColor::Rgb((r, g, b)) => { println!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b); } diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs index af78b0fcfb..2e180845b5 100644 --- a/default-plugins/tab-bar/src/line.rs +++ b/default-plugins/tab-bar/src/line.rs @@ -102,11 +102,11 @@ fn left_more_message(tab_count_to_the_left: usize, palette: Palette, separator: // 238 // chars length plus separator length on both sides let more_text_len = more_text.width() + 2 * separator.width(); - let left_separator = style!(palette.cyan, palette.orange).paint(separator); + let left_separator = style!(palette.gray, palette.orange).paint(separator); let more_styled_text = style!(palette.black, palette.orange) .bold() .paint(more_text); - let right_separator = style!(palette.orange, palette.cyan).paint(separator); + let right_separator = style!(palette.orange, palette.gray).paint(separator); let more_styled_text = format!( "{}", ANSIStrings(&[left_separator, more_styled_text, right_separator,]) @@ -132,11 +132,11 @@ fn right_more_message( }; // chars length plus separator length on both sides let more_text_len = more_text.width() + 2 * separator.width(); - let left_separator = style!(palette.cyan, palette.orange).paint(separator); + let left_separator = style!(palette.gray, palette.orange).paint(separator); let more_styled_text = style!(palette.black, palette.orange) .bold() .paint(more_text); - let right_separator = style!(palette.orange, palette.cyan).paint(separator); + let right_separator = style!(palette.orange, palette.gray).paint(separator); let more_styled_text = format!( "{}", ANSIStrings(&[left_separator, more_styled_text, right_separator,]) @@ -151,7 +151,7 @@ fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) -> let prefix_text = " Zellij ".to_string(); let prefix_text_len = prefix_text.chars().count(); - let prefix_styled_text = style!(palette.white, palette.cyan) + let prefix_styled_text = style!(palette.white, palette.gray) .bold() .paint(prefix_text); let mut parts = vec![LinePart { @@ -161,7 +161,7 @@ fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) -> if let Some(name) = session_name { let name_part = format!("({}) ", name); let name_part_len = name_part.width(); - let name_part_styled_text = style!(palette.white, palette.cyan).bold().paint(name_part); + let name_part_styled_text = style!(palette.white, palette.gray).bold().paint(name_part); if cols.saturating_sub(prefix_text_len) >= name_part_len { parts.push(LinePart { part: format!("{}", name_part_styled_text), diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 936c18e480..5902050aea 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -112,7 +112,7 @@ impl ZellijPlugin for State { } len_cnt += bar_part.len; } - match self.mode_info.palette.cyan { + match self.mode_info.palette.gray { PaletteColor::Rgb((r, g, b)) => { println!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", s, r, g, b); } diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index fdb498553e..fc34b6c36c 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -5,12 +5,12 @@ use zellij_tile::prelude::*; use zellij_tile_utils::style; pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { - let left_separator = style!(palette.cyan, palette.green).paint(separator); + let left_separator = style!(palette.gray, palette.green).paint(separator); let 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!(palette.black, palette.green) .bold() .paint(format!(" {} ", text)); - let right_separator = style!(palette.green, palette.cyan).paint(separator); + let right_separator = style!(palette.green, palette.gray).paint(separator); let tab_styled_text = format!( "{}", ANSIStrings(&[left_separator, tab_styled_text, right_separator,]) @@ -22,12 +22,12 @@ pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { } pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart { - let left_separator = style!(palette.cyan, palette.fg).paint(separator); + let left_separator = style!(palette.gray, palette.fg).paint(separator); let 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!(palette.black, palette.fg) .bold() .paint(format!(" {} ", text)); - let right_separator = style!(palette.fg, palette.cyan).paint(separator); + let right_separator = style!(palette.fg, palette.gray).paint(separator); let tab_styled_text = format!( "{}", ANSIStrings(&[left_separator, tab_styled_text, right_separator,]) diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index f38739f130..1855daea16 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -22,6 +22,7 @@ wasmer = "1.0.0" wasmer-wasi = "1.0.0" cassowary = "0.3.0" zellij-utils = { path = "../zellij-utils/", version = "0.21.0" } +zellij-tile = { path = "../zellij-tile/", version = "0.21.0" } log = "0.4.14" typetag = "0.1.7" chrono = "0.4.19" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 4d94607f41..cb8f050dca 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -11,7 +11,7 @@ mod ui; mod wasm_vm; use log::info; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::{ path::PathBuf, sync::{Arc, Mutex, RwLock}, @@ -134,9 +134,15 @@ impl SessionState { } } pub fn new_client(&mut self) -> ClientId { - let mut clients: Vec = self.clients.keys().copied().collect(); - clients.sort_unstable(); - let next_client_id = clients.last().unwrap_or(&0) + 1; + let clients: HashSet = self.clients.keys().copied().collect(); + let mut next_client_id = 1; + loop { + if clients.contains(&next_client_id) { + next_client_id += 1; + } else { + break; + } + } self.clients.insert(next_client_id, None); next_client_id } diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index e3752fef12..706e0e5499 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -448,6 +448,9 @@ impl Grid { pub fn render_full_viewport(&mut self) { self.output_buffer.update_all_lines(); } + pub fn update_line_for_rendering(&mut self, line_index: usize) { + self.output_buffer.update_line(line_index); + } pub fn advance_to_next_tabstop(&mut self, styles: CharacterStyles) { let mut next_tabstop = None; for tabstop in self.horizontal_tabstops.iter() { @@ -1057,6 +1060,16 @@ impl Grid { self.add_character_at_cursor_position(terminal_character); self.move_cursor_forward_until_edge(character_width); } + pub fn get_character_under_cursor(&self) -> Option { + let absolute_x_in_line = self.get_absolute_character_index(self.cursor.x, self.cursor.y); + self.viewport + .get(self.cursor.y) + .and_then(|current_line| current_line.columns.get(absolute_x_in_line)) + .copied() + } + pub fn get_absolute_character_index(&self, x: usize, y: usize) -> usize { + self.viewport.get(y).unwrap().absolute_character_index(x) + } pub fn move_cursor_forward_until_edge(&mut self, count: usize) { let count_to_move = std::cmp::min(count, self.width - (self.cursor.x)); self.cursor.x += count_to_move; diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 6b1cb41685..36274b3478 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -5,8 +5,9 @@ use std::unimplemented; use crate::panes::PaneId; use crate::pty::VteBytes; use crate::tab::Pane; -use crate::ui::pane_boundaries_frame::PaneFrame; +use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame}; use crate::wasm_vm::PluginInstruction; +use crate::ClientId; use zellij_utils::pane_size::Offset; use zellij_utils::position::Position; use zellij_utils::shared::ansi_len; @@ -27,7 +28,6 @@ pub(crate) struct PluginPane { pub active_at: Instant, pub pane_title: String, frame: bool, - frame_color: Option, borderless: bool, } @@ -47,7 +47,6 @@ impl PluginPane { send_plugin_instructions, active_at: Instant::now(), frame: false, - frame_color: None, content_offset: Offset::default(), pane_title: title, borderless: false, @@ -152,17 +151,6 @@ impl Pane for PluginPane { self.should_render = false; let contents = buf_rx.recv().unwrap(); - // FIXME: This is a hack that assumes all fixed-size panes are borderless. This - // will eventually need fixing! - if self.frame && !(self.geom.rows.is_fixed() || self.geom.cols.is_fixed()) { - let frame = PaneFrame { - geom: self.current_geom().into(), - title: self.pane_title.clone(), - color: self.frame_color, - ..Default::default() - }; - vte_output.push_str(&frame.render()); - } for (index, line) in contents.lines().enumerate() { let actual_len = ansi_len(line); let line_to_print = if actual_len > self.get_content_columns() { @@ -212,6 +200,28 @@ impl Pane for PluginPane { None } } + fn render_frame(&mut self, _client_id: ClientId, frame_params: FrameParams) -> Option { + // FIXME: This is a hack that assumes all fixed-size panes are borderless. This + // will eventually need fixing! + if self.frame && !(self.geom.rows.is_fixed() || self.geom.cols.is_fixed()) { + let frame = PaneFrame::new( + self.current_geom().into(), + (0, 0), // scroll position + self.pane_title.clone(), + frame_params, + ); + Some(frame.render()) + } else { + None + } + } + fn render_fake_cursor( + &mut self, + _cursor_color: PaletteColor, + _text_color: PaletteColor, + ) -> Option { + None + } fn pid(&self) -> PaneId { PaneId::Plugin(self.pid) } @@ -317,10 +327,6 @@ impl Pane for PluginPane { fn set_content_offset(&mut self, offset: Offset) { self.content_offset = offset; } - fn set_boundary_color(&mut self, color: Option) { - self.frame_color = color; - self.set_should_render(true); - } fn set_borderless(&mut self, borderless: bool) { self.borderless = borderless; } diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index c9de376035..e6a9d85cfa 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -1,9 +1,11 @@ +use std::convert::From; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; use zellij_utils::vte::ParamsIter; use crate::panes::alacritty_functions::parse_sgr_color; +use zellij_tile::data::PaletteColor; pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { character: ' ', @@ -35,6 +37,15 @@ pub enum AnsiCode { ColorIndex(u8), } +impl From for AnsiCode { + fn from(palette_color: PaletteColor) -> Self { + match palette_color { + PaletteColor::Rgb((r, g, b)) => AnsiCode::RgbCode((r, g, b)), + PaletteColor::EightBit(index) => AnsiCode::ColorIndex(index), + } + } +} + #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum NamedColor { Black, diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index f2974cee9d..0a6942e956 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -7,6 +7,8 @@ use crate::panes::{ }; use crate::pty::VteBytes; use crate::tab::Pane; +use crate::ClientId; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::os::unix::io::RawFd; use std::time::{self, Instant}; @@ -20,7 +22,7 @@ use zellij_utils::{ pub const SELECTION_SCROLL_INTERVAL_MS: u64 = 10; -use crate::ui::pane_boundaries_frame::PaneFrame; +use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame}; #[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)] pub enum PaneId { @@ -42,9 +44,9 @@ pub struct TerminalPane { selection_scrolled_at: time::Instant, content_offset: Offset, pane_title: String, - frame: Option, - frame_color: Option, + frame: HashMap, borderless: bool, + fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render } impl Pane for TerminalPane { @@ -172,9 +174,7 @@ impl Pane for TerminalPane { fn render_full_viewport(&mut self) { // this marks the pane for a full re-render, rather than just rendering the // diff as it usually does with the OutputBuffer - if self.frame.is_some() { - self.frame.replace(PaneFrame::default()); - } + self.frame.clear(); self.grid.render_full_viewport(); } fn selectable(&self) -> bool { @@ -187,14 +187,14 @@ impl Pane for TerminalPane { if self.should_render() { let mut vte_output = String::new(); let mut character_styles = CharacterStyles::new(); + let content_x = self.get_content_x(); + let content_y = self.get_content_y(); if self.grid.clear_viewport_before_rendering { for line_index in 0..self.grid.height { - let x = self.get_content_x(); - let y = self.get_content_y(); vte_output.push_str(&format!( "\u{1b}[{};{}H\u{1b}[m", - y + line_index + 1, - x + 1 + content_y + line_index + 1, + content_x + 1 )); // goto row/col and reset styles for _col_index in 0..self.grid.width { vte_output.push(EMPTY_TERMINAL_CHARACTER.character); @@ -202,6 +202,19 @@ impl Pane for TerminalPane { } self.grid.clear_viewport_before_rendering = false; } + // here we clear the previous cursor locations by adding an empty style-less character + // in their location, this is done before the main rendering logic so that if there + // actually is another character there, it will be overwritten + for (y, x) in self.fake_cursor_locations.drain() { + // we need to make sure to update the line in the line buffer so that if there's + // another character there it'll override it and we won't create holes with our + // empty character + self.grid.update_line_for_rendering(y); + let x = content_x + x; + let y = content_y + y; + vte_output.push_str(&format!("\u{1b}[{};{}H\u{1b}[m", y + 1, x + 1)); + vte_output.push(EMPTY_TERMINAL_CHARACTER.character); + } let max_width = self.get_content_columns(); for character_chunk in self.grid.read_changes() { let pane_x = self.get_content_x(); @@ -244,29 +257,69 @@ impl Pane for TerminalPane { } character_styles.clear(); } - if let Some(last_frame) = &self.frame { - let frame = PaneFrame { - geom: self.current_geom().into(), - title: self - .grid - .title - .clone() - .unwrap_or_else(|| self.pane_title.clone()), - scroll_position: self.grid.scrollback_position_and_length(), - color: self.frame_color, - }; + self.set_should_render(false); + Some(vte_output) + } else { + None + } + } + fn render_frame(&mut self, client_id: ClientId, frame_params: FrameParams) -> Option { + // TODO: remove the cursor stuff from here + let mut vte_output = None; + let frame = PaneFrame::new( + self.current_geom().into(), + self.grid.scrollback_position_and_length(), + self.grid + .title + .clone() + .unwrap_or_else(|| self.pane_title.clone()), + frame_params, + ); + match self.frame.get(&client_id) { + // TODO: use and_then or something? + Some(last_frame) => { if &frame != last_frame { if !self.borderless { - vte_output.push_str(&frame.render()); + vte_output = Some(frame.render()); } - self.frame = Some(frame); + self.frame.insert(client_id, frame); } + vte_output } - self.set_should_render(false); - Some(vte_output) - } else { - None + None => { + if !self.borderless { + vte_output = Some(frame.render()); + } + self.frame.insert(client_id, frame); + vte_output + } + } + } + fn render_fake_cursor( + &mut self, + cursor_color: PaletteColor, + text_color: PaletteColor, + ) -> Option { + let mut vte_output = None; + if let Some((cursor_x, cursor_y)) = self.cursor_coordinates() { + let mut character_under_cursor = self + .grid + .get_character_under_cursor() + .unwrap_or(EMPTY_TERMINAL_CHARACTER); + character_under_cursor.styles.background = Some(cursor_color.into()); + character_under_cursor.styles.foreground = Some(text_color.into()); + // we keep track of these so that we can clear them up later (see render function) + self.fake_cursor_locations.insert((cursor_y, cursor_x)); + let mut fake_cursor = format!( + "\u{1b}[{};{}H\u{1b}[m{}", // goto row column and clear styles + self.get_content_y() + cursor_y + 1, // + 1 because goto is 1 indexed + self.get_content_x() + cursor_x + 1, + &character_under_cursor.styles, + ); + fake_cursor.push(character_under_cursor.character); + vte_output = Some(fake_cursor); } + vte_output } fn pid(&self) -> PaneId { PaneId::Terminal(self.pid) @@ -384,12 +437,8 @@ impl Pane for TerminalPane { self.grid.get_selected_text() } - fn set_frame(&mut self, frame: bool) { - self.frame = if frame { - Some(PaneFrame::default()) - } else { - None - }; + fn set_frame(&mut self, _frame: bool) { + self.frame.clear(); } fn set_content_offset(&mut self, offset: Offset) { @@ -397,10 +446,6 @@ impl Pane for TerminalPane { self.reflow_lines(); } - fn set_boundary_color(&mut self, color: Option) { - self.frame_color = color; - self.set_should_render(true); - } fn set_borderless(&mut self, borderless: bool) { self.borderless = borderless; } @@ -423,8 +468,7 @@ impl TerminalPane { palette, ); TerminalPane { - frame: None, - frame_color: None, + frame: HashMap::new(), content_offset: Offset::default(), pid, grid, @@ -437,6 +481,7 @@ impl TerminalPane { selection_scrolled_at: time::Instant::now(), pane_title: initial_pane_title, borderless: false, + fake_cursor_locations: HashSet::new(), } } pub fn get_x(&self) -> usize { diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index 4fd18f32c6..bb44d2784d 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -3,13 +3,16 @@ use zellij_utils::{position::Position, serde, zellij_tile}; +use crate::ui::pane_boundaries_frame::FrameParams; use crate::ui::pane_resizer::PaneResizer; + use crate::{ os_input_output::ServerOsApi, panes::{PaneId, PluginPane, TerminalPane}, pty::{PtyInstruction, VteBytes}, thread_bus::ThreadSenders, ui::boundaries::Boundaries, + ui::pane_contents_and_ui::PaneContentsAndUi, wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; @@ -21,7 +24,7 @@ use std::{ cmp::Reverse, collections::{BTreeMap, HashMap, HashSet}, }; -use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PaletteColor}; +use zellij_tile::data::{Event, ModeInfo, Palette, PaletteColor}; use zellij_utils::{ input::{ layout::{Direction, Layout, Run}, @@ -112,6 +115,11 @@ impl Output { render_instruction.push_str(to_push) } } + pub fn push_to_client(&mut self, client_id: ClientId, to_push: &str) { + if let Some(render_instructions) = self.client_render_instructions.get_mut(&client_id) { + render_instructions.push_str(to_push); + } + } } pub(crate) struct Tab { @@ -133,6 +141,7 @@ pub(crate) struct Tab { pub colors: Palette, connected_clients: HashSet, draw_pane_frames: bool, + session_is_mirrored: bool, pending_vte_events: HashMap>, } @@ -171,6 +180,12 @@ pub trait Pane { fn selectable(&self) -> bool; fn set_selectable(&mut self, selectable: bool); fn render(&mut self) -> Option; + fn render_frame(&mut self, client_id: ClientId, frame_params: FrameParams) -> Option; + fn render_fake_cursor( + &mut self, + cursor_color: PaletteColor, + text_color: PaletteColor, + ) -> Option; fn pid(&self) -> PaneId; fn reduce_height(&mut self, percent: f64); fn increase_height(&mut self, percent: f64); @@ -266,7 +281,6 @@ pub trait Pane { fn relative_position(&self, position_on_screen: &Position) -> Position { position_on_screen.relative_to(self.get_content_y(), self.get_content_x()) } - fn set_boundary_color(&mut self, _color: Option) {} fn set_borderless(&mut self, borderless: bool); fn borderless(&self) -> bool; fn handle_right_click(&mut self, _to: &Position) {} @@ -331,6 +345,9 @@ impl Tab { mode_info, colors, draw_pane_frames, + // at the moment this is hard-coded while the feature is being developed + // the only effect this has is to make sure the UI is drawn without additional information about other connected clients + session_is_mirrored: true, pending_vte_events: HashMap::new(), connected_clients, } @@ -478,6 +495,8 @@ impl Tab { } pub fn remove_client(&mut self, client_id: ClientId) { self.connected_clients.remove(&client_id); + self.active_panes.remove(&client_id); + self.set_force_render(); } pub fn drain_connected_clients(&mut self) -> Vec { self.connected_clients.drain().collect() @@ -552,11 +571,15 @@ impl Tab { } } } - if client_id.is_some() { - // right now we administratively change focus of all clients until the - // mirroring/multiplayer situation is sorted out - let connected_clients: Vec = self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if let Some(client_id) = client_id { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, pid); + } + } else { self.active_panes.insert(client_id, pid); } } @@ -588,11 +611,14 @@ impl Tab { active_pane.set_geom(top_winsize); self.panes.insert(pid, Box::new(new_terminal)); - // right now we administratively change focus of all clients until the - // mirroring/multiplayer situation is sorted out - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, pid); + } + } else { self.active_panes.insert(client_id, pid); } @@ -623,11 +649,14 @@ impl Tab { active_pane.set_geom(left_winsize); self.panes.insert(pid, Box::new(new_terminal)); } - - // right now we administratively change focus of all clients until the - // mirroring/multiplayer situation is sorted out - let connected_clients: Vec = self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, pid); + } + } else { self.active_panes.insert(client_id, pid); } @@ -805,6 +834,10 @@ impl Tab { }; active_terminal.get_geom_override(full_screen_geom); } + let active_panes: Vec = self.active_panes.keys().copied().collect(); + for client_id in active_panes { + self.active_panes.insert(client_id, active_pane_id); + } self.set_force_render(); self.resize_whole_tab(self.display_area); self.toggle_fullscreen_is_active(); @@ -872,12 +905,10 @@ impl Tab { resize_pty!(pane, self.os_api); } } - pub fn render(&mut self, output: &mut Output, overlay: Option) { - if self.connected_clients.is_empty() || self.active_panes.is_empty() { - return; - } + fn update_active_panes_in_pty_thread(&self) { + // this is a bit hacky and we should ideally not keep this state in two different places at + // some point for connected_client in self.connected_clients.iter() { - // TODO: move this out of the render function self.senders .send_to_pty(PtyInstruction::UpdateActivePane( self.active_panes.get(connected_client).copied(), @@ -885,8 +916,57 @@ impl Tab { )) .unwrap(); } + } + pub fn render(&mut self, output: &mut Output, overlay: Option) { + if self.connected_clients.is_empty() || self.active_panes.is_empty() { + return; + } + self.update_active_panes_in_pty_thread(); output.add_clients(&self.connected_clients); - let mut boundaries = Boundaries::new(self.viewport); + let mut client_id_to_boundaries: HashMap = HashMap::new(); + self.hide_cursor_and_clear_display_as_needed(output); + // render panes and their frames + for pane in self.panes.values_mut() { + if !self.panes_to_hide.contains(&pane.pid()) { + let mut pane_contents_and_ui = PaneContentsAndUi::new( + pane, + output, + self.colors, + &self.active_panes, + self.mode_info.mode, + ); + pane_contents_and_ui.render_pane_contents_for_all_clients(); + for client_id in self.connected_clients.iter() { + if self.draw_pane_frames { + pane_contents_and_ui + .render_pane_frame(*client_id, self.session_is_mirrored); + } else { + let mut boundaries = client_id_to_boundaries + .entry(*client_id) + .or_insert_with(|| Boundaries::new(self.viewport)); + pane_contents_and_ui.render_pane_boundaries( + *client_id, + &mut boundaries, + self.session_is_mirrored, + ); + } + // this is done for panes that don't have their own cursor (eg. panes of + // another user) + pane_contents_and_ui.render_fake_cursor_if_needed(*client_id); + } + } + } + // render boundaries if needed + for (client_id, boundaries) in client_id_to_boundaries.iter_mut() { + output.push_to_client(*client_id, &boundaries.vte_output()); + } + // FIXME: Once clients can be distinguished + if let Some(overlay_vte) = &overlay { + output.push_str_to_all_clients(overlay_vte); + } + self.render_cursor(output); + } + fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) { let hide_cursor = "\u{1b}[?25l"; output.push_str_to_all_clients(hide_cursor); if self.should_clear_display_before_rendering { @@ -894,76 +974,27 @@ impl Tab { output.push_str_to_all_clients(clear_display); self.should_clear_display_before_rendering = false; } - let first_client_id = self.connected_clients.iter().next().unwrap(); // this is a temporary hack until we fix the ui for multiple clients - for (_kind, pane) in self.panes.iter_mut() { - if !self.panes_to_hide.contains(&pane.pid()) { - match self.active_panes.get(first_client_id).copied().unwrap() == pane.pid() { - true => { - pane.set_active_at(Instant::now()); - match self.mode_info.mode { - InputMode::Normal | InputMode::Locked => { - pane.set_boundary_color(Some(self.colors.green)); - } - _ => { - pane.set_boundary_color(Some(self.colors.orange)); - } - } - if !self.draw_pane_frames { - boundaries.add_rect( - pane.as_ref(), - self.mode_info.mode, - Some(self.colors), - ) - } - } - false => { - pane.set_boundary_color(None); - if !self.draw_pane_frames { - boundaries.add_rect(pane.as_ref(), self.mode_info.mode, None); - } - } - } - - // FIXME: Once clients can be distinguished - if let Some(overlay_vte) = &overlay { - output.push_str_to_all_clients(overlay_vte); - } - - if let Some(vte_output) = pane.render() { - // FIXME: Use Termion for cursor and style clearing? - output.push_str_to_all_clients(&format!( + } + fn render_cursor(&self, output: &mut Output) { + for client_id in self.connected_clients.iter() { + match self.get_active_terminal_cursor_position(*client_id) { + Some((cursor_position_x, cursor_position_y)) => { + let show_cursor = "\u{1b}[?25h"; + let change_cursor_shape = + self.get_active_pane(*client_id).unwrap().cursor_shape_csi(); + let goto_cursor_position = &format!( "\u{1b}[{};{}H\u{1b}[m{}", - pane.y() + 1, - pane.x() + 1, - vte_output - )); + cursor_position_y + 1, + cursor_position_x + 1, + change_cursor_shape + ); // goto row/col + output.push_to_client(*client_id, show_cursor); + output.push_to_client(*client_id, goto_cursor_position); + } + None => { + let hide_cursor = "\u{1b}[?25l"; + output.push_to_client(*client_id, hide_cursor); } - } - } - - if !self.draw_pane_frames { - output.push_str_to_all_clients(&boundaries.vte_output()); - } - - match self.get_active_terminal_cursor_position(*first_client_id) { - Some((cursor_position_x, cursor_position_y)) => { - let show_cursor = "\u{1b}[?25h"; - let change_cursor_shape = self - .get_active_pane(*first_client_id) - .unwrap() - .cursor_shape_csi(); - let goto_cursor_position = &format!( - "\u{1b}[{};{}H\u{1b}[m{}", - cursor_position_y + 1, - cursor_position_x + 1, - change_cursor_shape - ); // goto row/col - output.push_str_to_all_clients(show_cursor); - output.push_str_to_all_clients(goto_cursor_position); - } - None => { - let hide_cursor = "\u{1b}[?25l"; - output.push_str_to_all_clients(hide_cursor); } } } @@ -2396,15 +2427,29 @@ impl Tab { .panes .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); + previously_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + previously_active_pane.render_full_viewport(); + let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); - - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + next_active_pane.render_full_viewport(); + + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, p); + } + } else { self.active_panes.insert(client_id, p); } + return true; } None => Some(active.pid()), @@ -2454,8 +2499,14 @@ impl Tab { .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + next_active_pane.render_full_viewport(); Some(p) } @@ -2466,9 +2517,14 @@ impl Tab { }; match updated_active_pane { Some(updated_active_pane) => { - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, updated_active_pane); + } + } else { self.active_panes.insert(client_id, updated_active_pane); } } @@ -2504,8 +2560,14 @@ impl Tab { .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + next_active_pane.render_full_viewport(); Some(p) } @@ -2516,9 +2578,14 @@ impl Tab { }; match updated_active_pane { Some(updated_active_pane) => { - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, updated_active_pane); + } + } else { self.active_panes.insert(client_id, updated_active_pane); } } @@ -2555,12 +2622,23 @@ impl Tab { .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); - - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + next_active_pane.render_full_viewport(); + + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, p); + } + } else { self.active_panes.insert(client_id, p); } return true; @@ -2572,9 +2650,14 @@ impl Tab { }; match updated_active_pane { Some(updated_active_pane) => { - let connected_clients: Vec = - self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, updated_active_pane); + } + } else { self.active_panes.insert(client_id, updated_active_pane); } } @@ -3179,7 +3262,7 @@ impl Tab { fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option { if self.fullscreen_is_active { - let first_client_id = self.connected_clients.iter().next().unwrap(); // this is a temporary hack until we fix the ui for multiple clients + let first_client_id = self.connected_clients.iter().next().unwrap(); // TODO: instead of doing this, record the pane that is in fullscreen return self.get_active_pane_id(*first_client_id); } if search_selectable { @@ -3208,10 +3291,16 @@ impl Tab { pane.handle_right_click(&relative_position); }; } - fn focus_pane_at(&mut self, point: &Position, _client_id: ClientId) { + fn focus_pane_at(&mut self, point: &Position, client_id: ClientId) { if let Some(clicked_pane) = self.get_pane_id_at(point, true) { - let connected_clients: Vec = self.connected_clients.iter().copied().collect(); - for client_id in connected_clients { + if self.session_is_mirrored { + // move all clients + let connected_clients: Vec = + self.connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.active_panes.insert(client_id, clicked_pane); + } + } else { self.active_panes.insert(client_id, clicked_pane); } } diff --git a/zellij-server/src/ui/boundaries.rs b/zellij-server/src/ui/boundaries.rs index a912664caf..317c2e5dc1 100644 --- a/zellij-server/src/ui/boundaries.rs +++ b/zellij-server/src/ui/boundaries.rs @@ -3,7 +3,7 @@ use zellij_utils::{pane_size::Viewport, zellij_tile}; use crate::tab::Pane; use ansi_term::Colour::{Fixed, RGB}; use std::collections::HashMap; -use zellij_tile::data::{InputMode, Palette, PaletteColor}; +use zellij_tile::data::PaletteColor; use zellij_utils::shared::colors; use std::fmt::{Display, Error, Formatter}; @@ -413,17 +413,10 @@ impl Boundaries { boundary_characters: HashMap::new(), } } - pub fn add_rect(&mut self, rect: &dyn Pane, input_mode: InputMode, palette: Option) { + pub fn add_rect(&mut self, rect: &dyn Pane, color: Option) { if !self.is_fully_inside_screen(rect) { return; } - let color = match palette.is_some() { - true => match input_mode { - InputMode::Normal | InputMode::Locked => Some(palette.unwrap().green), - _ => Some(palette.unwrap().orange), - }, - false => None, - }; if rect.x() > self.viewport.x { // left boundary let boundary_x_coords = rect.x() - 1; diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index 08c3818363..1bf630712d 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod boundaries; pub mod overlay; pub mod pane_boundaries_frame; +pub mod pane_contents_and_ui; pub mod pane_resizer; diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index a283a6b3b3..5ab3815821 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -1,8 +1,9 @@ use crate::ui::boundaries::boundary_type; +use crate::ClientId; use ansi_term::Colour::{Fixed, RGB}; use ansi_term::Style; use zellij_utils::pane_size::Viewport; -use zellij_utils::zellij_tile::prelude::PaletteColor; +use zellij_utils::zellij_tile::prelude::{Palette, PaletteColor}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -20,27 +21,104 @@ fn color_string(character: &str, color: Option) -> String { } } +fn background_color(character: &str, color: Option) -> String { + match color { + Some(PaletteColor::Rgb((r, g, b))) => { + format!("{}", Style::new().on(RGB(r, g, b)).paint(character)) + } + Some(PaletteColor::EightBit(color)) => { + format!("{}", Style::new().on(Fixed(color)).paint(character)) + } + None => String::from(character), + } +} + +// TODO: move elsewhere +pub(crate) fn client_id_to_colors( + client_id: ClientId, + colors: Palette, +) -> Option<(PaletteColor, PaletteColor)> { + // (primary color, secondary color) + match client_id { + 1 => Some((colors.green, colors.black)), + 2 => Some((colors.blue, colors.black)), + 3 => Some((colors.cyan, colors.black)), + 4 => Some((colors.magenta, colors.black)), + 5 => Some((colors.yellow, colors.black)), + _ => None, + } +} + +pub struct FrameParams { + pub focused_client: Option, + pub is_main_client: bool, + pub other_focused_clients: Vec, + pub colors: Palette, + pub color: Option, + pub other_cursors_exist_in_session: bool, +} + #[derive(Default, PartialEq)] pub struct PaneFrame { pub geom: Viewport, pub title: String, pub scroll_position: (usize, usize), // (position, length) + pub colors: Palette, pub color: Option, + pub focused_client: Option, + pub is_main_client: bool, + pub other_cursors_exist_in_session: bool, + pub other_focused_clients: Vec, } impl PaneFrame { - fn render_title_right_side(&self, max_length: usize) -> Option { + pub fn new( + geom: Viewport, + scroll_position: (usize, usize), + main_title: String, + frame_params: FrameParams, + ) -> Self { + PaneFrame { + geom, + title: main_title, + scroll_position, + colors: frame_params.colors, + color: frame_params.color, + focused_client: frame_params.focused_client, + is_main_client: frame_params.is_main_client, + other_focused_clients: frame_params.other_focused_clients, + other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, + } + } + fn client_cursor(&self, client_id: ClientId) -> String { + let color = client_id_to_colors(client_id, self.colors); + background_color(" ", color.map(|c| c.0)) + } + fn render_title_right_side(&self, max_length: usize) -> Option<(String, usize)> { + // string and length because of color if self.scroll_position.0 > 0 || self.scroll_position.1 > 0 { let prefix = " SCROLL: "; let full_indication = format!(" {}/{} ", self.scroll_position.0, self.scroll_position.1); let short_indication = format!(" {} ", self.scroll_position.0); - if prefix.width() + full_indication.width() <= max_length { - Some(format!("{}{}", prefix, full_indication)) - } else if full_indication.width() <= max_length { - Some(full_indication) - } else if short_indication.width() <= max_length { - Some(short_indication) + let full_indication_len = full_indication.chars().count(); + let short_indication_len = short_indication.chars().count(); + let prefix_len = prefix.chars().count(); + if prefix_len + full_indication_len <= max_length { + Some(( + color_string(&format!("{}{}", prefix, full_indication), self.color), + prefix_len + full_indication_len, + )) + } else if full_indication_len <= max_length { + Some(( + color_string(&full_indication, self.color), + full_indication_len, + )) + } else if short_indication_len <= max_length { + Some(( + color_string(&short_indication, self.color), + short_indication_len, + )) } else { None } @@ -48,14 +126,148 @@ impl PaneFrame { None } } - fn render_title_left_side(&self, max_length: usize) -> Option { + fn render_my_focus(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = "MY FOCUS"; + let full_indication = format!( + "{} {} {}", + left_separator, + color_string(full_indication_text, self.color), + right_separator + ); + let full_indication_len = full_indication_text.width() + 4; // 2 for separators 2 for padding + let short_indication_text = "ME"; + let short_indication = format!( + "{} {} {}", + left_separator, + color_string(short_indication_text, self.color), + right_separator + ); + let short_indication_len = short_indication_text.width() + 4; // 2 for separators 2 for padding + if full_indication_len <= max_length { + Some((full_indication, full_indication_len)) + } else if short_indication_len <= max_length { + Some((short_indication, short_indication_len)) + } else { + None + } + } + fn render_my_and_others_focus(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = "MY FOCUS AND:"; + let short_indication_text = "+"; + let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication_len = full_indication_text.width(); + let mut short_indication = color_string(short_indication_text, self.color); + let mut short_indication_len = short_indication_text.width(); + for client_id in &self.other_focused_clients { + let text = format!(" {}", self.client_cursor(*client_id)); + full_indication_len += 2; + full_indication.push_str(&text); + short_indication_len += 2; + short_indication.push_str(&text); + } + if full_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!("{} {} {}", left_separator, full_indication, right_separator), + full_indication_len + 4, + )) + } else if short_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!( + "{} {} {}", + left_separator, short_indication, right_separator + ), + short_indication_len + 4, + )) + } else { + None + } + } + fn render_other_focused_users(&self, max_length: usize) -> Option<(String, usize)> { + let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + let full_indication_text = if self.other_focused_clients.len() == 1 { + "FOCUSED USER:" + } else { + "FOCUSED USERS:" + }; + let middle_indication_text = "U:"; + let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication_len = full_indication_text.width(); + let mut middle_indication = color_string(middle_indication_text, self.color); + let mut middle_indication_len = middle_indication_text.width(); + let mut short_indication = String::from(""); + let mut short_indication_len = 0; + for client_id in &self.other_focused_clients { + let text = format!(" {}", self.client_cursor(*client_id)); + full_indication_len += 2; + full_indication.push_str(&text); + middle_indication_len += 2; + middle_indication.push_str(&text); + short_indication_len += 2; + short_indication.push_str(&text); + } + if full_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!("{} {} {}", left_separator, full_indication, right_separator), + full_indication_len + 4, + )) + } else if middle_indication_len + 4 <= max_length { + // 2 for separators, 2 for padding + Some(( + format!( + "{} {} {}", + left_separator, middle_indication, right_separator + ), + middle_indication_len + 4, + )) + } else if short_indication_len + 3 <= max_length { + // 2 for separators, 1 for padding + Some(( + format!("{}{} {}", left_separator, short_indication, right_separator), + short_indication_len + 3, + )) + } else { + None + } + } + fn render_title_middle(&self, max_length: usize) -> Option<(String, usize)> { + // string and length because of color + if self.is_main_client + && self.other_focused_clients.is_empty() + && !self.other_cursors_exist_in_session + { + None + } else if self.is_main_client + && self.other_focused_clients.is_empty() + && self.other_cursors_exist_in_session + { + self.render_my_focus(max_length) + } else if self.is_main_client && !self.other_focused_clients.is_empty() { + self.render_my_and_others_focus(max_length) + } else if !self.other_focused_clients.is_empty() { + self.render_other_focused_users(max_length) + } else { + None + } + } + fn render_title_left_side(&self, max_length: usize) -> Option<(String, usize)> { let middle_truncated_sign = "[..]"; let middle_truncated_sign_long = "[...]"; let full_text = format!(" {} ", &self.title); if max_length <= 6 || self.title.is_empty() { None } else if full_text.width() <= max_length { - Some(full_text) + Some(( + color_string(&full_text, self.color), + full_text.chars().count(), + )) } else { let length_of_each_half = (max_length - middle_truncated_sign.width()) / 2; @@ -89,53 +301,234 @@ impl PaneFrame { } else { format!("{}{}{}", first_part, middle_truncated_sign, second_part) }; - Some(title_left_side) + Some(( + color_string(&title_left_side, self.color), + title_left_side.chars().count(), + )) } } - fn render_title(&self, vte_output: &mut String) { + fn three_part_title_line( + &self, + left_side: &str, + left_side_len: &usize, + middle: &str, + middle_len: &usize, + right_side: &str, + right_side_len: &usize, + ) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let left_side_start_position = self.geom.x + 1; + let middle_start_position = self.geom.x + (total_title_length / 2) - (middle_len / 2) + 1; + let right_side_start_position = + (self.geom.x + self.geom.cols - 1).saturating_sub(*right_side_len); + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == left_side_start_position { + title_line.push_str(left_side); + col += left_side_len; + continue; + } else if col == middle_start_position { + title_line.push_str(middle); + col += middle_len; + continue; + } else if col == right_side_start_position { + title_line.push_str(right_side); + col += right_side_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn left_and_middle_title_line( + &self, + left_side: &str, + left_side_len: &usize, + middle: &str, + middle_len: &usize, + ) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let left_side_start_position = self.geom.x + 1; + let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == left_side_start_position { + title_line.push_str(left_side); + col += *left_side_len; + continue; + } else if col == middle_start_position { + title_line.push_str(middle); + col += *middle_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn middle_only_title_line(&self, middle: &str, middle_len: &usize) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut title_line = String::new(); + let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; + + let mut col = self.geom.x; + loop { + if col == self.geom.x { + title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + } else if col == self.geom.x + self.geom.cols - 1 { + title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + } else if col == middle_start_position { + title_line.push_str(middle); + col += *middle_len; + continue; + } else { + title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); + // TODO: BETTER + } + if col == self.geom.x + self.geom.cols - 1 { + break; + } + col += 1; + } + title_line + } + fn two_part_title_line( + &self, + left_side: &str, + left_side_len: &usize, + right_side: &str, + right_side_len: &usize, + ) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle = String::new(); + for _ in (left_side_len + right_side_len)..total_title_length { + middle.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}{}", + left_boundary, + left_side, + color_string(&middle, self.color), + color_string(right_side, self.color), + &right_boundary + ) + } + fn left_only_title_line(&self, left_side: &str, left_side_len: &usize) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle_padding = String::new(); + for _ in *left_side_len..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}{}", + left_boundary, + left_side, + color_string(&middle_padding, self.color), + &right_boundary + ) + } + fn empty_title_line(&self) -> String { + let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); + let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let mut middle_padding = String::new(); + for _ in 0..total_title_length { + middle_padding.push_str(boundary_type::HORIZONTAL); + } + format!( + "{}{}{}", + left_boundary, + color_string(&middle_padding, self.color), + right_boundary + ) + } + fn title_line_with_middle(&self, middle: &str, middle_len: &usize) -> String { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + let length_of_each_side = total_title_length.saturating_sub(*middle_len + 2) / 2; + let mut left_side = self.render_title_left_side(length_of_each_side); + let mut right_side = self.render_title_right_side(length_of_each_side); + + match (left_side.as_mut(), right_side.as_mut()) { + (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => self + .three_part_title_line( + left_side, + left_side_len, + middle, + middle_len, + right_side, + right_side_len, + ), + (Some((left_side, left_side_len)), None) => { + self.left_and_middle_title_line(left_side, left_side_len, middle, middle_len) + } + _ => self.middle_only_title_line(middle, middle_len), + } + } + fn title_line_without_middle(&self) -> String { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let left_boundary = boundary_type::TOP_LEFT; - let right_boundary = boundary_type::TOP_RIGHT; let left_side = self.render_title_left_side(total_title_length); - let right_side = left_side.as_ref().and_then(|left_side| { - let space_left = total_title_length.saturating_sub(left_side.width() + 1); // 1 for a middle separator + let right_side = left_side.as_ref().and_then(|(_left_side, left_side_len)| { + let space_left = total_title_length.saturating_sub(*left_side_len + 1); // 1 for a middle separator self.render_title_right_side(space_left) }); - let title_text = match (left_side, right_side) { - (Some(left_side), Some(right_side)) => { - let mut middle = String::new(); - for _ in (left_side.width() + right_side.width())..total_title_length { - middle.push_str(boundary_type::HORIZONTAL); - } - format!( - "{}{}{}{}{}", - left_boundary, left_side, middle, right_side, right_boundary - ) - } - (Some(left_side), None) => { - let mut middle_padding = String::new(); - for _ in left_side.width()..total_title_length { - middle_padding.push_str(boundary_type::HORIZONTAL); - } - format!( - "{}{}{}{}", - left_boundary, left_side, middle_padding, right_boundary - ) + match (left_side, right_side) { + (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => { + self.two_part_title_line(&left_side, &left_side_len, &right_side, &right_side_len) } - _ => { - let mut middle_padding = String::new(); - for _ in 0..total_title_length { - middle_padding.push_str(boundary_type::HORIZONTAL); - } - format!("{}{}{}", left_boundary, middle_padding, right_boundary) + (Some((left_side, left_side_len)), None) => { + self.left_only_title_line(&left_side, &left_side_len) } - }; - vte_output.push_str(&format!( - "\u{1b}[{};{}H\u{1b}[m{}", - self.geom.y + 1, // +1 because goto is 1 indexed - self.geom.x + 1, // +1 because goto is 1 indexed - color_string(&title_text, self.color), - )); // goto row/col + boundary character + _ => self.empty_title_line(), + } + } + fn render_title(&self, vte_output: &mut String) { + let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners + + if let Some((middle, middle_length)) = self.render_title_middle(total_title_length).as_mut() + { + let title_text = self.title_line_with_middle(middle, middle_length); + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.geom.y + 1, // +1 because goto is 1 indexed + self.geom.x + 1, // +1 because goto is 1 indexed + color_string(&title_text, self.color), + )); // goto row/col + boundary character + } else { + let title_text = self.title_line_without_middle(); + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.geom.y + 1, // +1 because goto is 1 indexed + self.geom.x + 1, // +1 because goto is 1 indexed + color_string(&title_text, self.color), + )); // goto row/col + boundary character + } } pub fn render(&self) -> String { let mut vte_output = String::new(); diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs new file mode 100644 index 0000000000..4f700f5dcf --- /dev/null +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -0,0 +1,165 @@ +use crate::panes::PaneId; +use crate::tab::{Output, Pane}; +use crate::ui::boundaries::Boundaries; +use crate::ui::pane_boundaries_frame::client_id_to_colors; +use crate::ui::pane_boundaries_frame::FrameParams; +use crate::ClientId; +use std::collections::HashMap; +use zellij_tile::data::{InputMode, Palette, PaletteColor}; + +pub struct PaneContentsAndUi<'a> { + pane: &'a mut Box, + output: &'a mut Output, + colors: Palette, + focused_clients: Vec, + multiple_users_exist_in_session: bool, + mode: InputMode, // TODO: per client +} + +impl<'a> PaneContentsAndUi<'a> { + pub fn new( + pane: &'a mut Box, + output: &'a mut Output, + colors: Palette, + active_panes: &HashMap, + mode: InputMode, + ) -> Self { + let focused_clients: Vec = active_panes + .iter() + .filter(|(_c_id, p_id)| **p_id == pane.pid()) + .map(|(c_id, _p_id)| *c_id) + .collect(); + let multiple_users_exist_in_session = active_panes.len() > 1; + PaneContentsAndUi { + pane, + output, + colors, + focused_clients, + multiple_users_exist_in_session, + mode, + } + } + pub fn render_pane_contents_for_all_clients(&mut self) { + if let Some(vte_output) = self.pane.render() { + // FIXME: Use Termion for cursor and style clearing? + self.output.push_str_to_all_clients(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + vte_output + )); + } + } + pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) { + let pane_focused_for_client_id = self.focused_clients.contains(&client_id); + let pane_focused_for_different_client = self + .focused_clients + .iter() + .filter(|c_id| **c_id != client_id) + .count() + > 0; + if pane_focused_for_different_client && !pane_focused_for_client_id { + let fake_cursor_client_id = self + .focused_clients + .iter() + .find(|c_id| **c_id != client_id) + .unwrap(); + if let Some(colors) = client_id_to_colors(*fake_cursor_client_id, self.colors) { + if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) { + self.output.push_to_client( + client_id, + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + vte_output + ), + ); + } + } + } + } + pub fn render_pane_frame(&mut self, client_id: ClientId, session_is_mirrored: bool) { + let pane_focused_for_client_id = self.focused_clients.contains(&client_id); + let other_focused_clients: Vec = self + .focused_clients + .iter() + .filter(|c_id| **c_id != client_id) + .copied() + .collect(); + let pane_focused_for_differet_client = !other_focused_clients.is_empty(); + + let frame_color = self.frame_color(client_id, self.mode, session_is_mirrored); + let focused_client = if pane_focused_for_client_id { + Some(client_id) + } else if pane_focused_for_differet_client { + Some(*other_focused_clients.first().unwrap()) + } else { + None + }; + let frame_params = if session_is_mirrored { + FrameParams { + focused_client, + is_main_client: pane_focused_for_client_id, + other_focused_clients: vec![], + colors: self.colors, + color: frame_color, + other_cursors_exist_in_session: false, + } + } else { + FrameParams { + focused_client, + is_main_client: pane_focused_for_client_id, + other_focused_clients, + colors: self.colors, + color: frame_color, + other_cursors_exist_in_session: self.multiple_users_exist_in_session, + } + }; + if let Some(vte_output) = self.pane.render_frame(client_id, frame_params) { + // FIXME: Use Termion for cursor and style clearing? + self.output.push_to_client( + client_id, + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + vte_output + ), + ); + } + } + pub fn render_pane_boundaries( + &self, + client_id: ClientId, + boundaries: &mut Boundaries, + session_is_mirrored: bool, + ) { + let color = self.frame_color(client_id, self.mode, session_is_mirrored); + boundaries.add_rect(self.pane.as_ref(), color); + } + fn frame_color( + &self, + client_id: ClientId, + mode: InputMode, + session_is_mirrored: bool, + ) -> Option { + let pane_focused_for_client_id = self.focused_clients.contains(&client_id); + if pane_focused_for_client_id { + match mode { + InputMode::Normal | InputMode::Locked => { + if session_is_mirrored { + let colors = client_id_to_colors(1, self.colors); // mirrored sessions only have one focused color + colors.map(|colors| colors.0) + } else { + let colors = client_id_to_colors(client_id, self.colors); + colors.map(|colors| colors.0) + } + } + _ => Some(self.colors.orange), + } + } else { + None + } + } +} diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 11b94beff2..e43049a540 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -163,6 +163,7 @@ pub struct Palette { pub cyan: PaletteColor, pub white: PaletteColor, pub orange: PaletteColor, + pub gray: PaletteColor, } /// Represents the contents of the help message that is printed in the status bar, diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs index 37dfc3be2f..008f1ff6be 100644 --- a/zellij-utils/src/shared.rs +++ b/zellij-utils/src/shared.rs @@ -49,6 +49,10 @@ pub mod colors { pub const RED: u8 = 88; pub const ORANGE: u8 = 166; pub const BLACK: u8 = 16; + pub const MAGENTA: u8 = 201; + pub const CYAN: u8 = 51; + pub const YELLOW: u8 = 226; + pub const BLUE: u8 = 45; } pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) { @@ -66,12 +70,13 @@ pub fn default_palette() -> Palette { black: PaletteColor::EightBit(colors::BLACK), red: PaletteColor::EightBit(colors::RED), green: PaletteColor::EightBit(colors::GREEN), - yellow: PaletteColor::EightBit(colors::GRAY), - blue: PaletteColor::EightBit(colors::GRAY), - magenta: PaletteColor::EightBit(colors::GRAY), - cyan: PaletteColor::EightBit(colors::GRAY), + yellow: PaletteColor::EightBit(colors::YELLOW), + blue: PaletteColor::EightBit(colors::BLUE), + magenta: PaletteColor::EightBit(colors::MAGENTA), + cyan: PaletteColor::EightBit(colors::CYAN), white: PaletteColor::EightBit(colors::WHITE), orange: PaletteColor::EightBit(colors::ORANGE), + gray: PaletteColor::EightBit(colors::GRAY), } }