diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-03 08:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-03 08:20:04 +0100 |
| commit | f618b0653abc918f9c34b5a4e8a06e4a11745b43 (patch) | |
| tree | b2a1cea091bd395fda4843733a081d1c87180823 | |
| parent | feat(web): improve web client ui on mobile (#4723) (diff) | |
| download | sl0ppij-f618b0653abc918f9c34b5a4e8a06e4a11745b43.tar.gz sl0ppij-f618b0653abc918f9c34b5a4e8a06e4a11745b43.tar.xz sl0ppij-f618b0653abc918f9c34b5a4e8a06e4a11745b43.zip | |
Visual indication for terminal bells (#4781)
* initial implementation
* rustfmt
* allow disabling with config
* add missing kdl
* add pr
* fix tests
36 files changed, 640 insertions, 17 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa9bc08..de24119b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix: respect cursor blink and shape in the web client even when terminal attempts to override it (https://github.com/zellij-org/zellij/pull/4762) * feat: allow cli/plugins/keybindings to atomically show/hide floating panes rather than just toggle (https://github.com/zellij-org/zellij/pull/4764) * fix: improve line-wrapping/resize performance (https://github.com/zellij-org/zellij/pull/4778) +* fix: forward terminal BEL from unfocused tab (and also show visual indication in unfocused panes/tabs that have a BEL) (https://github.com/zellij-org/zellij/pull/4781) * fix: improve mobile web client viewport sizing and touch scrolling behavior (https://github.com/zellij-org/zellij/pull/4723) diff --git a/default-plugins/compact-bar/src/tab.rs b/default-plugins/compact-bar/src/tab.rs index 4ed63e69..8d4ada96 100644 --- a/default-plugins/compact-bar/src/tab.rs +++ b/default-plugins/compact-bar/src/tab.rs @@ -42,7 +42,13 @@ pub fn render_tab( } else { palette.ribbon_unselected.background }; - let foreground_color = if tab.active { + let foreground_color = if tab.is_flashing_bell { + if tab.active { + palette.ribbon_selected.emphasis_3 + } else { + palette.ribbon_unselected.emphasis_3 + } + } else if tab.active { palette.ribbon_selected.base } else { palette.ribbon_unselected.base @@ -102,6 +108,9 @@ pub fn tab_style( } else if tab.is_sync_panes_active { tabname.push_str(" (SYNC)"); } + if tab.has_bell_notification || tab.is_flashing_bell { + tabname.push_str(" [!]"); + } // we only color alternate tabs differently if we can't use the arrow fonts to separate them if !capabilities.arrow_fonts { is_alternate_tab = false; diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index d3eca295..d211d142 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -42,7 +42,13 @@ pub fn render_tab( } else { palette.ribbon_unselected.background }; - let foreground_color = if tab.active { + let foreground_color = if tab.is_flashing_bell { + if tab.active { + palette.ribbon_selected.emphasis_3 + } else { + palette.ribbon_unselected.emphasis_3 + } + } else if tab.active { palette.ribbon_selected.base } else { palette.ribbon_unselected.base @@ -102,6 +108,9 @@ pub fn tab_style( } else if tab.is_sync_panes_active { tabname.push_str(" (SYNC)"); } + if tab.has_bell_notification || tab.is_flashing_bell { + tabname.push_str(" [!]"); + } // we only color alternate tabs differently if we can't use the arrow fonts to separate them if !capabilities.arrow_fonts { is_alternate_tab = false; diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 3e060b1f..bf4b5829 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -68,6 +68,10 @@ pub enum BackgroundJob { ClearHelpText { client_id: ClientId, }, + FlashPaneBell(Vec<PaneId>), + StopFlashPaneBell(Vec<PaneId>), + FlashTabBell(usize), // usize = tab_id + StopFlashTabBell(usize), // usize = tab_id Exit, } @@ -95,6 +99,10 @@ impl From<&BackgroundJob> for BackgroundJobContext { BackgroundJobContext::QueryZellijWebServerStatus }, BackgroundJob::ClearHelpText { .. } => BackgroundJobContext::ClearHelpText, + BackgroundJob::FlashPaneBell(..) => BackgroundJobContext::FlashPaneBell, + BackgroundJob::StopFlashPaneBell(..) => BackgroundJobContext::StopFlashPaneBell, + BackgroundJob::FlashTabBell(..) => BackgroundJobContext::FlashTabBell, + BackgroundJob::StopFlashTabBell(..) => BackgroundJobContext::StopFlashTabBell, BackgroundJob::Exit => BackgroundJobContext::Exit, } } @@ -128,6 +136,8 @@ pub(crate) fn background_jobs_main( let last_render_request: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None)); let pending_help_text_clear: Arc<Mutex<HashMap<ClientId, Instant>>> = Arc::new(Mutex::new(HashMap::new())); + let mut flashing_pane_bells: HashMap<PaneId, Arc<AtomicBool>> = HashMap::new(); + let mut flashing_tab_bells: HashMap<usize, Arc<AtomicBool>> = HashMap::new(); let http_client = HttpClient::builder() // TODO: timeout? @@ -557,6 +567,68 @@ pub(crate) fn background_jobs_main( }); } }, + BackgroundJob::FlashPaneBell(pane_ids) => { + let is_flashing = Arc::new(AtomicBool::new(true)); + for &pane_id in &pane_ids { + flashing_pane_bells.insert(pane_id, is_flashing.clone()); + } + runtime.spawn({ + let senders = bus.senders.clone(); + let pane_ids_clone = pane_ids.clone(); + let flag = is_flashing.clone(); + async move { + let _ = senders.send_to_screen( + ScreenInstruction::AddHighlightPaneFrameColorOverride( + pane_ids_clone.clone(), + None, + ), + ); + tokio::time::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)) + .await; + if flag.load(Ordering::SeqCst) { + let _ = senders.send_to_screen( + ScreenInstruction::ClearPaneFrameColorOverride(pane_ids_clone), + ); + } + } + }); + }, + BackgroundJob::StopFlashPaneBell(pane_ids) => { + for &pane_id in &pane_ids { + if let Some(flag) = flashing_pane_bells.remove(&pane_id) { + flag.store(false, Ordering::SeqCst); + } + } + let _ = bus + .senders + .send_to_screen(ScreenInstruction::ClearPaneFrameColorOverride(pane_ids)); + }, + BackgroundJob::FlashTabBell(tab_id) => { + let is_flashing = Arc::new(AtomicBool::new(true)); + flashing_tab_bells.insert(tab_id, is_flashing.clone()); + runtime.spawn({ + let senders = bus.senders.clone(); + let flag = is_flashing.clone(); + async move { + let _ = senders + .send_to_screen(ScreenInstruction::SetTabBellFlash(tab_id, true)); + tokio::time::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)) + .await; + if flag.load(Ordering::SeqCst) { + let _ = senders + .send_to_screen(ScreenInstruction::SetTabBellFlash(tab_id, false)); + } + } + }); + }, + BackgroundJob::StopFlashTabBell(tab_id) => { + if let Some(flag) = flashing_tab_bells.remove(&tab_id) { + flag.store(false, Ordering::SeqCst); + } + let _ = bus + .senders + .send_to_screen(ScreenInstruction::SetTabBellFlash(tab_id, false)); + }, BackgroundJob::Exit => { for loading_plugin in loading_plugins.values() { loading_plugin.store(false, Ordering::SeqCst); diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 3001fba6..fc699240 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -405,6 +405,7 @@ impl SessionMetaData { .advanced_mouse_actions .unwrap_or(true), mouse_hover_effects: new_config.options.mouse_hover_effects.unwrap_or(true), + visual_bell: new_config.options.visual_bell.unwrap_or(true), }) .unwrap(); self.senders diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 04563b2a..092e5689 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1228,11 +1228,6 @@ impl Grid { } } } - if self.ring_bell { - let ring_bell = '\u{7}'; - raw_vte_output.push(ring_bell); - self.ring_bell = false; - } return Ok(Some(( character_chunks, Some(raw_vte_output), diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 87fac0ae..db32a88a 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -148,6 +148,7 @@ pub struct TerminalPane { banner: Option<String>, // a banner to be rendered inside this TerminalPane, used for panes // held on startup and can possibly be used to display some errors pane_frame_color_override: Option<(PaletteColor, Option<String>)>, + has_bell_notification: bool, invoked_with: Option<Run>, #[allow(dead_code)] arrow_fonts: bool, @@ -347,13 +348,7 @@ impl Pane for TerminalPane { ) -> Result<Option<(Vec<CharacterChunk>, Option<String>)>> { let err_context = || format!("failed to render frame for client {client_id}"); // TODO: remove the cursor stuff from here - let pane_title = if let Some(text_color_override) = self - .pane_frame_color_override - .as_ref() - .and_then(|(_color, text)| text.as_ref()) - { - text_color_override.into() - } else if self.pane_name.is_empty() + let normal_title = if self.pane_name.is_empty() && input_mode == InputMode::RenamePane && frame_params.is_main_client { @@ -391,6 +386,17 @@ impl Pane for TerminalPane { } else { self.pane_name.clone() }; + let pane_title = if let Some(text_color_override) = self + .pane_frame_color_override + .as_ref() + .and_then(|(_color, text)| text.as_ref()) + { + text_color_override.into() + } else if self.has_bell_notification { + format!("{} [!]", normal_title) + } else { + normal_title + }; let frame_geom = self.current_geom(); let is_pinned = frame_geom.is_pinned; @@ -772,6 +778,18 @@ impl Pane for TerminalPane { } self.set_should_render(true); } + fn has_bell(&self) -> bool { + self.grid.ring_bell + } + fn consume_bell(&mut self) { + self.grid.ring_bell = false; + } + fn set_bell_notification(&mut self, val: bool) { + self.has_bell_notification = val; + } + fn get_bell_notification(&self) -> bool { + self.has_bell_notification + } fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) { self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text)); } @@ -781,7 +799,7 @@ impl Pane for TerminalPane { _client_id: Option<ClientId>, ) { // TODO: if we have a client_id, we should only highlight the frame for this client - self.pane_frame_color_override = Some((self.style.colors.frame_highlight.base, text)); + self.pane_frame_color_override = Some((self.style.colors.frame_highlight.emphasis_0, text)); } fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) { // TODO: if we have a client_id, we should only clear the highlight for this client @@ -999,6 +1017,7 @@ impl TerminalPane { is_held: None, banner: None, pane_frame_color_override: None, + has_bell_notification: false, invoked_with, arrow_fonts, notification_end, diff --git a/zellij-server/src/panes/unit/terminal_pane_tests.rs b/zellij-server/src/panes/unit/terminal_pane_tests.rs index 3fecdf5d..2e34b273 100644 --- a/zellij-server/src/panes/unit/terminal_pane_tests.rs +++ b/zellij-server/src/panes/unit/terminal_pane_tests.rs @@ -2,7 +2,7 @@ use super::super::TerminalPane; use crate::panes::sixel::SixelImageStore; use crate::panes::LinkHandler; use crate::tab::Pane; -use ::insta::assert_snapshot; +use insta::assert_snapshot; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -704,6 +704,79 @@ pub fn pane_with_bottom_and_right_borders_position_is_on_frame() { assert!(!terminal_pane.position_is_on_frame(&Position::new(30, 131))); } +fn make_terminal_pane_for_bell() -> TerminalPane { + let mut fake_win_size = PaneGeom::default(); + fake_win_size.cols.set_inner(121); + fake_win_size.rows.set_inner(20); + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + TerminalPane::new( + 1, + fake_win_size, + Style::default(), + 0, + String::new(), + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), + sixel_image_store, + terminal_emulator_colors, + terminal_emulator_color_codes, + None, + None, + false, + true, + true, + true, + false, + None, + ) +} + +#[test] +pub fn bell_notification_state_set_and_cleared() { + let mut terminal_pane = make_terminal_pane_for_bell(); + + assert!( + !terminal_pane.get_bell_notification(), + "Initially no bell notification" + ); + + terminal_pane.set_bell_notification(true); + assert!( + terminal_pane.get_bell_notification(), + "Bell notification should be set" + ); + + terminal_pane.set_bell_notification(false); + assert!( + !terminal_pane.get_bell_notification(), + "Bell notification should be cleared" + ); +} + +#[test] +pub fn has_bell_reflects_grid_ring_bell() { + let mut terminal_pane = make_terminal_pane_for_bell(); + + assert!( + !terminal_pane.has_bell(), + "Initially has_bell should be false" + ); + + terminal_pane.handle_pty_bytes(vec![7u8]); + assert!( + terminal_pane.has_bell(), + "has_bell should be true after pty bell byte" + ); + + terminal_pane.consume_bell(); + assert!( + !terminal_pane.has_bell(), + "has_bell should be false after consume_bell" + ); +} + #[test] pub fn frameless_pane_position_is_on_frame() { let mut fake_win_size = PaneGeom { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c04cbf29..fe50546a 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -434,6 +434,7 @@ pub enum ScreenInstruction { SearchToggleWrap(ClientId, Option<NotificationEnd>), AddRedPaneFrameColorOverride(Vec<PaneId>, Option<String>), // Option<String> => optional error text ClearPaneFrameColorOverride(Vec<PaneId>), + SetTabBellFlash(usize, bool), // tab_id, is_flashing PreviousSwapLayout(ClientId, Option<NotificationEnd>), NextSwapLayout(ClientId, Option<NotificationEnd>), OverrideLayout( @@ -593,6 +594,7 @@ pub enum ScreenInstruction { default_editor: Option<PathBuf>, advanced_mouse_actions: bool, mouse_hover_effects: bool, + visual_bell: bool, }, RerunCommandPane(u32, Option<NotificationEnd>), // u32 - terminal pane id ResizePaneWithId(ResizeStrategy, PaneId), @@ -807,6 +809,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ClearPaneFrameColorOverride(..) => { ScreenContext::ClearPaneFrameColorOverride }, + ScreenInstruction::SetTabBellFlash(..) => ScreenContext::SetTabBellFlash, ScreenInstruction::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout, ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout, ScreenInstruction::OverrideLayout(..) => ScreenContext::OverrideLayout, @@ -1097,6 +1100,7 @@ pub(crate) struct Screen { current_pane_group: Rc<RefCell<PaneGroups>>, advanced_mouse_actions: bool, mouse_hover_effects: bool, + visual_bell: bool, currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>, // the below are the configured values - the ones that will be set if and when the web server // is brought online @@ -1138,6 +1142,7 @@ impl Screen { web_sharing: WebSharing, advanced_mouse_actions: bool, mouse_hover_effects: bool, + visual_bell: bool, web_server_ip: IpAddr, web_server_port: u16, ) -> Self { @@ -1192,6 +1197,7 @@ impl Screen { currently_marking_pane_group: Rc::new(RefCell::new(HashMap::new())), advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, render_blocker: RenderBlocker::new(100), @@ -1332,6 +1338,7 @@ impl Screen { 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) @@ -1363,6 +1370,7 @@ impl Screen { self.active_tab_ids.insert(client_id, new_tab_index); }, } + self.clear_bell_for_focused_pane(client_id); } /// A helper function to switch to a new tab at specified position. @@ -1445,6 +1453,16 @@ impl Screen { .non_fatal(); } + // Clear tab bell notification for the newly active tab + if let Some(new_tab) = self.tabs.get_mut(&new_tab_index) { + let tab_id = new_tab.id; + new_tab.clear_tab_bell_notification(); + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); + } + self.log_and_report_session_state() .with_context(err_context)?; return self.render(None).with_context(err_context); @@ -1749,7 +1767,61 @@ impl Screen { .send_to_plugin(PluginInstruction::PaneRenderReport(pane_render_report)); non_watcher_output_was_dirty = output.is_dirty(); - if non_watcher_output_was_dirty { + + let mut bell_state_changed = false; + let mut has_bell = false; + + if self.visual_bell { + let mut panes_to_flash: Vec<PaneId> = vec![]; + let mut tabs_to_flash: Vec<usize> = vec![]; + + let active_tab_ids_snapshot: Vec<usize> = + self.active_tab_ids.values().copied().collect(); + + for tab in self.tabs.values_mut() { + let is_active = active_tab_ids_snapshot.contains(&tab.id); + let (new_panes, tab_newly_set) = + tab.check_and_handle_bell_notifications(is_active); + if !new_panes.is_empty() { + panes_to_flash.extend(new_panes); + bell_state_changed = true; + } + if tab_newly_set { + tabs_to_flash.push(tab.id); + bell_state_changed = true; + } + } + + has_bell = !panes_to_flash.is_empty() || !tabs_to_flash.is_empty(); + if !panes_to_flash.is_empty() { + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::FlashPaneBell(panes_to_flash)); + } + for tab_id in tabs_to_flash { + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::FlashTabBell(tab_id)); + } + } else { + // visual_bell disabled: still detect bell for ANSI BEL forwarding only + for tab in self.tabs.values_mut() { + if tab.check_and_consume_bells_without_visual_notification() { + has_bell = true; + } + } + } + + if has_bell { + output.add_post_vte_instruction_to_multiple_clients( + self.active_tab_ids.keys().copied(), + "\u{7}", // ANSI BEL + ); + } + + if non_watcher_output_was_dirty || has_bell { let serialized_output = output.serialize().context(err_context)?; let _ = self .bus @@ -1757,6 +1829,10 @@ impl Screen { .send_to_server(ServerInstruction::Render(Some(serialized_output))) .context(err_context); } + + if bell_state_changed { + self.log_and_report_session_state()?; + } } else { // No regular clients, output is not dirty non_watcher_output_was_dirty = false; @@ -1909,6 +1985,61 @@ impl Screen { self.get_tabs_mut().get_mut(&tab_index) } + /// Clear bell notification for the currently focused pane of the given client. + /// Also cancels any running flash jobs if applicable. + pub fn clear_bell_for_focused_pane(&mut self, client_id: ClientId) { + let tab_id_and_pane_id: Option<(usize, PaneId)> = + self.get_active_tab_mut(client_id).ok().and_then(|tab| { + tab.get_active_pane_id(client_id) + .map(|pane_id| (tab.id, pane_id)) + }); + if let Some((tab_id, focused_pane_id)) = tab_id_and_pane_id { + if let Some(tab) = self.tabs.get_mut(&tab_id) { + if tab.panes_with_pending_bell.contains(&focused_pane_id) { + let tab_had_bell = tab.tab_has_pending_bell; + tab.clear_bell_notification_for_pane(focused_pane_id); + let tab_bell_now_cleared = tab_had_bell && !tab.tab_has_pending_bell; + let _ = + self.bus + .senders + .send_to_background_jobs(BackgroundJob::StopFlashPaneBell(vec![ + focused_pane_id, + ])); + if tab_bell_now_cleared { + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); + } + } + } + } + } + + /// Clear bell notification for a specific pane ID in the given client's active tab. + pub fn clear_bell_for_pane_id(&mut self, pane_id: PaneId, client_id: ClientId) { + let tab_id: Option<usize> = self.get_active_tab_mut(client_id).ok().map(|tab| tab.id); + if let Some(tab_id) = tab_id { + if let Some(tab) = self.tabs.get_mut(&tab_id) { + if tab.panes_with_pending_bell.contains(&pane_id) { + let tab_had_bell = tab.tab_has_pending_bell; + tab.clear_bell_notification_for_pane(pane_id); + let tab_bell_now_cleared = tab_had_bell && !tab.tab_has_pending_bell; + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::StopFlashPaneBell(vec![pane_id])); + if tab_bell_now_cleared { + let _ = self + .bus + .senders + .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); + } + } + } + } + } + pub fn show_floating_panes_in_tab( &mut self, client_id: ClientId, @@ -2291,6 +2422,10 @@ impl Screen { selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, + has_bell_notification: tab.tab_has_pending_bell + && !self.active_tab_ids.values().any(|i| i == &tab.id), + is_flashing_bell: tab.tab_bell_flash + && !self.active_tab_ids.values().any(|i| i == &tab.id), }; tab_infos_for_screen_state.insert(tab.position, tab_info_for_screen); } @@ -2332,6 +2467,8 @@ impl Screen { selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, + has_bell_notification: tab.tab_has_pending_bell && *active_tab_index != tab.id, + is_flashing_bell: tab.tab_bell_flash && *active_tab_index != tab.id, }; plugin_tab_updates.push(tab_info_for_plugins); } @@ -3450,6 +3587,7 @@ impl Screen { default_editor: Option<PathBuf>, advanced_mouse_actions: bool, mouse_hover_effects: bool, + visual_bell: bool, client_id: ClientId, ) -> Result<()> { let should_support_arrow_fonts = !simplified_ui; @@ -3466,6 +3604,7 @@ impl Screen { self.draw_pane_frames = pane_frames; self.advanced_mouse_actions = advanced_mouse_actions; self.mouse_hover_effects = mouse_hover_effects; + self.visual_bell = visual_bell; self.default_mode_info .update_arrow_fonts(should_support_arrow_fonts); self.default_mode_info @@ -3976,6 +4115,10 @@ impl Screen { selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, + has_bell_notification: tab.tab_has_pending_bell + && !self.active_tab_ids.values().any(|i| i == &tab.id), + is_flashing_bell: tab.tab_bell_flash + && !self.active_tab_ids.values().any(|i| i == &tab.id), } }) } @@ -4178,6 +4321,7 @@ pub(crate) fn screen_thread_main( let web_sharing = config_options.web_sharing.unwrap_or_else(Default::default); let advanced_mouse_actions = config_options.advanced_mouse_actions.unwrap_or(true); let mouse_hover_effects = config_options.mouse_hover_effects.unwrap_or(true); + let visual_bell = config_options.visual_bell.unwrap_or(true); let thread_senders = bus.senders.clone(); let mut screen = Screen::new( @@ -4216,6 +4360,7 @@ pub(crate) fn screen_thread_main( web_sharing, advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, ); @@ -4426,6 +4571,7 @@ pub(crate) fn screen_thread_main( ScreenInstruction::ToggleFloatingPanes(client_id, default_shell, completion_tx) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_floating_panes(Some(client_id), default_shell, completion_tx), ?); + screen.clear_bell_for_focused_pane(client_id); screen.log_and_report_session_state()?; screen.render(None)?; @@ -4602,6 +4748,7 @@ pub(crate) fn screen_thread_main( |tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id), ? ); + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -4612,6 +4759,7 @@ pub(crate) fn screen_thread_main( // waiting for it ) => { screen.move_focus_left_or_previous_tab(client_id)?; + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -4627,6 +4775,7 @@ pub(crate) fn screen_thread_main( |tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id), ? ); + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -4642,6 +4791,7 @@ pub(crate) fn screen_thread_main( |tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id), ? ); + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -4652,6 +4802,7 @@ pub(crate) fn screen_thread_main( // waiting for it ) => { screen.move_focus_right_or_next_tab(client_id)?; + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -4667,6 +4818,7 @@ pub(crate) fn screen_thread_main( |tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id), ? ); + screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; @@ -5788,6 +5940,14 @@ pub(crate) fn screen_thread_main( } screen.render(None)?; }, + ScreenInstruction::SetTabBellFlash(tab_id, is_flashing) => { + if let Some(tab) = screen.tabs.values_mut().find(|t| t.id == tab_id) { + tab.tab_bell_flash = is_flashing; + tab.clear_tab_bell_ring(); + } + screen.render(None)?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::PreviousSwapLayout( client_id, _completion_tx, // the action ends here, dropping this will release anything @@ -6557,6 +6717,7 @@ pub(crate) fn screen_thread_main( should_be_in_place_if_hidden, client_id, )?; + screen.clear_bell_for_pane_id(pane_id, client_id); screen.log_and_report_session_state()?; }, ScreenInstruction::RenamePane( @@ -6905,6 +7066,7 @@ pub(crate) fn screen_thread_main( default_editor, advanced_mouse_actions, mouse_hover_effects, + visual_bell, } => { screen .reconfigure( @@ -6924,6 +7086,7 @@ pub(crate) fn screen_thread_main( default_editor, advanced_mouse_actions, mouse_hover_effects, + visual_bell, client_id, ) .non_fatal(); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index c824ab92..2fa9996f 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -216,6 +216,10 @@ pub(crate) struct Tab { // is brought online web_server_ip: IpAddr, web_server_port: u16, + pub panes_with_pending_bell: HashSet<PaneId>, + pub tab_has_pending_bell: bool, + pub tab_bell_flash: bool, // currently in mid-notification-flash + pub tab_bell_ring: bool, // need to send ANSI BEL to the controlling terminal } // FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication @@ -540,6 +544,14 @@ pub trait Pane { fn hold(&mut self, _exit_status: Option<i32>, _is_first_run: bool, _run_command: RunCommand) { // No-op by default, only terminal panes support holding } + fn has_bell(&self) -> bool { + false + } + fn consume_bell(&mut self) {} + fn set_bell_notification(&mut self, _val: bool) {} + fn get_bell_notification(&self) -> bool { + false + } fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>); fn add_highlight_pane_frame_color_override( &mut self, @@ -772,6 +784,10 @@ impl Tab { connected_clients_in_app, web_server_ip, web_server_port, + panes_with_pending_bell: HashSet::new(), + tab_has_pending_bell: false, + tab_bell_flash: false, + tab_bell_ring: false, } } @@ -2427,6 +2443,85 @@ impl Tab { None } } + pub fn check_and_handle_bell_notifications( + &mut self, + is_active_tab: bool, + ) -> (Vec<PaneId>, bool) { + let mut newly_notified_panes = vec![]; + let mut tab_bell_newly_set = false; + + let focused_pane_ids: HashSet<PaneId> = self + .connected_clients + .borrow() + .iter() + .filter_map(|c_id| self.get_active_pane_id(*c_id)) + .collect(); + + // Collect ringing pane IDs first (immutable borrow) + let ringing_panes: Vec<PaneId> = self + .tiled_panes + .get_panes() + .chain(self.floating_panes.get_panes()) + .filter(|(_, pane)| pane.has_bell()) + .map(|(pane_id, _)| *pane_id) + .collect(); + + for pane_id in ringing_panes { + // Consume the bell from the pane + if let Some(pane) = self.get_pane_with_id_mut(pane_id) { + pane.consume_bell(); + } + let is_focused = focused_pane_ids.contains(&pane_id); + if !is_focused && !self.panes_with_pending_bell.contains(&pane_id) { + if let Some(pane) = self.get_pane_with_id_mut(pane_id) { + pane.set_bell_notification(true); + } + self.panes_with_pending_bell.insert(pane_id); + newly_notified_panes.push(pane_id); + } + if !self.tab_bell_ring { + self.tab_bell_ring = true; + tab_bell_newly_set = true; + } + if !is_active_tab { + self.tab_has_pending_bell = true; + } + } + (newly_notified_panes, tab_bell_newly_set) + } + pub fn clear_bell_notification_for_pane(&mut self, pane_id: PaneId) { + self.panes_with_pending_bell.remove(&pane_id); + if let Some(pane) = self.get_pane_with_id_mut(pane_id) { + pane.set_bell_notification(false); + pane.clear_pane_frame_color_override(None); + } + self.tab_has_pending_bell = false; + } + pub fn clear_tab_bell_notification(&mut self) { + self.tab_has_pending_bell = false; + } + pub fn clear_tab_bell_ring(&mut self) { + self.tab_bell_ring = false; + } + /// Checks if any pane in the tab has a pending bell, consumes all such bells, and returns + /// whether any were found. Does not update notification state (used when visual_bell is + /// disabled but ANSI BEL forwarding is still desired). + pub fn check_and_consume_bells_without_visual_notification(&mut self) -> bool { + let ringing_panes: Vec<PaneId> = self + .tiled_panes + .get_panes() + .chain(self.floating_panes.get_panes()) + .filter(|(_, pane)| pane.has_bell()) + .map(|(pane_id, _)| *pane_id) + .collect(); + let had_bell = !ringing_panes.is_empty(); + for pane_id in ringing_panes { + if let Some(pane) = self.get_pane_with_id_mut(pane_id) { + pane.consume_bell(); + } + } + had_bell + } pub fn has_terminal_pid(&self, pid: u32) -> bool { self.tiled_panes.panes_contain(&PaneId::Terminal(pid)) || self.floating_panes.panes_contain(&PaneId::Terminal(pid)) diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 05210c0d..b299439c 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -15245,3 +15245,84 @@ fn get_pane_z_index_returns_none_for_nonexistent_pane() { "Different floating panes should have different z-indices" ); } + +#[test] +pub fn bell_in_unfocused_pane_sets_notification() { + let size = Size { + cols: 121, + rows: 20, + }; + let stacked_resize = true; + let mut tab = create_new_tab(size, stacked_resize); + let new_pane_id = PaneId::Terminal(2); + let client_id = 1; + + // Create a second pane; client is focused on pane 1 (PaneId::Terminal(1)) + tab.horizontal_split(new_pane_id, None, client_id, None, None) + .unwrap(); + // Move focus back to pane 1 + tab.move_focus_up(client_id).unwrap(); + + // Simulate bell in pane 2 via pty bytes (\x07) + tab.handle_pty_bytes(2, vec![7u8]).unwrap(); + + // Now call check_and_handle_bell_notifications as non-active tab + let (new_panes, tab_newly_set) = tab.check_and_handle_bell_notifications(false); + + assert!( + new_panes.contains(&new_pane_id), + "Pane 2 should be in new_panes" + ); + assert!( + tab.panes_with_pending_bell.contains(&new_pane_id), + "Pane 2 should be in panes_with_pending_bell" + ); + assert!( + tab.tab_has_pending_bell, + "tab_has_pending_bell should be true" + ); + assert!(tab_newly_set, "tab_bell_newly_set should be true"); + assert!( + tab.get_pane_with_id(new_pane_id) + .map(|p| p.get_bell_notification()) + .unwrap_or(false), + "Pane 2 should have bell notification" + ); +} + +#[test] +pub fn clearing_last_pane_bell_clears_tab_bell() { + let size = Size { + cols: 121, + rows: 20, + }; + let stacked_resize = true; + let mut tab = create_new_tab(size, stacked_resize); + let new_pane_id = PaneId::Terminal(2); + let client_id = 1; + + tab.horizontal_split(new_pane_id, None, client_id, None, None) + .unwrap(); + tab.move_focus_up(client_id).unwrap(); + + // Set bell on unfocused pane 2 via check_and_handle_bell_notifications + tab.handle_pty_bytes(2, vec![7u8]).unwrap(); + tab.check_and_handle_bell_notifications(false); + + assert!( + tab.tab_has_pending_bell, + "tab_has_pending_bell should be set before clearing" + ); + + // Clear bell for pane 2 + tab.clear_bell_notification_for_pane(new_pane_id); + + assert!( + tab.panes_with_pending_bell.is_empty(), + "panes_with_pending_bell should be empty after clearing" + ); + assert!( + !tab.tab_has_pending_bell, + "tab_has_pending_bell should be false after last pane bell cleared" + ); +} diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 9ab7ec14..6bd1db8c 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -283,6 +283,7 @@ fn create_new_screen( let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; + let visual_bell = true; let screen = Screen::new( bus, &client_attributes, @@ -310,6 +311,7 @@ fn create_new_screen( web_sharing, advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, ); diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap index c1669de6..8cadb9e1 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap @@ -32,6 +32,8 @@ Some( selectable_tiled_panes_count: 2, selectable_floating_panes_count: 0, tab_id: 0, + has_bell_notification: false, + is_flashing_bell: false, }, ], ), diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap index f2365e71..2ad7494d 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap @@ -32,6 +32,8 @@ Some( selectable_tiled_panes_count: 2, selectable_floating_panes_count: 0, tab_id: 0, + has_bell_notification: false, + is_flashing_bell: false, }, ], ), diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 3fac82af..3bab18af 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -589,6 +589,10 @@ pub struct TabInfo { pub selectable_floating_panes_count: u32, #[prost(uint32, tag="17")] pub tab_id: u32, + #[prost(bool, tag="18")] + pub has_bell_notification: bool, + #[prost(bool, tag="19")] + pub is_flashing_bell: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index c61229e9..79a8b6b3 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -1799,6 +1799,8 @@ pub struct Options { pub mouse_hover_effects: ::core::option::Option<bool>, #[prost(uint64, optional, tag="42")] pub client_async_worker_tasks: ::core::option::Option<u64>, + #[prost(bool, optional, tag="43")] + pub visual_bell: ::core::option::Option<bool>, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index 46f1b812..03602019 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -1098,6 +1098,7 @@ message Options { optional bool osc8_hyperlinks = 40; optional bool mouse_hover_effects = 41; optional uint64 client_async_worker_tasks = 42; + optional bool visual_bell = 43; } enum OnForceClose { diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 74dfdfe7..66cf0815 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -2192,6 +2192,10 @@ pub struct TabInfo { pub selectable_floating_panes_count: usize, /// The stable identifier for this tab pub tab_id: usize, + /// Whether this tab has an active (persistent) bell notification + pub has_bell_notification: bool, + /// Whether this tab is currently flashing its bell (transient 400ms state) + pub is_flashing_bell: bool, } /// The `PaneManifest` contains a dictionary of panes, indexed by the tab position (0 indexed). diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 6c85ad4e..2085e913 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -327,6 +327,7 @@ pub enum ScreenContext { SearchToggleWrap, AddRedPaneFrameColorOverride, ClearPaneFrameColorOverride, + SetTabBellFlash, PreviousSwapLayout, NextSwapLayout, OverrideLayout, @@ -581,6 +582,10 @@ pub enum BackgroundJobContext { HighlightPanesWithMessage, QueryZellijWebServerStatus, ClearHelpText, + FlashPaneBell, + StopFlashPaneBell, + FlashTabBell, + StopFlashTabBell, Exit, } diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index ccba4d9c..dfbe089e 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -231,6 +231,12 @@ pub struct Options { #[serde(default)] pub mouse_hover_effects: Option<bool>, + /// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix) + /// default is true + #[clap(long, value_parser)] + #[serde(default)] + pub visual_bell: Option<bool>, + // these are intentionally excluded from the CLI options as they must be specified in the // configuration file pub web_server_ip: Option<IpAddr>, @@ -336,6 +342,7 @@ impl Options { let show_release_notes = other.show_release_notes.or(self.show_release_notes); let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions); let mouse_hover_effects = other.mouse_hover_effects.or(self.mouse_hover_effects); + let visual_bell = other.visual_bell.or(self.visual_bell); let web_server_ip = other.web_server_ip.or(self.web_server_ip); let web_server_port = other.web_server_port.or(self.web_server_port); let web_server_cert = other @@ -388,6 +395,7 @@ impl Options { show_release_notes, advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, web_server_cert, @@ -461,6 +469,7 @@ impl Options { let show_release_notes = other.show_release_notes.or(self.show_release_notes); let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions); let mouse_hover_effects = other.mouse_hover_effects.or(self.mouse_hover_effects); + let visual_bell = other.visual_bell.or(self.visual_bell); let web_server_ip = other.web_server_ip.or(self.web_server_ip); let web_server_port = other.web_server_port.or(self.web_server_port); let web_server_cert = other @@ -513,6 +522,7 @@ impl Options { show_release_notes, advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, web_server_cert, diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index b6bfbf9c..91f64070 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -615,6 +615,7 @@ impl From<crate::input::options::Options> enforce_https_for_localhost: options.enforce_https_for_localhost, post_command_discovery_hook: options.post_command_discovery_hook, client_async_worker_tasks: options.client_async_worker_tasks.map(|v| v as u64), + visual_bell: options.visual_bell, } } } @@ -707,6 +708,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options> enforce_https_for_localhost: options.enforce_https_for_localhost, post_command_discovery_hook: options.post_command_discovery_hook, client_async_worker_tasks: options.client_async_worker_tasks.map(|v| v as usize), + visual_bell: options.visual_bell, }) } } diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 8cd0cc84..233a0e5d 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -473,6 +473,7 @@ fn test_client_messages() { post_command_discovery_hook: Some("post_command_discovery_hook".to_owned()), client_async_worker_tasks: Some(16), mouse_hover_effects: Some(false), + visual_bell: Some(true), }), layout: None, terminal_window_size: Size { rows: 80, cols: 42 }, diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index ced75920..12eadff6 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -2781,6 +2781,8 @@ impl Options { }, None => None, }; + let visual_bell = + kdl_property_first_arg_as_bool_or_error!(kdl_options, "visual_bell").map(|(v, _)| v); Ok(Options { simplified_ui, @@ -2818,6 +2820,7 @@ impl Options { show_release_notes, advanced_mouse_actions, mouse_hover_effects, + visual_bell, web_server_ip, web_server_port, web_server_cert, @@ -3949,6 +3952,33 @@ impl Options { None } } + fn visual_bell_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { + let comment_text = format!( + "{}\n{}\n{}", + " ", + "// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix)", + "// default is true", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("visual_bell"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(visual_bell) = self.visual_bell { + let mut node = create_node(visual_bell); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(true); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } fn web_server_ip_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { let comment_text = format!( "{}\n{}\n{}\n{}", @@ -4183,6 +4213,9 @@ impl Options { if let Some(mouse_hover_effects) = self.mouse_hover_effects_to_kdl(add_comments) { nodes.push(mouse_hover_effects); } + if let Some(visual_bell) = self.visual_bell_to_kdl(add_comments) { + nodes.push(visual_bell); + } if let Some(web_server_ip) = self.web_server_ip_to_kdl(add_comments) { nodes.push(web_server_ip); } @@ -5659,6 +5692,8 @@ impl TabInfo { selectable_tiled_panes_count, selectable_floating_panes_count, tab_id, + has_bell_notification: false, + is_flashing_bell: false, }) } pub fn encode_to_kdl(&self) -> KdlDocument { @@ -6090,6 +6125,8 @@ fn serialize_and_deserialize_session_info_with_data() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 0, + is_flashing_bell: false, + has_bell_notification: false, }, TabInfo { position: 1, @@ -6109,6 +6146,8 @@ fn serialize_and_deserialize_session_info_with_data() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 1, + is_flashing_bell: false, + has_bell_notification: false, }, ], panes: PaneManifest { panes }, diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap index 26a1be67..da967bcb 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap @@ -536,6 +536,10 @@ web_client { // default is true // mouse_hover_effects false +// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix) +// default is true +// visual_bell true + // The ip address the web server should listen on when it starts // Default: "127.0.0.1" // (Requires restart) diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap index 18293ffb..e6a0ddda 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap @@ -256,6 +256,10 @@ web_sharing "disabled" // default is true // mouse_hover_effects false +// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix) +// default is true +// visual_bell true + // The ip address the web server should listen on when it starts // Default: "127.0.0.1" // (Requires restart) diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 5daf3ecd..5aa119a9 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -431,6 +431,8 @@ message TabInfo { uint32 selectable_tiled_panes_count = 15; uint32 selectable_floating_panes_count = 16; uint32 tab_id = 17; + bool has_bell_notification = 18; + bool is_flashing_bell = 19; } message ModeUpdatePayload { diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 431e5e0f..3b135b6d 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -1633,6 +1633,8 @@ impl TryFrom<ProtobufTabInfo> for TabInfo { selectable_floating_panes_count: protobuf_tab_info.selectable_floating_panes_count as usize, tab_id: protobuf_tab_info.tab_id as usize, + has_bell_notification: protobuf_tab_info.has_bell_notification, + is_flashing_bell: protobuf_tab_info.is_flashing_bell, }) } } @@ -1662,6 +1664,8 @@ impl TryFrom<TabInfo> for ProtobufTabInfo { selectable_tiled_panes_count: tab_info.selectable_tiled_panes_count as u32, selectable_floating_panes_count: tab_info.selectable_floating_panes_count as u32, tab_id: tab_info.tab_id as u32, + has_bell_notification: tab_info.has_bell_notification, + is_flashing_bell: tab_info.is_flashing_bell, }) } } @@ -2150,6 +2154,8 @@ fn serialize_tab_update_event_with_non_default_values() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 0, + has_bell_notification: false, + is_flashing_bell: false, }, TabInfo { position: 1, @@ -2169,6 +2175,8 @@ fn serialize_tab_update_event_with_non_default_values() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 1, + has_bell_notification: false, + is_flashing_bell: false, }, TabInfo::default(), ]); @@ -2443,6 +2451,8 @@ fn serialize_session_update_event_with_non_default_values() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 0, + has_bell_notification: false, + is_flashing_bell: false, }, TabInfo { position: 1, @@ -2462,6 +2472,8 @@ fn serialize_session_update_event_with_non_default_values() { selectable_tiled_panes_count: 10, selectable_floating_panes_count: 10, tab_id: 1, + has_bell_notification: false, + is_flashing_bell: false, }, TabInfo::default(), ]; diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap index 5c62f124..2522ab46 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap @@ -40,6 +40,7 @@ Options { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap index 7b18b100..9ad64f6f 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap @@ -40,6 +40,7 @@ Options { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap index 0561e8be..1c16a2fd 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap @@ -38,6 +38,7 @@ Options { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index d794c05f..1684ff37 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -6007,6 +6007,7 @@ Config { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 4151c52a..01603ece 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -6007,6 +6007,7 @@ Config { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap index eb5db32b..3ced7620 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap @@ -125,6 +125,7 @@ Config { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap index 78d7c05a..994932a4 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap @@ -40,6 +40,7 @@ Options { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index c7e71adf..ac52b53f 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -6007,6 +6007,7 @@ Config { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index b5211847..927928e0 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -6007,6 +6007,7 @@ Config { show_release_notes: None, advanced_mouse_actions: None, mouse_hover_effects: None, + visual_bell: None, web_server_ip: None, web_server_port: None, web_server_cert: None, |
