Skip to content

Commit 60f7aa7

Browse files
authored
Minor fixes for 45 patch (#5550)
* fix: stale theme and floating visibility issues, notification bounding * fix: repaint excess tab surface when closing tab and moving to a smaller one
1 parent f4d38d6 commit 60f7aa7

6 files changed

Lines changed: 211 additions & 7 deletions

File tree

zellij-integration-tests/tests/clients.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,44 @@ fn focusing_a_smaller_tab_leaves_nothing_behind_outside_it() {
522522
zellij.quit();
523523
}
524524

525+
#[test]
526+
fn closing_a_larger_tab_leaves_nothing_behind_outside_the_one_returned_to() {
527+
let mut zellij = start_zellij();
528+
let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij);
529+
mark_pane(zellij.main_client(), &first_terminal, "oneone");
530+
531+
let second_client = zellij.attach_client(LARGER_CLIENT_SIZE);
532+
second_client.wait_until(
533+
"second client attached to the shared tab",
534+
|grid_snapshot| settled_in_tab_sized(grid_snapshot, TERMINAL_SIZE, "oneone"),
535+
);
536+
let second_tab_terminal = open_marked_tab(&zellij, &second_client, "twotwo");
537+
second_tab_terminal.wait_for_size("second tab laid out for its lone viewer", |cols, rows| {
538+
(cols, rows) == pane_size_in_tab_sized(LARGER_CLIENT_SIZE)
539+
});
540+
541+
close_focused_tab(&second_client);
542+
543+
let main_grid = zellij.wait_until("main client settled on the shared tab", |grid_snapshot| {
544+
settled_in_normal_mode(grid_snapshot, TERMINAL_SIZE, "oneone")
545+
&& grid_snapshot.contains("Tab #1 [ ]")
546+
});
547+
let second_grid =
548+
second_client.wait_until("larger client settled on the shared tab", |grid_snapshot| {
549+
settled_in_normal_mode(grid_snapshot, TERMINAL_SIZE, "oneone")
550+
&& grid_snapshot.contains("Tab #1 [ ]")
551+
&& !grid_snapshot.contains("twotwo")
552+
});
553+
554+
assert_eq!(
555+
rendered_content(&second_grid),
556+
rendered_content(&main_grid),
557+
"a client returning from a tab larger than the one it lands in must not leave that tab's leftovers around it"
558+
);
559+
second_client.quit();
560+
zellij.quit();
561+
}
562+
525563
#[test]
526564
fn opening_a_new_tab_regrows_the_tab_its_creator_left() {
527565
let mut zellij = start_zellij();

zellij-server/src/panes/grid.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const BASE64_DECODER: GeneralPurpose = GeneralPurpose::new(
4545
);
4646

4747
const MAX_TRACKED_NOTIFICATION_IDS: usize = 256;
48+
const MAX_NOTIFICATION_ASSEMBLY_BYTES: usize = 4096;
4849

4950
#[derive(Debug, Clone, PartialEq, Eq)]
5051
pub enum PendingNotification {
@@ -129,6 +130,22 @@ struct NotificationAssembly {
129130
body: String,
130131
}
131132

133+
fn append_bounded(destination: &mut String, payload: &str) {
134+
let remaining = MAX_NOTIFICATION_ASSEMBLY_BYTES.saturating_sub(destination.len());
135+
if remaining == 0 {
136+
return;
137+
}
138+
if payload.len() <= remaining {
139+
destination.push_str(payload);
140+
return;
141+
}
142+
let mut truncate_at = remaining;
143+
while truncate_at > 0 && !payload.is_char_boundary(truncate_at) {
144+
truncate_at -= 1;
145+
}
146+
destination.push_str(&payload[..truncate_at]);
147+
}
148+
132149
#[derive(Debug, Clone, Default)]
133150
pub struct NotificationTracker {
134151
wants_report: HashMap<String, bool>,
@@ -180,8 +197,8 @@ impl NotificationTracker {
180197
self.remember(id);
181198
let assembly = self.assemblies.entry(id.to_owned()).or_default();
182199
match payload_type {
183-
Osc99PayloadType::Title => assembly.title.push_str(&payload),
184-
Osc99PayloadType::Body => assembly.body.push_str(&payload),
200+
Osc99PayloadType::Title => append_bounded(&mut assembly.title, &payload),
201+
Osc99PayloadType::Body => append_bounded(&mut assembly.body, &payload),
185202
_ => {},
186203
}
187204
}

zellij-server/src/panes/unit/grid_tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9085,6 +9085,42 @@ fn the_notification_state_remembered_per_pane_is_bounded() {
90859085
);
90869086
}
90879087

9088+
#[test]
9089+
fn a_single_notification_being_assembled_is_bounded() {
9090+
let mut grid = new_grid_for_forwarding_test();
9091+
let mut vte_parser = vte::Parser::new();
9092+
let chunk = "\u{5efa}".repeat(300);
9093+
for _ in 0..100 {
9094+
vte_parser.advance(
9095+
&mut grid,
9096+
format!("\x1b]99;i=1:d=0;{}\x1b\\", chunk).as_bytes(),
9097+
);
9098+
}
9099+
9100+
let assembled_bytes = grid
9101+
.notification_tracker
9102+
.assemblies
9103+
.get("1")
9104+
.map(|assembly| assembly.title.len())
9105+
.unwrap_or(0);
9106+
assert!(
9107+
assembled_bytes <= 4096,
9108+
"an app streaming an endless single notification does not grow the pane's state without \
9109+
bound, got {} bytes",
9110+
assembled_bytes
9111+
);
9112+
9113+
vte_parser.advance(&mut grid, b"\x1b]99;i=1:d=1;\x1b\\");
9114+
let last_notification = grid.pending_desktop_notifications.len() - 1;
9115+
let (title, _body) =
9116+
osc99_display(&grid, last_notification).expect("the truncated notification is still shown");
9117+
assert_eq!(
9118+
title,
9119+
"\u{5efa}".repeat(1365),
9120+
"the assembled text is truncated on a character boundary"
9121+
);
9122+
}
9123+
90889124
fn rendered_row(grid: &Grid, row_index: usize) -> String {
90899125
grid.viewport[row_index]
90909126
.columns

zellij-server/src/screen.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,7 +1913,7 @@ impl Screen {
19131913
.next()
19141914
.context("screen contained no tabs")
19151915
.with_context(err_context)?;
1916-
let mut destination_tab_ids = HashSet::new();
1916+
let mut arriving_client_ids: HashMap<usize, Vec<ClientId>> = HashMap::new();
19171917
for (client_id, client_mode_info) in client_ids_and_mode_infos {
19181918
let client_tab_history = self.tab_history.entry(client_id).or_insert_with(Vec::new);
19191919
if let Some(client_previous_tab) = client_tab_history.pop() {
@@ -1922,7 +1922,10 @@ impl Screen {
19221922
client_active_tab
19231923
.add_client(client_id, Some(client_mode_info))
19241924
.with_context(err_context)?;
1925-
destination_tab_ids.insert(client_previous_tab);
1925+
arriving_client_ids
1926+
.entry(client_previous_tab)
1927+
.or_default()
1928+
.push(client_id);
19261929
continue;
19271930
}
19281931
}
@@ -1932,13 +1935,26 @@ impl Screen {
19321935
.with_context(err_context)?
19331936
.add_client(client_id, Some(client_mode_info))
19341937
.with_context(err_context)?;
1935-
destination_tab_ids.insert(first_tab_index);
1938+
arriving_client_ids
1939+
.entry(first_tab_index)
1940+
.or_default()
1941+
.push(client_id);
19361942
}
1937-
for destination_tab_id in destination_tab_ids {
1938-
if let Some(destination_tab) = self.tabs.get_mut(&destination_tab_id) {
1943+
let destinations_overflowed_by_arriving_clients: HashSet<usize> = arriving_client_ids
1944+
.iter()
1945+
.filter(|(destination_tab_id, client_ids)| {
1946+
self.clients_are_larger_than_tab(**destination_tab_id, client_ids)
1947+
})
1948+
.map(|(destination_tab_id, _client_ids)| *destination_tab_id)
1949+
.collect();
1950+
for destination_tab_id in arriving_client_ids.keys() {
1951+
if let Some(destination_tab) = self.tabs.get_mut(destination_tab_id) {
19391952
destination_tab
19401953
.update_input_modes()
19411954
.with_context(err_context)?;
1955+
if destinations_overflowed_by_arriving_clients.contains(destination_tab_id) {
1956+
destination_tab.set_should_clear_display_before_rendering();
1957+
}
19421958
}
19431959
}
19441960
Ok(())
@@ -5070,8 +5086,12 @@ impl Screen {
50705086
.tabs
50715087
.get_mut(&tab_index)
50725088
.with_context(|| err_context(tab_index))?;
5089+
let tab_was_empty = tab.has_no_connected_clients();
50735090
tab.add_client(client_id, None)
50745091
.with_context(|| err_context(tab_index))?;
5092+
if tab_was_empty {
5093+
tab.visible(true).with_context(|| err_context(tab_index))?;
5094+
}
50755095
if attach_to_first_tab_on_tiled_surface && tab.are_floating_panes_visible() {
50765096
tab.hide_floating_panes();
50775097
}
@@ -7100,6 +7120,7 @@ impl Screen {
71007120
explicit_theme_hue: Option<ThemeHue>,
71017121
) -> Result<()> {
71027122
self.configured_explicit_theme_hue = explicit_theme_hue;
7123+
self.host_terminal_theme_mode = None;
71037124
match explicit_theme_hue {
71047125
Some(hue) => {
71057126
let mode = HostTerminalThemeMode::from(hue);

zellij-server/src/unit/screen_tests.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9770,6 +9770,19 @@ impl ThemeCapture {
97709770
}
97719771
out
97729772
}
9773+
fn drain_visible_events(&self) -> Vec<(Option<u32>, bool)> {
9774+
let mut out = Vec::new();
9775+
while let Ok((instr, _ctx)) = self.plugin_rx.try_recv() {
9776+
if let PluginInstruction::Update(updates) = instr {
9777+
for (pid, _cid, ev) in updates {
9778+
if let Event::Visible(is_visible) = ev {
9779+
out.push((pid, is_visible));
9780+
}
9781+
}
9782+
}
9783+
}
9784+
out
9785+
}
97739786
fn drain_pty_writes(&self) -> Vec<(Vec<u8>, u32)> {
97749787
let mut out = Vec::new();
97759788
while let Ok((instr, _ctx)) = self.pty_writer_rx.try_recv() {
@@ -9781,6 +9794,41 @@ impl ThemeCapture {
97819794
}
97829795
}
97839796

9797+
#[test]
9798+
fn reattaching_a_client_restores_floating_pane_visibility_notifications() {
9799+
let size = Size {
9800+
cols: 121,
9801+
rows: 20,
9802+
};
9803+
let (mut screen, capture) = create_new_screen_with_theme_capture(size);
9804+
new_tab(&mut screen, 1, 0);
9805+
screen
9806+
.get_active_tab_mut(1)
9807+
.unwrap()
9808+
.new_pane(
9809+
PaneId::Plugin(2),
9810+
None,
9811+
None,
9812+
false,
9813+
true,
9814+
NewPanePlacement::Floating(None),
9815+
Some(1),
9816+
None,
9817+
)
9818+
.unwrap();
9819+
9820+
screen.remove_client(1).expect("TEST");
9821+
screen.add_client(1, false).expect("TEST");
9822+
let _ = capture.drain_visible_events();
9823+
screen.get_active_tab_mut(1).unwrap().hide_floating_panes();
9824+
9825+
assert!(
9826+
capture.drain_visible_events().contains(&(Some(2), false)),
9827+
"a floating plugin must still be told when its surface is hidden after a reattach, \
9828+
otherwise plugins idling on a timer keep working while off screen"
9829+
);
9830+
}
9831+
97849832
fn create_new_screen_with_theme_capture(size: Size) -> (Screen, ThemeCapture) {
97859833
let (plugin_tx, plugin_rx) = channels::unbounded::<(PluginInstruction, ErrorContext)>();
97869834
let (pty_writer_tx, pty_writer_rx) =
@@ -10269,6 +10317,50 @@ fn removing_explicit_theme_hue_hands_authority_back_to_the_host() {
1026910317
);
1027010318
}
1027110319

10320+
#[test]
10321+
fn pinning_the_current_hue_repaints_the_resolved_palette() {
10322+
let size = Size { cols: 80, rows: 20 };
10323+
let (mut screen, _capture) = create_new_screen_with_dark_and_light_themes(size);
10324+
screen
10325+
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Dark)
10326+
.expect("host report applied");
10327+
screen.style.colors = styling_with_background((1, 2, 3));
10328+
10329+
screen
10330+
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Dark))
10331+
.expect("explicit hue applied");
10332+
10333+
assert_eq!(
10334+
screen.style.colors.text_unselected.background,
10335+
zellij_utils::data::PaletteColor::Rgb(TEST_DARK_BG),
10336+
"pinning the hue the session is already in must still resolve the palette, \
10337+
otherwise a reconfigure leaves the session painted with the static theme"
10338+
);
10339+
}
10340+
10341+
#[test]
10342+
fn unpinning_back_to_the_current_hue_repaints_the_resolved_palette() {
10343+
let size = Size { cols: 80, rows: 20 };
10344+
let (mut screen, _capture) = create_new_screen_with_dark_and_light_themes(size);
10345+
screen
10346+
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Dark)
10347+
.expect("host report applied");
10348+
screen
10349+
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Dark))
10350+
.expect("explicit hue applied");
10351+
screen.style.colors = styling_with_background((1, 2, 3));
10352+
10353+
screen
10354+
.apply_configured_explicit_theme_hue(None)
10355+
.expect("unpinned");
10356+
10357+
assert_eq!(
10358+
screen.style.colors.text_unselected.background,
10359+
zellij_utils::data::PaletteColor::Rgb(TEST_DARK_BG),
10360+
"unpinning to the mode the session is already in must still resolve the palette"
10361+
);
10362+
}
10363+
1027210364
#[test]
1027310365
fn effective_theme_mode_is_reasserted_after_a_theme_definition_change() {
1027410366
let size = Size { cols: 80, rows: 20 };
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)