This adds a UI for multiple users in panes (behind a feature flag) (#897)

* feat(ui): multiple users in panes

* style(fmt): make rustfmt happy

* style(fmt): make clippy happy
This commit is contained in:
Aram Drevekenin 2021-11-25 16:21:59 +01:00 committed by GitHub
parent 9fb2c7ca16
commit 6c6a4393f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1008 additions and 284 deletions

1
Cargo.lock generated
View file

@ -2924,6 +2924,7 @@ dependencies = [
"url",
"wasmer",
"wasmer-wasi",
"zellij-tile",
"zellij-utils",
]

View file

@ -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);
}

View file

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

View file

@ -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);
}

View file

@ -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,])

View file

@ -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"

View file

@ -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<ClientId> = self.clients.keys().copied().collect();
clients.sort_unstable();
let next_client_id = clients.last().unwrap_or(&0) + 1;
let clients: HashSet<ClientId> = 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
}

View file

@ -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<TerminalCharacter> {
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;

View file

@ -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<PaletteColor>,
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<String> {
// 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<String> {
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<PaletteColor>) {
self.frame_color = color;
self.set_should_render(true);
}
fn set_borderless(&mut self, borderless: bool) {
self.borderless = borderless;
}

View file

@ -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<PaletteColor> 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,

View file

@ -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<PaneFrame>,
frame_color: Option<PaletteColor>,
frame: HashMap<ClientId, PaneFrame>,
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,30 +257,70 @@ 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,
};
if &frame != last_frame {
if !self.borderless {
vte_output.push_str(&frame.render());
}
self.frame = Some(frame);
}
}
self.set_should_render(false);
Some(vte_output)
} else {
None
}
}
fn render_frame(&mut self, client_id: ClientId, frame_params: FrameParams) -> Option<String> {
// 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 = Some(frame.render());
}
self.frame.insert(client_id, frame);
}
vte_output
}
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<String> {
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<PaletteColor>) {
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 {

View file

@ -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<ClientId>,
draw_pane_frames: bool,
session_is_mirrored: bool,
pending_vte_events: HashMap<RawFd, Vec<VteBytes>>,
}
@ -171,6 +180,12 @@ pub trait Pane {
fn selectable(&self) -> bool;
fn set_selectable(&mut self, selectable: bool);
fn render(&mut self) -> Option<String>;
fn render_frame(&mut self, client_id: ClientId, frame_params: FrameParams) -> Option<String>;
fn render_fake_cursor(
&mut self,
cursor_color: PaletteColor,
text_color: PaletteColor,
) -> Option<String>;
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<PaletteColor>) {}
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<ClientId> {
self.connected_clients.drain().collect()
@ -552,13 +571,17 @@ 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<ClientId> = self.connected_clients.iter().copied().collect();
if let Some(client_id) = client_id {
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
}
}
pub fn horizontal_split(&mut self, pid: PaneId, client_id: ClientId) {
@ -588,13 +611,16 @@ 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
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
self.relayout_tab(Direction::Vertical);
}
@ -623,13 +649,16 @@ 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<ClientId> = self.connected_clients.iter().copied().collect();
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
self.relayout_tab(Direction::Horizontal);
}
@ -805,6 +834,10 @@ impl Tab {
};
active_terminal.get_geom_override(full_screen_geom);
}
let active_panes: Vec<ClientId> = 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<String>) {
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<String>) {
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<ClientId, Boundaries> = 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!(
"\u{1b}[{};{}H\u{1b}[m{}",
pane.y() + 1,
pane.x() + 1,
vte_output
));
}
}
}
if !self.draw_pane_frames {
output.push_str_to_all_clients(&boundaries.vte_output());
}
match self.get_active_terminal_cursor_position(*first_client_id) {
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(*first_client_id)
.unwrap()
.cursor_shape_csi();
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{}",
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);
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_str_to_all_clients(hide_cursor);
output.push_to_client(*client_id, 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);
// 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<ClientId> =
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,11 +2517,16 @@ impl Tab {
};
match updated_active_pane {
Some(updated_active_pane) => {
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
}
None => {
// TODO: can this happen?
@ -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,11 +2578,16 @@ impl Tab {
};
match updated_active_pane {
Some(updated_active_pane) => {
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
}
None => {
// TODO: can this happen?
@ -2555,14 +2622,25 @@ 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();
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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()),
@ -2572,11 +2650,16 @@ impl Tab {
};
match updated_active_pane {
Some(updated_active_pane) => {
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
}
None => {
// TODO: can this happen?
@ -3179,7 +3262,7 @@ impl Tab {
fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option<PaneId> {
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,12 +3291,18 @@ 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<ClientId> = self.connected_clients.iter().copied().collect();
if self.session_is_mirrored {
// move all clients
let connected_clients: Vec<ClientId> =
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);
}
}
}
pub fn handle_mouse_release(&mut self, position: &Position, client_id: ClientId) {

View file

@ -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<Palette>) {
pub fn add_rect(&mut self, rect: &dyn Pane, color: Option<PaletteColor>) {
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;

View file

@ -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;

View file

@ -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<PaletteColor>) -> String {
}
}
fn background_color(character: &str, color: Option<PaletteColor>) -> 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<ClientId>,
pub is_main_client: bool,
pub other_focused_clients: Vec<ClientId>,
pub colors: Palette,
pub color: Option<PaletteColor>,
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<PaletteColor>,
pub focused_client: Option<ClientId>,
pub is_main_client: bool,
pub other_cursors_exist_in_session: bool,
pub other_focused_clients: Vec<ClientId>,
}
impl PaneFrame {
fn render_title_right_side(&self, max_length: usize) -> Option<String> {
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<String> {
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 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
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 {
for _ in (left_side_len + right_side_len)..total_title_length {
middle.push_str(boundary_type::HORIZONTAL);
}
format!(
"{}{}{}{}{}",
left_boundary, left_side, middle, right_side, right_boundary
left_boundary,
left_side,
color_string(&middle, self.color),
color_string(right_side, self.color),
&right_boundary
)
}
(Some(left_side), None) => {
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.width()..total_title_length {
for _ in *left_side_len..total_title_length {
middle_padding.push_str(boundary_type::HORIZONTAL);
}
format!(
"{}{}{}{}",
left_boundary, left_side, middle_padding, right_boundary
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, middle_padding, right_boundary)
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_side = self.render_title_left_side(total_title_length);
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)
});
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)
}
(Some((left_side, left_side_len)), None) => {
self.left_only_title_line(&left_side, &left_side_len)
}
_ => 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();

View file

@ -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<dyn Pane>,
output: &'a mut Output,
colors: Palette,
focused_clients: Vec<ClientId>,
multiple_users_exist_in_session: bool,
mode: InputMode, // TODO: per client
}
impl<'a> PaneContentsAndUi<'a> {
pub fn new(
pane: &'a mut Box<dyn Pane>,
output: &'a mut Output,
colors: Palette,
active_panes: &HashMap<ClientId, PaneId>,
mode: InputMode,
) -> Self {
let focused_clients: Vec<ClientId> = 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<ClientId> = 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<PaletteColor> {
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
}
}
}

View file

@ -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,

View file

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