diff options
author | Aram Drevekenin <aram@poor.dev> | 2021-12-20 17:31:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-20 17:31:07 +0100 |
commit | ca8438b0aa0e5e9aa1c69f0217c275d20f269a8f (patch) | |
tree | 97ee4922eb41415d54d3cb183ad9dbef66bd7186 | |
parent | 2c1d3a9817e70ea3d4b55672166f3417d296cc44 (diff) |
feat(collaboration): implement multiple users (#957)
* work
* feat(collaboration): implement multiple users
* style(cleanup): some leftovers
22 files changed, 858 insertions, 173 deletions
diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm Binary files differindex b45a3247b..fdff24cf6 100644..100755 --- a/assets/plugins/status-bar.wasm +++ b/assets/plugins/status-bar.wasm diff --git a/assets/plugins/strider.wasm b/assets/plugins/strider.wasm Binary files differindex 3b62b85ae..379a32473 100644..100755 --- a/assets/plugins/strider.wasm +++ b/assets/plugins/strider.wasm diff --git a/assets/plugins/tab-bar.wasm b/assets/plugins/tab-bar.wasm Binary files differindex 4f8107df1..ebcfe2ca7 100644..100755 --- a/assets/plugins/tab-bar.wasm +++ b/assets/plugins/tab-bar.wasm diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 31b2f2d37..3db531384 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 db960fd52..4ed3b2076 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<ANSIString>, usize) { + // cursor section, text length + let mut len = 0; + let mut cursors = vec![]; + for client_id in focused_clients.iter() { + if let Some(color) = client_id_to_colors(*client_id, palette) { + cursors.push(style!(color.1, color.0).paint(" ")); + len += 1; + } } + (cursors, len) } -pub fn 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 0585c373b..1457c0a91 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) @@ -1398,6 +1398,286 @@ pub fn mirrored_sessions() { #[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() { let fake_win_size = Size { cols: 120, diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 205710f1d..feaeecdac 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(M |