diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm old mode 100644 new mode 100755 index b45a3247..fdff24cf Binary files a/assets/plugins/status-bar.wasm and b/assets/plugins/status-bar.wasm differ diff --git a/assets/plugins/strider.wasm b/assets/plugins/strider.wasm old mode 100644 new mode 100755 index 3b62b85a..379a3247 Binary files a/assets/plugins/strider.wasm and b/assets/plugins/strider.wasm differ diff --git a/assets/plugins/tab-bar.wasm b/assets/plugins/tab-bar.wasm old mode 100644 new mode 100755 index 4f8107df..ebcfe2ca Binary files a/assets/plugins/tab-bar.wasm and b/assets/plugins/tab-bar.wasm differ diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 31b2f2d3..3db53138 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -85,6 +85,7 @@ impl ZellijPlugin for State { t.is_sync_panes_active, self.mode_info.palette, self.mode_info.capabilities, + t.other_focused_clients.as_slice(), ); all_tabs.push(tab); } diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index db960fd5..4ed3b207 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -1,33 +1,62 @@ use crate::{line::tab_separator, LinePart}; -use ansi_term::ANSIStrings; +use ansi_term::{ANSIString, ANSIStrings}; use unicode_width::UnicodeWidthStr; 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.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.gray).paint(separator); - let tab_styled_text = - ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string(); - LinePart { - part: tab_styled_text, - len: tab_text_len, +fn cursors(focused_clients: &[ClientId], palette: Palette) -> (Vec, usize) { + // cursor section, text length + let mut len = 0; + let mut cursors = vec![]; + for client_id in focused_clients.iter() { + if let Some(color) = client_id_to_colors(*client_id, palette) { + cursors.push(style!(color.1, color.0).paint(" ")); + len += 1; + } } + (cursors, len) } -pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart { - 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) +pub fn render_tab( + text: String, + palette: Palette, + separator: &str, + focused_clients: &[ClientId], + active: bool, +) -> LinePart { + let background_color = if active { palette.green } else { palette.fg }; + let left_separator = style!(palette.gray, background_color).paint(separator); + let mut tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding + + let tab_styled_text = style!(palette.black, background_color) .bold() .paint(format!(" {} ", text)); - let right_separator = style!(palette.fg, palette.gray).paint(separator); - let tab_styled_text = - ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string(); + + let right_separator = style!(background_color, palette.gray).paint(separator); + let tab_styled_text = if !focused_clients.is_empty() { + let (cursor_section, extra_length) = cursors(focused_clients, palette); + tab_text_len += extra_length; + let mut s = String::new(); + let cursor_beginning = style!(palette.black, background_color) + .bold() + .paint("[") + .to_string(); + let cursor_section = ANSIStrings(&cursor_section).to_string(); + let cursor_end = style!(palette.black, background_color) + .bold() + .paint("]") + .to_string(); + s.push_str(&left_separator.to_string()); + s.push_str(&tab_styled_text.to_string()); + s.push_str(&cursor_beginning); + s.push_str(&cursor_section); + s.push_str(&cursor_end); + s.push_str(&right_separator.to_string()); + s + } else { + ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string() + }; + LinePart { part: tab_styled_text, len: tab_text_len, @@ -40,15 +69,12 @@ pub fn tab_style( is_sync_panes_active: bool, palette: Palette, capabilities: PluginCapabilities, + focused_clients: &[ClientId], ) -> LinePart { let separator = tab_separator(capabilities); let mut tab_text = text; if is_sync_panes_active { tab_text.push_str(" (Sync)"); } - if is_active_tab { - active_tab(tab_text, palette, separator) - } else { - non_active_tab(tab_text, palette, separator) - } + render_tab(tab_text, palette, separator, focused_clients, is_active_tab) } diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 0585c373..1457c0a9 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -178,7 +178,6 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { name: "Make sure only one pane appears", instruction: |remote_terminal: RemoteTerminal| -> bool { let mut step_is_complete = false; - // if remote_terminal.cursor_position_is(3, 2) && remote_terminal.snapshot_contains("...") if remote_terminal.cursor_position_is(3, 2) { // ... is the truncated tip line step_is_complete = true; @@ -928,7 +927,7 @@ pub fn detach_and_attach_session() { let last_snapshot = loop { RemoteRunner::kill_running_sessions(fake_win_size); drop(()); - let mut runner = RemoteRunner::new(fake_win_size) + let mut runner = RemoteRunner::new_mirrored_session(fake_win_size) .add_step(Step { name: "Split pane to the right", instruction: |mut remote_terminal: RemoteTerminal| -> bool { @@ -1261,20 +1260,21 @@ pub fn mirrored_sessions() { // then make sure they were also reflected (mirrored) in the first runner afterwards RemoteRunner::kill_running_sessions(fake_win_size); drop(()); - let mut first_runner = RemoteRunner::new_with_session_name(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); + let mut first_runner = + RemoteRunner::new_with_session_name(fake_win_size, session_name, true) + .dont_panic() + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }); first_runner.run_all_steps(); let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) @@ -1396,6 +1396,286 @@ pub fn mirrored_sessions() { assert_snapshot!(second_runner_snapshot); } +#[test] +#[ignore] +pub fn multiple_users_in_same_pane_and_tab() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let mut test_attempts = 10; + let session_name = "multiple_users_in_same_pane_and_tab"; + let (first_runner_snapshot, second_runner_snapshot) = loop { + // here we connect with one runner, then connect with another, perform some actions and + // then make sure they were also reflected (mirrored) in the first runner afterwards + RemoteRunner::kill_running_sessions(fake_win_size); + drop(()); + let mut first_runner = + RemoteRunner::new_with_session_name(fake_win_size, session_name, false) + .dont_panic() + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }); + first_runner.run_all_steps(); + + let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) + .dont_panic() + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }); + second_runner.run_all_steps(); + + if first_runner.test_timed_out || second_runner.test_timed_out { + test_attempts -= 1; + continue; + } + let second_runner_snapshot = second_runner.take_snapshot_after(Step { + name: "take snapshot after", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) + && remote_terminal.snapshot_contains("MY FOCUS") + { + // cursor is back in the first tab + step_is_complete = true; + } + step_is_complete + }, + }); + let first_runner_snapshot = first_runner.take_snapshot_after(Step { + name: "take snapshot after", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) + && remote_terminal.snapshot_contains("MY FOCUS") + { + // cursor is back in the first tab + step_is_complete = true; + } + step_is_complete + }, + }); + + if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { + test_attempts -= 1; + continue; + } else { + break (first_runner_snapshot, second_runner_snapshot); + } + }; + assert_snapshot!(first_runner_snapshot); + assert_snapshot!(second_runner_snapshot); +} + +#[test] +#[ignore] +pub fn multiple_users_in_different_panes_and_same_tab() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let mut test_attempts = 10; + let session_name = "multiple_users_in_same_pane_and_tab"; + let (first_runner_snapshot, second_runner_snapshot) = loop { + // here we connect with one runner, then connect with another, perform some actions and + // then make sure they were also reflected (mirrored) in the first runner afterwards + RemoteRunner::kill_running_sessions(fake_win_size); + drop(()); + let mut first_runner = + RemoteRunner::new_with_session_name(fake_win_size, session_name, false) + .dont_panic() + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }); + first_runner.run_all_steps(); + + let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) + .dont_panic() + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }); + second_runner.run_all_steps(); + + if first_runner.test_timed_out || second_runner.test_timed_out { + test_attempts -= 1; + continue; + } + + let second_runner_snapshot = second_runner.take_snapshot_after(Step { + name: "take snapshot after", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + + let first_runner_snapshot = first_runner.take_snapshot_after(Step { + name: "take snapshot after", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) + && remote_terminal.snapshot_contains("││$") + { + // cursor is back in the first tab + step_is_complete = true; + } + step_is_complete + }, + }); + + if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { + test_attempts -= 1; + continue; + } else { + break (first_runner_snapshot, second_runner_snapshot); + } + }; + assert_snapshot!(first_runner_snapshot); + assert_snapshot!(second_runner_snapshot); +} + +#[test] +#[ignore] +pub fn multiple_users_in_different_tabs() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let mut test_attempts = 10; + let session_name = "multiple_users_in_different_tabs"; + let (first_runner_snapshot, second_runner_snapshot) = loop { + // here we connect with one runner, then connect with another, perform some actions and + // then make sure they were also reflected (mirrored) in the first runner afterwards + RemoteRunner::kill_running_sessions(fake_win_size); + drop(()); + let mut first_runner = + RemoteRunner::new_with_session_name(fake_win_size, session_name, false) + .dont_panic() + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }); + first_runner.run_all_steps(); + + let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) + .dont_panic() + .add_step(Step { + name: "Open new tab", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&TAB_MODE); + remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }); + second_runner.run_all_steps(); + + if first_runner.test_timed_out || second_runner.test_timed_out { + test_attempts -= 1; + continue; + } + + let second_runner_snapshot = second_runner.take_snapshot_after(Step { + name: "Wait for new tab to open", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2") + && remote_terminal.status_bar_appears() + { + // cursor is in the newly opened second tab + step_is_complete = true; + } + step_is_complete + }, + }); + + let first_runner_snapshot = first_runner.take_snapshot_after(Step { + name: "Wait for new tab to open", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(3, 2) + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2") + && remote_terminal.status_bar_appears() + { + // cursor is in the newly opened second tab + step_is_complete = true; + } + step_is_complete + }, + }); + + if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { + test_attempts -= 1; + continue; + } else { + break (first_runner_snapshot, second_runner_snapshot); + } + }; + assert_snapshot!(first_runner_snapshot); + assert_snapshot!(second_runner_snapshot); +} + #[test] #[ignore] pub fn bracketed_paste() { diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 205710f1..feaeecda 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -67,13 +67,27 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel.flush().unwrap(); } -fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str) { +fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) { stop_zellij(channel); channel .write_all( format!( - "{} --session {}\n", - ZELLIJ_EXECUTABLE_LOCATION, session_name + "{} --session {} options --mirror-session true\n", + ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME + ) + .as_bytes(), + ) + .unwrap(); + channel.flush().unwrap(); +} + +fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) { + stop_zellij(channel); + channel + .write_all( + format!( + "{} --session {} options --mirror-session {}\n", + ZELLIJ_EXECUTABLE_LOCATION, session_name, mirrored ) .as_bytes(), ) @@ -333,13 +347,48 @@ impl RemoteRunner { reader_thread, } } + pub fn new_mirrored_session(win_size: Size) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + setup_remote_environment(&mut channel, win_size); + start_zellij_mirrored_session(&mut channel); + let channel = Arc::new(Mutex::new(channel)); + let last_snapshot = Arc::new(Mutex::new(String::new())); + let cursor_coordinates = Arc::new(Mutex::new((0, 0))); + sess.set_blocking(false); + let reader_thread = + read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); + RemoteRunner { + steps: vec![], + channel, + currently_running_step: None, + current_step_index: 0, + retries_left: RETRIES, + retry_pause_ms: 100, + test_timed_out: false, + panic_on_no_retries_left: true, + last_snapshot, + cursor_coordinates, + reader_thread, + } + } pub fn kill_running_sessions(win_size: Size) { let sess = ssh_connect(); let mut channel = sess.channel_session().unwrap(); setup_remote_environment(&mut channel, win_size); start_zellij(&mut channel); } - pub fn new_with_session_name(win_size: Size, session_name: &str) -> Self { + pub fn new_with_session_name(win_size: Size, session_name: &str, mirrored: bool) -> Self { // notice that this method does not have a timeout, so use with caution! let sess = ssh_connect_without_timeout(); let mut channel = sess.channel_session().unwrap(); @@ -354,7 +403,7 @@ impl RemoteRunner { cols, }; setup_remote_environment(&mut channel, win_size); - start_zellij_in_session(&mut channel, session_name); + start_zellij_in_session(&mut channel, session_name, mirrored); let channel = Arc::new(Mutex::new(channel)); let last_snapshot = Arc::new(Mutex::new(String::new())); let cursor_coordinates = Arc::new(Mutex::new((0, 0))); diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap new file mode 100644 index 00000000..5868eb44 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: second_runner_snapshot + +--- + Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ] +┌ Pane #1 ───────────┤ FOCUSED USER: ├───────────────────┐┌ Pane #2 ──────────────┤ MY FOCUS ├───────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap new file mode 100644 index 00000000..e9355a44 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: first_runner_snapshot + +--- + Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ] +┌ Pane #1 ──────────────┤ MY FOCUS ├───────────────────────┐┌ Pane #2 ───────────┤ FOCUSED USER: ├───────────────────┐ +│$ █ ││$ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap new file mode 100644 index 00000000..7b983dfd --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: second_runner_snapshot + +--- + Zellij (multiple_users_in_different_tabs)  Tab #1 [ ] Tab #2  +┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap new file mode 100644 index 00000000..4a1926d6 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: first_runner_snapshot + +--- + Zellij (multiple_users_in_different_tabs)  Tab #1  Tab #2 [ ] +┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap new file mode 100644 index 00000000..92d189c4 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: second_runner_snapshot + +--- + Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ] +┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap new file mode 100644 index 00000000..9944ee81 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: first_runner_snapshot + +--- + Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ] +┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index f5beade5..7fa63275 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -1,7 +1,9 @@ //! Things related to [`Screen`]s. +use std::cell::RefCell; use std::collections::{BTreeMap, HashSet}; use std::os::unix::io::RawFd; +use std::rc::Rc; use std::str; use zellij_utils::pane_size::Size; @@ -182,6 +184,7 @@ pub(crate) struct Screen { size: Size, /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] overlay: OverlayWindow, + connected_clients: Rc>>, /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap, tab_history: BTreeMap>, @@ -189,6 +192,7 @@ pub(crate) struct Screen { default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication colors: Palette, draw_pane_frames: bool, + session_is_mirrored: bool, } impl Screen { @@ -199,12 +203,14 @@ impl Screen { max_panes: Option, mode_info: ModeInfo, draw_pane_frames: bool, + session_is_mirrored: bool, ) -> Self { Screen { bus, max_panes, size: client_attributes.size, colors: client_attributes.palette, + connected_clients: Rc::new(RefCell::new(HashSet::new())), active_tab_indices: BTreeMap::new(), tabs: BTreeMap::new(), overlay: OverlayWindow::default(), @@ -212,6 +218,7 @@ impl Screen { mode_info: BTreeMap::new(), default_mode_info: mode_info, draw_pane_frames, + session_is_mirrored, } } @@ -226,34 +233,50 @@ impl Screen { } } - fn move_clients_from_closed_tab(&mut self, previous_tab_index: usize) { - let client_ids_in_closed_tab: Vec = self - .active_tab_indices - .iter() - .filter(|(_c_id, t_index)| **t_index == previous_tab_index) - .map(|(c_id, _t_index)| c_id) - .copied() - .collect(); - for client_id in client_ids_in_closed_tab { + fn move_clients_from_closed_tab( + &mut self, + client_ids_and_mode_infos: Vec<(ClientId, ModeInfo)>, + ) { + for (client_id, client_mode_info) in client_ids_and_mode_infos { let client_previous_tab = self.tab_history.get_mut(&client_id).unwrap().pop().unwrap(); self.active_tab_indices .insert(client_id, client_previous_tab); self.tabs .get_mut(&client_previous_tab) .unwrap() - .add_client(client_id); + .add_client(client_id, Some(client_mode_info)); } } - fn move_clients(&mut self, source_index: usize, destination_index: usize) { - let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = { - let source_tab = self.tabs.get_mut(&source_index).unwrap(); - source_tab.drain_connected_clients() - }; - let destination_tab = self.tabs.get_mut(&destination_index).unwrap(); - destination_tab.add_multiple_clients( - connected_clients_in_source_tab, - client_mode_infos_in_source_tab, - ); + fn move_clients_between_tabs( + &mut self, + source_tab_index: usize, + destination_tab_index: usize, + clients_to_move: Option>, + ) { + // None ==> move all clients + let drained_clients = self + .get_indexed_tab_mut(source_tab_index) + .map(|t| t.drain_connected_clients(clients_to_move)); + if let Some(client_mode_info_in_source_tab) = drained_clients { + let destination_tab = self.get_indexed_tab_mut(destination_tab_index).unwrap(); + destination_tab.add_multiple_clients(client_mode_info_in_source_tab); + destination_tab.update_input_modes(); + destination_tab.set_force_render(); + destination_tab.visible(true); + } + } + fn update_client_tab_focus(&mut self, client_id: ClientId, new_tab_index: usize) { + match self.active_tab_indices.remove(&client_id) { + Some(old_active_index) => { + self.active_tab_indices.insert(client_id, new_tab_index); + let client_tab_history = self.tab_history.entry(client_id).or_insert_with(Vec::new); + client_tab_history.retain(|&e| e != new_tab_index); + client_tab_history.push(old_active_index); + } + None => { + self.active_tab_indices.insert(client_id, new_tab_index); + } + } } /// A helper function to switch to a new tab at specified position. fn switch_active_tab(&mut self, new_tab_pos: usize, client_id: ClientId) { @@ -265,24 +288,29 @@ impl Screen { return; } - current_tab.visible(false); let current_tab_index = current_tab.index; let new_tab_index = new_tab.index; - let new_tab = self.get_indexed_tab_mut(new_tab_index).unwrap(); - new_tab.set_force_render(); - new_tab.visible(true); - - // currently all clients are just mirrors, so we perform the action for every entry in - // tab_history - // TODO: receive a client_id and only do it for the client - for (client_id, tab_history) in &mut self.tab_history { - let old_active_index = self.active_tab_indices.remove(client_id).unwrap(); - self.active_tab_indices.insert(*client_id, new_tab_index); - tab_history.retain(|&e| e != new_tab_pos); - tab_history.push(old_active_index); + if self.session_is_mirrored { + self.move_clients_between_tabs(current_tab_index, new_tab_index, None); + let all_connected_clients: Vec = + self.connected_clients.borrow().iter().copied().collect(); + for client_id in all_connected_clients { + self.update_client_tab_focus(client_id, new_tab_index); + } + } else { + self.move_clients_between_tabs( + current_tab_index, + new_tab_index, + Some(vec![client_id]), + ); + self.update_client_tab_focus(client_id, new_tab_index); } - self.move_clients(current_tab_index, new_tab_index); + if let Some(current_tab) = self.get_indexed_tab_mut(current_tab_index) { + if current_tab.has_no_connected_clients() { + current_tab.visible(false); + } + } self.update_tabs(); self.render(); @@ -314,7 +342,7 @@ impl Screen { } fn close_tab_at_index(&mut self, tab_index: usize) { - let tab_to_close = self.tabs.remove(&tab_index).unwrap(); + let mut tab_to_close = self.tabs.remove(&tab_index).unwrap(); let pane_ids = tab_to_close.get_pane_ids(); // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread @@ -330,7 +358,8 @@ impl Screen { .send_to_server(ServerInstruction::Render(None)) .unwrap(); } else { - self.move_clients_from_closed_tab(tab_index); + let client_mode_infos_in_closed_tab = tab_to_close.drain_connected_clients(None); + self.move_clients_from_closed_tab(client_mode_infos_in_closed_tab); let visible_tab_indices: HashSet = self.active_tab_indices.values().copied().collect(); for t in self.tabs.values_mut() { @@ -446,28 +475,38 @@ impl Screen { client_mode_info, self.colors, self.draw_pane_frames, + self.connected_clients.clone(), + self.session_is_mirrored, client_id, ); tab.apply_layout(layout, new_pids, tab_index, client_id); - if let Some(active_tab) = self.get_active_tab_mut(client_id) { - active_tab.visible(false); - let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = - active_tab.drain_connected_clients(); - tab.add_multiple_clients( - connected_clients_in_source_tab, - client_mode_infos_in_source_tab, - ); - } - for (client_id, tab_history) in &mut self.tab_history { - let old_active_index = self.active_tab_indices.remove(client_id).unwrap(); - self.active_tab_indices.insert(*client_id, tab_index); - tab_history.retain(|&e| e != tab_index); - tab_history.push(old_active_index); + if self.session_is_mirrored { + if let Some(active_tab) = self.get_active_tab_mut(client_id) { + let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None); + tab.add_multiple_clients(client_mode_infos_in_source_tab); + if active_tab.has_no_connected_clients() { + active_tab.visible(false); + } + } + let all_connected_clients: Vec = + self.connected_clients.borrow().iter().copied().collect(); + for client_id in all_connected_clients { + self.update_client_tab_focus(client_id, tab_index); + } + } else if let Some(active_tab) = self.get_active_tab_mut(client_id) { + let client_mode_info_in_source_tab = + active_tab.drain_connected_clients(Some(vec![client_id])); + tab.add_multiple_clients(client_mode_info_in_source_tab); + if active_tab.has_no_connected_clients() { + active_tab.visible(false); + } + self.update_client_tab_focus(client_id, tab_index); } tab.update_input_modes(); tab.visible(true); self.tabs.insert(tab_index, tab); if !self.active_tab_indices.contains_key(&client_id) { + // this means this is a new client and we need to add it to our state properly self.add_client(client_id); } self.update_tabs(); @@ -486,12 +525,19 @@ impl Screen { tab_history = first_tab_history.clone(); } self.active_tab_indices.insert(client_id, tab_index); + self.connected_clients.borrow_mut().insert(client_id); self.tab_history.insert(client_id, tab_history); - self.tabs.get_mut(&tab_index).unwrap().add_client(client_id); + self.tabs + .get_mut(&tab_index) + .unwrap() + .add_client(client_id, None); } pub fn remove_client(&mut self, client_id: ClientId) { if let Some(client_tab) = self.get_active_tab_mut(client_id) { client_tab.remove_client(client_id); + if client_tab.has_no_connected_clients() { + client_tab.visible(false); + } } if self.active_tab_indices.contains_key(&client_id) { self.active_tab_indices.remove(&client_id); @@ -499,30 +545,41 @@ impl Screen { if self.tab_history.contains_key(&client_id) { self.tab_history.remove(&client_id); } + self.connected_clients.borrow_mut().remove(&client_id); + self.update_tabs(); } pub fn update_tabs(&self) { - let mut tab_data = vec![]; - // TODO: right now all clients are synced, so we just take the first active_tab which is - // the same for everyone - when this is no longer the case, we need to update the TabInfo - // to account for this (or send multiple TabInfos) - if let Some((_first_client, first_active_tab_index)) = self.active_tab_indices.iter().next() - { + for (client_id, active_tab_index) in self.active_tab_indices.iter() { + let mut tab_data = vec![]; for tab in self.tabs.values() { + let other_focused_clients: Vec = if self.session_is_mirrored { + vec![] + } else { + self.active_tab_indices + .iter() + .filter(|(c_id, tab_position)| { + **tab_position == tab.index && *c_id != client_id + }) + .map(|(c_id, _)| c_id) + .copied() + .collect() + }; tab_data.push(TabInfo { position: tab.position, name: tab.name.clone(), - active: *first_active_tab_index == tab.index, + active: *active_tab_index == tab.index, panes_to_hide: tab.panes_to_hide.len(), is_fullscreen_active: tab.is_fullscreen_active(), is_sync_panes_active: tab.is_sync_panes_active(), + other_focused_clients, }); } self.bus .senders .send_to_plugin(PluginInstruction::Update( None, - None, + Some(*client_id), Event::TabUpdate(tab_data), )) .unwrap(); @@ -607,6 +664,7 @@ pub(crate) fn screen_thread_main( ) { let capabilities = config_options.simplified_ui; let draw_pane_frames = config_options.pane_frames.unwrap_or(true); + let session_is_mirrored = config_options.mirror_session.unwrap_or(false); let mut screen = Screen::new( bus, @@ -620,6 +678,7 @@ pub(crate) fn screen_thread_main( }, ), draw_pane_frames, + session_is_mirrored, ); loop { let (event, mut err_ctx) = screen diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index ca8153ec..fc5b0718 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -17,7 +17,9 @@ use crate::{ ClientId, ServerInstruction, }; use serde::{Deserialize, Serialize}; +use std::cell::RefCell; use std::os::unix::io::RawFd; +use std::rc::Rc; use std::sync::mpsc::channel; use std::time::Instant; use std::{ @@ -111,9 +113,16 @@ impl Output { .insert(*client_id, String::new()); } } - pub fn push_str_to_all_clients(&mut self, to_push: &str) { - for render_instruction in self.client_render_instructions.values_mut() { - render_instruction.push_str(to_push) + pub fn push_str_to_multiple_clients( + &mut self, + to_push: &str, + client_ids: impl Iterator, + ) { + for client_id in client_ids { + self.client_render_instructions + .get_mut(&client_id) + .unwrap() + .push_str(to_push) } } pub fn push_to_client(&mut self, client_id: ClientId, to_push: &str) { @@ -141,6 +150,7 @@ pub(crate) struct Tab { mode_info: HashMap, default_mode_info: ModeInfo, pub colors: Palette, + connected_clients_in_app: Rc>>, // TODO: combine this and connected_clients connected_clients: HashSet, draw_pane_frames: bool, session_is_mirrored: bool, @@ -322,6 +332,8 @@ impl Tab { mode_info: ModeInfo, colors: Palette, draw_pane_frames: bool, + connected_clients_in_app: Rc>>, + session_is_mirrored: bool, client_id: ClientId, ) -> Self { let panes = BTreeMap::new(); @@ -354,10 +366,9 @@ impl Tab { default_mode_info: 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, + session_is_mirrored, pending_vte_events: HashMap::new(), + connected_clients_in_app, connected_clients, } } @@ -491,14 +502,16 @@ impl Tab { .unwrap(); } } - pub fn add_client(&mut self, client_id: ClientId) { + pub fn add_client(&mut self, client_id: ClientId, mode_info: Option) { match self.connected_clients.iter().next() { Some(first_client_id) => { let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap(); self.connected_clients.insert(client_id); self.active_panes.insert(client_id, first_active_pane_id); - self.mode_info - .insert(client_id, self.default_mode_info.clone()); + self.mode_info.insert( + client_id, + mode_info.unwrap_or_else(|| self.default_mode_info.clone()), + ); } None => { let mut pane_ids: Vec = self.panes.keys().copied().collect(); @@ -511,40 +524,53 @@ impl Tab { let first_pane_id = pane_ids.get(0).unwrap(); self.connected_clients.insert(client_id); self.active_panes.insert(client_id, *first_pane_id); - self.mode_info - .insert(client_id, self.default_mode_info.clone()); + self.mode_info.insert( + client_id, + mode_info.unwrap_or_else(|| self.default_mode_info.clone()), + ); } } // TODO: we might be able to avoid this, we do this so that newly connected clients will // necessarily get a full render self.set_force_render(); + self.update_input_modes(); } pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) { self.mode_info.insert(client_id, mode_info); } - pub fn add_multiple_clients( - &mut self, - client_ids: Vec, - client_mode_infos: Vec<(ClientId, ModeInfo)>, - ) { - for client_id in client_ids { - self.add_client(client_id); - } - for (client_id, client_mode_info) in client_mode_infos { + pub fn add_multiple_clients(&mut self, client_ids_to_mode_infos: Vec<(ClientId, ModeInfo)>) { + for (client_id, client_mode_info) in client_ids_to_mode_infos { + self.add_client(client_id, None); self.mode_info.insert(client_id, client_mode_info); } } 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, Vec<(ClientId, ModeInfo)>) { - let client_mode_info = self.mode_info.drain(); - ( - self.connected_clients.drain().collect(), - client_mode_info.collect(), - ) + pub fn drain_connected_clients( + &mut self, + clients_to_drain: Option>, + ) -> Vec<(ClientId, ModeInfo)> { + // None => all clients + let mut client_ids_to_mode_infos = vec![]; + let clients_to_drain = + clients_to_drain.unwrap_or_else(|| self.connected_clients.drain().collect()); + for client_id in clients_to_drain { + client_ids_to_mode_infos.push(self.drain_single_client(client_id)); + } + client_ids_to_mode_infos + } + pub fn drain_single_client(&mut self, client_id: ClientId) -> (ClientId, ModeInfo) { + let client_mode_info = self + .mode_info + .remove(&client_id) + .unwrap_or_else(|| self.default_mode_info.clone()); + self.connected_clients.remove(&client_id); + (client_id, client_mode_info) + } + pub fn has_no_connected_clients(&self) -> bool { + self.connected_clients.is_empty() } pub fn new_pane(&mut self, pid: PaneId, client_id: Option) { self.close_down_to_max_terminals(); @@ -786,7 +812,8 @@ impl Tab { }); } pub fn write_to_active_terminal(&mut self, input_bytes: Vec, client_id: ClientId) { - self.write_to_pane_id(input_bytes, self.get_active_pane_id(client_id).unwrap()); + let pane_id = self.get_active_pane_id(client_id).unwrap(); + self.write_to_pane_id(input_bytes, pane_id); } pub fn write_to_pane_id(&mut self, input_bytes: Vec, pane_id: PaneId) { match pane_id { @@ -981,10 +1008,21 @@ impl Tab { // render panes and their frames for (kind, pane) in self.panes.iter_mut() { if !self.panes_to_hide.contains(&pane.pid()) { - let mut pane_contents_and_ui = - PaneContentsAndUi::new(pane, output, self.colors, &self.active_panes); + let mut active_panes = self.active_panes.clone(); + let multiple_users_exist_in_session = + { self.connected_clients_in_app.borrow().len() > 1 }; + active_panes.retain(|c_id, _| self.connected_clients.contains(c_id)); + let mut pane_contents_and_ui = PaneContentsAndUi::new( + pane, + output, + self.colors, + &active_panes, + multiple_users_exist_in_session, + ); if let PaneId::Terminal(..) = kind { - pane_contents_and_ui.render_pane_contents_for_all_clients(); + pane_contents_and_ui.render_pane_contents_to_multiple_clients( + self.connected_clients.iter().copied(), + ); } for &client_id in &self.connected_clients { let client_mode = self @@ -1024,16 +1062,21 @@ impl Tab { } // FIXME: Once clients can be distinguished if let Some(overlay_vte) = &overlay { - output.push_str_to_all_clients(overlay_vte); + // output.push_str_to_all_clients(overlay_vte); + output + .push_str_to_multiple_clients(overlay_vte, self.connected_clients.iter().copied()); } 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); + output.push_str_to_multiple_clients(hide_cursor, self.connected_clients.iter().copied()); if self.should_clear_display_before_rendering { let clear_display = "\u{1b}[2J"; - output.push_str_to_all_clients(clear_display); + output.push_str_to_multiple_clients( + clear_display, + self.connected_clients.iter().copied(), + ); self.should_clear_display_before_rendering = false; } } @@ -3400,10 +3443,10 @@ impl Tab { fn write_selection_to_clipboard(&self, selection: &str) { let mut output = Output::default(); output.add_clients(&self.connected_clients); - output.push_str_to_all_clients(&format!( - "\u{1b}]52;c;{}\u{1b}\\", - base64::encode(selection) - )); + output.push_str_to_multiple_clients( + &format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)), + self.connected_clients.iter().copied(), + ); // TODO: ideally we should be sending the Render instruction from the screen self.senders diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 1ed29d83..419dea79 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -3,7 +3,7 @@ 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::{Palette, PaletteColor}; +use zellij_utils::zellij_tile::prelude::{client_id_to_colors, Palette, PaletteColor}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -29,22 +29,6 @@ fn background_color(character: &str, color: Option) -> String { } } -// 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, diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index 25550fe9..d8967f6e 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -1,11 +1,12 @@ 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}; +use zellij_tile::data::{ + client_id_to_colors, single_client_color, InputMode, Palette, PaletteColor, +}; pub struct PaneContentsAndUi<'a> { pane: &'a mut Box, @@ -21,13 +22,13 @@ impl<'a> PaneContentsAndUi<'a> { output: &'a mut Output, colors: Palette, active_panes: &HashMap, + multiple_users_exist_in_session: bool, ) -> 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, @@ -36,15 +37,21 @@ impl<'a> PaneContentsAndUi<'a> { multiple_users_exist_in_session, } } - pub fn render_pane_contents_for_all_clients(&mut self) { + pub fn render_pane_contents_to_multiple_clients( + &mut self, + clients: impl Iterator, + ) { if let Some(vte_output) = self.pane.render(None) { // 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 - )); + self.output.push_str_to_multiple_clients( + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + vte_output + ), + clients, + ); } } pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) { @@ -165,9 +172,9 @@ impl<'a> PaneContentsAndUi<'a> { 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) + if session_is_mirrored || !self.multiple_users_exist_in_session { + let colors = single_client_color(self.colors); // mirrored sessions only have one focused color + Some(colors.0) } else { let colors = client_id_to_colors(client_id, self.colors); colors.map(|colors| colors.0) diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index cb279958..ca34645d 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -90,12 +90,14 @@ fn create_new_screen(size: Size) -> Screen { let max_panes = None; let mode_info = ModeInfo::default(); let draw_pane_frames = false; + let session_is_mirrored = true; Screen::new( bus, &client_attributes, max_panes, mode_info, draw_pane_frames, + session_is_mirrored, ) } diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index 426ae98b..d356cad7 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -12,7 +12,10 @@ use zellij_utils::input::layout::LayoutTemplate; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; +use std::cell::{RefCell, RefMut}; +use std::collections::HashSet; use std::os::unix::io::RawFd; +use std::rc::Rc; use zellij_utils::nix; @@ -89,6 +92,10 @@ fn create_new_tab(size: Size) -> Tab { let colors = Palette::default(); let draw_pane_frames = true; let client_id = 1; + let session_is_mirrored = true; + let mut connected_clients = HashSet::new(); + connected_clients.insert(client_id); + let connected_clients = Rc::new(RefCell::new(connected_clients)); let mut tab = Tab::new( index, position, @@ -100,6 +107,8 @@ fn create_new_tab(size: Size) -> Tab { mode_info, colors, draw_pane_frames, + connected_clients, + session_is_mirrored, client_id, ); tab.apply_layout( diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 3e269af3..36b44e25 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -3,6 +3,32 @@ use std::fmt; use std::str::FromStr; use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; +pub type ClientId = u16; // TODO: merge with crate type? + +pub fn client_id_to_colors( + client_id: ClientId, + colors: Palette, +) -> Option<(PaletteColor, PaletteColor)> { + // (primary color, secondary color) + match client_id { + 1 => Some((colors.magenta, colors.black)), + 2 => Some((colors.blue, colors.black)), + 3 => Some((colors.purple, colors.black)), + 4 => Some((colors.yellow, colors.black)), + 5 => Some((colors.cyan, colors.black)), + 6 => Some((colors.gold, colors.black)), + 7 => Some((colors.red, colors.black)), + 8 => Some((colors.silver, colors.black)), + 9 => Some((colors.pink, colors.black)), + 10 => Some((colors.brown, colors.black)), + _ => None, + } +} + +pub fn single_client_color(colors: Palette) -> (PaletteColor, PaletteColor) { + (colors.green, colors.black) +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Key { Backspace, @@ -169,6 +195,11 @@ pub struct Palette { pub white: PaletteColor, pub orange: PaletteColor, pub gray: PaletteColor, + pub purple: PaletteColor, + pub gold: PaletteColor, + pub silver: PaletteColor, + pub pink: PaletteColor, + pub brown: PaletteColor, } /// Represents the contents of the help message that is printed in the status bar, @@ -193,6 +224,7 @@ pub struct TabInfo { pub panes_to_hide: usize, pub is_fullscreen_active: bool, pub is_sync_panes_active: bool, + pub other_focused_clients: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 55036069..e0a38946 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -65,6 +65,10 @@ pub struct Options { #[serde(default)] /// Set display of the pane frames (true or false) pub pane_frames: Option, + #[structopt(long)] + #[serde(default)] + /// Mirror session when multiple users are connected (true or false) + pub mirror_session: Option, /// Set behaviour on force close (quit or detach) #[structopt(long)] pub on_force_close: Option, @@ -85,6 +89,7 @@ impl Options { pub fn merge(&self, other: Options) -> Options { let mouse_mode = other.mouse_mode.or(self.mouse_mode); let pane_frames = other.pane_frames.or(self.pane_frames); + let mirror_session = other.mirror_session.or(self.mirror_session); let simplified_ui = other.simplified_ui.or(self.simplified_ui); let default_mode = other.default_mode.or(self.default_mode); let default_shell = other.default_shell.or_else(|| self.default_shell.clone()); @@ -100,6 +105,7 @@ impl Options { layout_dir, mouse_mode, pane_frames, + mirror_session, on_force_close, } } @@ -122,6 +128,7 @@ impl Options { let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui); let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode); let pane_frames = merge_bool(other.pane_frames, self.pane_frames); + let mirror_session = merge_bool(other.mirror_session, self.mirror_session); let default_mode = other.default_mode.or(self.default_mode); let default_shell = other.default_shell.or_else(|| self.default_shell.clone()); @@ -137,6 +144,7 @@ impl Options { layout_dir, mouse_mode, pane_frames, + mirror_session, on_force_close, } } @@ -183,6 +191,7 @@ impl From for Options { layout_dir: opts.layout_dir, mouse_mode: opts.mouse_mode, pane_frames: opts.pane_frames, + mirror_session: opts.mirror_session, on_force_close: opts.on_force_close, } } diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs index 0211fb2c..79695435 100644 --- a/zellij-utils/src/shared.rs +++ b/zellij-utils/src/shared.rs @@ -53,6 +53,11 @@ pub mod colors { pub const CYAN: u8 = 51; pub const YELLOW: u8 = 226; pub const BLUE: u8 = 45; + pub const PURPLE: u8 = 99; + pub const GOLD: u8 = 136; + pub const SILVER: u8 = 245; + pub const PINK: u8 = 207; + pub const BROWN: u8 = 215; } pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) { @@ -77,6 +82,11 @@ pub fn default_palette() -> Palette { white: PaletteColor::EightBit(colors::WHITE), orange: PaletteColor::EightBit(colors::ORANGE), gray: PaletteColor::EightBit(colors::GRAY), + purple: PaletteColor::EightBit(colors::PURPLE), + gold: PaletteColor::EightBit(colors::GOLD), + silver: PaletteColor::EightBit(colors::SILVER), + pink: PaletteColor::EightBit(colors::PINK), + brown: PaletteColor::EightBit(colors::BROWN), } }