aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-03 08:20:04 +0100
committerGitHub <noreply@github.com>2026-03-03 08:20:04 +0100
commitf618b0653abc918f9c34b5a4e8a06e4a11745b43 (patch)
treeb2a1cea091bd395fda4843733a081d1c87180823
parentfeat(web): improve web client ui on mobile (#4723) (diff)
downloadsl0ppij-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
-rw-r--r--CHANGELOG.md1
-rw-r--r--default-plugins/compact-bar/src/tab.rs11
-rw-r--r--default-plugins/tab-bar/src/tab.rs11
-rw-r--r--zellij-server/src/background_jobs.rs72
-rw-r--r--zellij-server/src/lib.rs1
-rw-r--r--zellij-server/src/panes/grid.rs5
-rw-r--r--zellij-server/src/panes/terminal_pane.rs35
-rw-r--r--zellij-server/src/panes/unit/terminal_pane_tests.rs75
-rw-r--r--zellij-server/src/screen.rs165
-rw-r--r--zellij-server/src/tab/mod.rs95
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs81
-rw-r--r--zellij-server/src/unit/screen_tests.rs2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap2
-rw-r--r--zellij-utils/assets/prost/api.event.rs4
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs2
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto1
-rw-r--r--zellij-utils/src/data.rs4
-rw-r--r--zellij-utils/src/errors.rs5
-rw-r--r--zellij-utils/src/input/options.rs10
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs2
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs1
-rw-r--r--zellij-utils/src/kdl/mod.rs39
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap4
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap4
-rw-r--r--zellij-utils/src/plugin_api/event.proto2
-rw-r--r--zellij-utils/src/plugin_api/event.rs12
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap1
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,