From 0ea25f9125b0ba4cb9a1d912691706e407fb5dde Mon Sep 17 00:00:00 2001 From: HoneyHazard <8847050+HoneyHazard@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:23:45 -0700 Subject: [PATCH 1/2] Add show_dividers option to draw a line between items Item rows already have a 2-row gap between them (NodeWidget::spacing()/ DeviceWidget::spacing()), but that space is always blank. On a list with many similarly-styled rows it can be hard to tell at a glance where one item ends and the next begins, especially at a glance or on a low-contrast theme. Adds show_dividers: bool (default false) plus a new divider theme key and divider char_set glyph. When enabled, draws a one-row line spanning the item's width in the gap immediately below it, styled by theme.divider and built from char_set.divider (a single glyph, repeated to fill the width - same pattern as volume_filled/volume_empty). No divider is drawn after the last visible item, so it never touches the footer/tab bar. Since the divider only ever draws into the gap row that already exists between items, turning it on never changes the list's layout or item spacing - only what's drawn in space that was already blank. Off by default, so it's a no-op for any existing config. Both the Playback/Recording/Output/Input Devices node list (NodeWidget-based) and the Configuration tab's device list (DeviceWidget-based) go through the same object_list.rs render loop, so a single render_divider() helper covers both list kinds. Tested: cargo test --release (147/147 passing, including 3 new tests covering the disabled no-op case, the enabled case drawing the glyph across the full width, and clipping at the list's bottom edge without panicking). cargo fmt --check, cargo clippy -- -D warnings, and cargo doc (all matching wiremix's CI) clean. Manually verified in tmux with --show-dividers that the divider line renders between every pair of adjacent items without disturbing layout, and that leaving the option unset reproduces stock behavior exactly. --- src/app.rs | 2 + src/config.rs | 20 ++++++++ src/config/char_set.rs | 5 ++ src/config/theme.rs | 5 ++ src/object_list.rs | 106 ++++++++++++++++++++++++++++++++++++++++- src/opt.rs | 8 ++++ wiremix.toml | 16 +++++++ 7 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6c973d2..f162fea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -882,6 +882,7 @@ mod tests { tabs: vec![TabKind::Playback], lazy_capture: Default::default(), filters: Default::default(), + show_dividers: Default::default(), }; let mut app = App::new(wirehose, event_rx, config); @@ -983,6 +984,7 @@ mod tests { ], lazy_capture: Default::default(), filters: Default::default(), + show_dividers: Default::default(), }; let mut app = App::new(&wirehose, event_rx, config); diff --git a/src/config.rs b/src/config.rs index ac871b0..0301c7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,7 @@ pub struct Config { pub peaks: Peaks, pub char_set: CharSet, pub theme: Theme, + pub show_dividers: bool, pub max_volume_percent: f32, pub enforce_max_volume: bool, pub keybindings: HashMap, @@ -64,6 +65,8 @@ struct ConfigFile { char_set: String, #[serde(default = "default_theme_name")] theme: String, + #[serde(default = "default_show_dividers")] + show_dividers: bool, #[serde(default = "default_max_volume_percent")] max_volume_percent: Option, #[serde(default = "default_enforce_max_volume")] @@ -151,6 +154,7 @@ pub struct CharSet { pub tab_marker_left: String, pub tab_marker_right: String, pub list_more: String, + pub divider: String, pub volume_empty: String, pub volume_filled: String, pub meter_left_inactive: String, @@ -181,6 +185,7 @@ pub struct Theme { pub tab_selected: Style, pub tab_marker: Style, pub list_more: Style, + pub divider: Style, pub node_title: Style, pub node_target: Style, pub volume: Style, @@ -270,6 +275,10 @@ fn default_lazy_capture() -> bool { false } +fn default_show_dividers() -> bool { + false +} + impl ConfigFile { /// Override configuration with command-line arguments. pub fn apply_opt(&mut self, opt: &Opt) { @@ -328,6 +337,14 @@ impl ConfigFile { if opt.lazy_capture { self.lazy_capture = true; } + + if opt.no_show_dividers { + self.show_dividers = false; + } + + if opt.show_dividers { + self.show_dividers = true; + } } } @@ -390,6 +407,7 @@ impl TryFrom for Config { enforce_max_volume: config_file.enforce_max_volume, char_set, theme, + show_dividers: config_file.show_dividers, keybindings: config_file.keybindings, help, names: config_file.names, @@ -469,6 +487,7 @@ pub mod strict { peaks: Option, char_set: String, theme: String, + show_dividers: bool, max_volume_percent: Option, enforce_max_volume: bool, #[serde(deserialize_with = "keybindings")] @@ -493,6 +512,7 @@ pub mod strict { peaks: strict.peaks, char_set: strict.char_set, theme: strict.theme, + show_dividers: strict.show_dividers, max_volume_percent: strict.max_volume_percent, enforce_max_volume: strict.enforce_max_volume, keybindings: strict.keybindings, diff --git a/src/config/char_set.rs b/src/config/char_set.rs index f6d8a2b..c979b28 100644 --- a/src/config/char_set.rs +++ b/src/config/char_set.rs @@ -21,6 +21,7 @@ pub struct CharSetOverlay { tab_marker_left: Option, tab_marker_right: Option, list_more: Option, + divider: Option, volume_empty: Option, volume_filled: Option, meter_left_inactive: Option, @@ -103,6 +104,7 @@ impl TryFrom for CharSet { validate_and_set!(tab_marker_left, 1); validate_and_set!(tab_marker_right, 1); validate_and_set!(list_more, 0); + validate_and_set!(divider, 1); validate_and_set!(volume_empty, 1); validate_and_set!(volume_filled, 1); validate_and_set!(meter_left_inactive, 1); @@ -143,6 +145,7 @@ impl Default for CharSet { tab_marker_left: String::from("["), tab_marker_right: String::from("]"), list_more: String::from("•••"), + divider: String::from("─"), volume_empty: String::from("╌"), volume_filled: String::from("━"), meter_left_inactive: String::from("▮"), @@ -184,6 +187,7 @@ impl CharSet { tab_marker_left: String::from("["), tab_marker_right: String::from("]"), list_more: String::from("•••"), + divider: String::from("─"), volume_empty: String::from("─"), volume_filled: String::from("━"), meter_left_inactive: String::from("┃"), @@ -215,6 +219,7 @@ impl CharSet { tab_marker_left: String::from("["), tab_marker_right: String::from("]"), list_more: String::from("~~~"), + divider: String::from("-"), volume_empty: String::from("-"), volume_filled: String::from("="), meter_left_inactive: String::from("="), diff --git a/src/config/theme.rs b/src/config/theme.rs index 6919078..149e405 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -17,6 +17,7 @@ pub struct ThemeOverlay { tab_selected: Option, tab_marker: Option, list_more: Option, + divider: Option, node_title: Option, node_target: Option, volume: Option, @@ -96,6 +97,7 @@ impl TryFrom for Theme { set!(tab_selected); set!(tab_marker); set!(list_more); + set!(divider); set!(node_title); set!(node_target); set!(volume); @@ -131,6 +133,7 @@ impl Default for Theme { tab_selected: Style::default().fg(Color::LightCyan), tab_marker: Style::default().fg(Color::LightCyan), list_more: Style::default().fg(Color::DarkGray), + divider: Style::default().fg(Color::DarkGray), node_title: Style::default(), node_target: Style::default(), volume: Style::default(), @@ -175,6 +178,7 @@ impl Theme { tab_selected: Style::default().add_modifier(Modifier::BOLD), tab_marker: Style::default().add_modifier(Modifier::BOLD), list_more: Style::default(), + divider: Style::default(), node_title: Style::default(), node_target: Style::default(), volume: Style::default(), @@ -208,6 +212,7 @@ impl Theme { tab_selected: Style::default(), tab_marker: Style::default(), list_more: Style::default(), + divider: Style::default(), node_title: Style::default(), node_target: Style::default(), volume: Style::default(), diff --git a/src/object_list.rs b/src/object_list.rs index c43947c..61a5f96 100644 --- a/src/object_list.rs +++ b/src/object_list.rs @@ -323,6 +323,39 @@ struct ObjectListRenderContext<'a> { objects_visible: usize, } +/// Draws a one-row divider line spanning `object_area`'s width, in the gap +/// immediately below it, when `config.show_dividers` is set. A no-op +/// (nothing drawn, no space reserved) when it isn't - dividers only ever +/// use space that the existing gap between items (NodeWidget::spacing()/ +/// DeviceWidget::spacing()) already reserves, so turning this on never +/// changes the list's layout, only what's drawn in space that was already +/// blank. Clipped to `list_area` so it can never bleed into a neighboring +/// item or past the list into the footer/tab bar. +fn render_divider( + buf: &mut Buffer, + config: &Config, + object_area: Rect, + list_area: Rect, +) { + if !config.show_dividers { + return; + } + + let divider_area = Rect { + x: object_area.x, + y: object_area.y.saturating_add(object_area.height), + width: object_area.width, + height: 1, + }; + let clipped = list_area.intersection(divider_area); + if clipped.is_empty() { + return; + } + + let line = config.char_set.divider.repeat(clipped.width as usize); + Line::from(Span::styled(line, config.theme.divider)).render(clipped, buf); +} + impl ObjectListWidget<'_, '_> { fn render_node_list( &mut self, @@ -342,7 +375,9 @@ impl ObjectListWidget<'_, '_> { let objects_and_areas: Vec<(&&view::Node, &Rect)> = objects.zip(context.objects_layout.iter()).collect(); - for (object, &object_area) in &objects_and_areas { + let last_index = objects_and_areas.len().saturating_sub(1); + for (i, (object, &object_area)) in objects_and_areas.iter().enumerate() + { let selected = self .object_list .selected @@ -355,6 +390,15 @@ impl ObjectListWidget<'_, '_> { selected, ) .render(object_area, buf, mouse_areas); + + if i < last_index { + render_divider( + buf, + self.config, + object_area, + context.list_area, + ); + } } // Show the target dropdown? @@ -399,7 +443,9 @@ impl ObjectListWidget<'_, '_> { let objects_and_areas: Vec<(&&view::Device, &Rect)> = objects.zip(context.objects_layout.iter()).collect(); - for (object, &object_area) in &objects_and_areas { + let last_index = objects_and_areas.len().saturating_sub(1); + for (i, (object, &object_area)) in objects_and_areas.iter().enumerate() + { let selected = self .object_list .selected @@ -410,6 +456,15 @@ impl ObjectListWidget<'_, '_> { buf, mouse_areas, ); + + if i < last_index { + render_divider( + buf, + self.config, + object_area, + context.list_area, + ); + } } // Show the target dropdown? @@ -1130,4 +1185,51 @@ mod tests { assert!(visible.contains(&stream_id)); assert!(visible.contains(&source_id)); } + + #[test] + fn render_divider_noop_when_disabled() { + let config = config::Config::from_toml_str("show_dividers = false"); + let object_area = Rect::new(0, 0, 10, 3); + let list_area = Rect::new(0, 0, 10, 10); + let mut buf = Buffer::empty(list_area); + + render_divider(&mut buf, &config, object_area, list_area); + + let divider_row = Rect::new(0, 3, 10, 1); + for cell in buf.content[buf.index_of(divider_row.x, divider_row.y) + ..buf.index_of(divider_row.x, divider_row.y) + 10] + .iter() + { + assert_eq!(cell.symbol(), " "); + } + } + + #[test] + fn render_divider_draws_when_enabled() { + let config = config::Config::from_toml_str("show_dividers = true"); + let object_area = Rect::new(0, 0, 10, 3); + let list_area = Rect::new(0, 0, 10, 10); + let mut buf = Buffer::empty(list_area); + + render_divider(&mut buf, &config, object_area, list_area); + + let divider_row_start = buf.index_of(0, 3); + for cell in + buf.content[divider_row_start..divider_row_start + 10].iter() + { + assert_eq!(cell.symbol(), config.char_set.divider); + } + } + + #[test] + fn render_divider_clips_to_list_area() { + let config = config::Config::from_toml_str("show_dividers = true"); + // object_area's bottom row falls just outside list_area's bottom + // edge - nothing should be drawn, and this must not panic. + let object_area = Rect::new(0, 8, 10, 3); + let list_area = Rect::new(0, 0, 10, 10); + let mut buf = Buffer::empty(list_area); + + render_divider(&mut buf, &config, object_area, list_area); + } } diff --git a/src/opt.rs b/src/opt.rs index 534d130..83527c3 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -78,6 +78,14 @@ pub struct Opt { #[clap(long, conflicts_with = "no_lazy_capture")] pub lazy_capture: bool, + /// Hide the divider line between items + #[clap(long, conflicts_with = "show_dividers")] + pub no_show_dividers: bool, + + /// Draw a divider line between items + #[clap(long, conflicts_with = "no_show_dividers")] + pub show_dividers: bool, + #[cfg(debug_assertions)] #[clap(short, long)] pub dump_events: bool, diff --git a/wiremix.toml b/wiremix.toml index 218653b..28aeb39 100644 --- a/wiremix.toml +++ b/wiremix.toml @@ -29,6 +29,12 @@ char_set = "default" # Theme to use (see Themes section) theme = "default" +# If true, draw a divider line in the gap between items, styled by the +# divider theme key and using the divider char_set glyph. Off by default - +# the gap between items already exists either way, so this only changes +# what's drawn in space that was already blank, never the list's layout. +show_dividers = false + # Initial tab tab = "playback" @@ -316,6 +322,9 @@ tab_selected = { fg = "LightCyan" } tab_marker = { fg = "LightCyan" } # The symbol at the top/bottom of a tab indicating that there are more items list_more = { fg = "DarkGray" } +# The divider line drawn in the gap between items, when show_dividers is +# true (see Main Options above). No effect otherwise. +divider = { fg = "DarkGray" } # The name of a PipeWire node node_title = { } # The name of the selected target for a node @@ -393,6 +402,9 @@ tab_marker_left = "[" tab_marker_right = "]" # Displayed at the top/bottom of a tab when there are more items list_more = "•••" +# The glyph repeated to draw the divider line between items, when +# show_dividers is true. Must be exactly 1 character wide. +divider = "─" # Volume bar volume_empty = "╌" volume_filled = "━" @@ -440,6 +452,7 @@ tab = { } tab_selected = { add_modifier = "BOLD" } tab_marker = { add_modifier = "BOLD" } list_more = { } +divider = { } node_title = { } node_target = { } volume = { } @@ -469,6 +482,7 @@ tab = { } tab_selected = { } tab_marker = { } list_more = { } +divider = { } node_title = { } node_target = { } volume = { } @@ -499,6 +513,7 @@ selector_bottom = "░" tab_marker_left = "[" tab_marker_right = "]" list_more = "•••" +divider = "─" volume_empty = "─" volume_filled = "━" meter_left_inactive = "┃" @@ -527,6 +542,7 @@ selector_bottom = "-" tab_marker_left = "[" tab_marker_right = "]" list_more = "~~~" +divider = "-" volume_empty = "-" volume_filled = "=" meter_left_inactive = "=" From e2a1096ea7e8e512e3893c2c53ec32c8681db65c Mon Sep 17 00:00:00 2001 From: HoneyHazard <8847050+HoneyHazard@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:45:02 -0700 Subject: [PATCH 2/2] Center dividers symmetrically instead of hugging the item above Dividers previously drew into the first row of the existing 2-row gap between items, immediately below whichever item sat above - one blank row below the divider, none above it. Now, whenever show_dividers is on, one extra row of spacing is reserved per item (2 -> 3) so the divider can sit on the middle row instead: one blank row above it, one below, symmetric with respect to both neighboring items. With show_dividers off (the default), spacing and the whole list's layout are completely untouched - same row count, same scroll/page math, same everything as before this change and as stock wiremix. ObjectList::update()/visible_objects() (and the private visible_count() they both rely on) now take a show_dividers bool so their scroll/ viewport math can agree with the extra row ObjectListWidget::render() reserves per item - previously this was implicit and only render() knew about it. --- src/app.rs | 13 +++-- src/object_list.rs | 131 ++++++++++++++++++++++++++++++++++----------- wiremix.toml | 10 ++-- 3 files changed, 116 insertions(+), 38 deletions(-) diff --git a/src/app.rs b/src/app.rs index f162fea..06c4a46 100644 --- a/src/app.rs +++ b/src/app.rs @@ -291,10 +291,17 @@ impl<'a> App<'a> { self.state_dirty = false; let frame = terminal.get_frame(); - current_list!(self).update(frame.area(), &self.view); + current_list!(self).update( + frame.area(), + &self.view, + self.config.show_dividers, + ); - let new_visible_objects = - current_list!(self).visible_objects(&frame.area(), &self.view); + let new_visible_objects = current_list!(self).visible_objects( + &frame.area(), + &self.view, + self.config.show_dividers, + ); if new_visible_objects != self.visible_objects { needs_render = true; self.visible_objects = new_visible_objects; diff --git a/src/object_list.rs b/src/object_list.rs index 61a5f96..b41320a 100644 --- a/src/object_list.rs +++ b/src/object_list.rs @@ -200,14 +200,20 @@ impl ObjectList { /// Returns a set of object IDs of the visible objects. This includes all /// dependencies that affect the display of the objects. + /// + /// See `update()` for why `show_dividers` is needed here. pub fn visible_objects( &self, area: &Rect, view: &view::View, + show_dividers: bool, ) -> HashSet { let objects = view.object_ids(self.list_kind); - let last = cmp::min(objects.len(), self.top + self.visible_count(area)); + let last = cmp::min( + objects.len(), + self.top + self.visible_count(area, show_dividers), + ); // Always include object 0 - the global PipeWire state. let mut visible_objects = HashSet::from([ObjectId::from_raw_id(0)]); @@ -241,21 +247,33 @@ impl ObjectList { } /// Returns the number of objects visible. - fn visible_count(&self, area: &Rect) -> usize { + fn visible_count(&self, area: &Rect, show_dividers: bool) -> usize { let (_, list_area, _) = self.areas(area); - let full_height = match self.list_kind { - ListKind::Node(_) => { - NodeWidget::height().saturating_add(NodeWidget::spacing()) - } + let (spacing, height) = match self.list_kind { + ListKind::Node(_) => (NodeWidget::spacing(), NodeWidget::height()), ListKind::Device => { - DeviceWidget::height().saturating_add(DeviceWidget::spacing()) + (DeviceWidget::spacing(), DeviceWidget::height()) } }; + // One extra row of spacing to center a divider between items - see + // the same +1 in ObjectListWidget::render() and the comment on + // render_divider() below. + let spacing = if show_dividers { spacing + 1 } else { spacing }; + let full_height = height.saturating_add(spacing); (list_area.height / full_height) as usize } /// Reconciles changes to objects, viewport, and selection. - pub fn update(&mut self, area: Rect, view: &view::View) { + /// + /// `show_dividers` must match `Config::show_dividers`, so that the + /// scroll/viewport math here agrees with the extra row `render()` + /// reserves per item when dividers are on - see `visible_count()`. + pub fn update( + &mut self, + area: Rect, + view: &view::View, + show_dividers: bool, + ) { let selected_index = self.selected_index(view).or_else(|| { // There's nothing selected! Select the first item and try again. self.select(view.next_id(self.list_kind, None)); @@ -264,7 +282,7 @@ impl ObjectList { let objects_len = view.len(self.list_kind); - let visible_count = self.visible_count(&area); + let visible_count = self.visible_count(&area, show_dividers); // If objects were removed and the viewport is now below the visible // objects, move the viewport up so that the bottom of the object list @@ -323,14 +341,17 @@ struct ObjectListRenderContext<'a> { objects_visible: usize, } -/// Draws a one-row divider line spanning `object_area`'s width, in the gap -/// immediately below it, when `config.show_dividers` is set. A no-op -/// (nothing drawn, no space reserved) when it isn't - dividers only ever -/// use space that the existing gap between items (NodeWidget::spacing()/ -/// DeviceWidget::spacing()) already reserves, so turning this on never -/// changes the list's layout, only what's drawn in space that was already -/// blank. Clipped to `list_area` so it can never bleed into a neighboring -/// item or past the list into the footer/tab bar. +/// Draws a one-row divider line spanning `object_area`'s width, centered in +/// the gap below it, when `config.show_dividers` is set. A no-op (nothing +/// drawn) when it isn't. Callers reserve one extra row of spacing whenever +/// `show_dividers` is on specifically so this can center the divider - one +/// blank row above it, one below - instead of it hugging whichever item +/// happens to sit above; with `show_dividers` off, spacing (and therefore +/// the whole list's layout) is untouched, matching stock wiremix exactly. +/// See `NodeWidget::spacing()`/`DeviceWidget::spacing()` and the `+ 1` in +/// `ObjectListWidget::render()`/`ObjectList::visible_count()`. Clipped to +/// `list_area` so it can never bleed into a neighboring item or past the +/// list into the footer/tab bar. fn render_divider( buf: &mut Buffer, config: &Config, @@ -343,7 +364,12 @@ fn render_divider( let divider_area = Rect { x: object_area.x, - y: object_area.y.saturating_add(object_area.height), + // + 1 to skip the first (blank) row of the gap the caller reserved, + // landing the divider on the middle row instead of the first. + y: object_area + .y + .saturating_add(object_area.height) + .saturating_add(1), width: object_area.width, height: 1, }; @@ -532,6 +558,14 @@ impl StatefulWidget for &mut ObjectListWidget<'_, '_> { (DeviceWidget::spacing(), DeviceWidget::height()) } }; + // One extra row of spacing to center a divider between items - see + // ObjectList::visible_count() and the comment on render_divider() + // below. Layout stays exactly as it is without show_dividers. + let spacing = if self.config.show_dividers { + spacing + 1 + } else { + spacing + }; let full_object_height = height.saturating_add(spacing); let objects_visible = (list_area.height / full_object_height) as usize; @@ -710,7 +744,7 @@ mod tests { assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1))); object_list.up(&view); - object_list.update(rect, &view); + object_list.update(rect, &view, false); assert_eq!(object_list.top, 0); assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1))); } @@ -741,7 +775,7 @@ mod tests { object_list.down(&view); } - object_list.update(rect, &view); + object_list.update(rect, &view, false); assert_eq!(object_list.top, 7); assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(10))); } @@ -763,7 +797,7 @@ mod tests { ObjectList::new(ListKind::Node(NodeKind::All), None); // Start at top - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert_eq!(visible.len(), 4); assert!(visible.contains(&ObjectId::from_raw_id(0))); assert!(visible.contains(&ObjectId::from_raw_id(1))); @@ -772,7 +806,7 @@ mod tests { // Scroll down object_list.top = 5; - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert_eq!(visible.len(), 4); assert!(visible.contains(&ObjectId::from_raw_id(0))); assert!(visible.contains(&ObjectId::from_raw_id(6))); @@ -781,7 +815,7 @@ mod tests { // Scroll up object_list.top = 4; - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert_eq!(visible.len(), 4); assert!(visible.contains(&ObjectId::from_raw_id(0))); assert!(visible.contains(&ObjectId::from_raw_id(5))); @@ -789,6 +823,33 @@ mod tests { assert!(visible.contains(&ObjectId::from_raw_id(7))); } + #[test] + fn show_dividers_true_reserves_extra_row_per_item() { + let (state, wirehose) = init(); + let view = View::from( + &wirehose, + &state, + &config::Names::default(), + &Vec::new(), + ); + + // Exactly enough room for 3 items at the default (no-divider) + // spacing - with show_dividers's extra row per item, only 2 now + // fit. This is the same rect/expected-4 case + // visible_objects_changes_with_scroll already covers for + // show_dividers = false; the point here is the contrast with + // show_dividers = true, not re-proving the false case. + let height = NodeWidget::height() + NodeWidget::spacing(); + let rect = Rect::new(0, 0, 80, height * 3 + 2); + let object_list = ObjectList::new(ListKind::Node(NodeKind::All), None); + + let without_dividers = object_list.visible_objects(&rect, &view, false); + let with_dividers = object_list.visible_objects(&rect, &view, true); + + assert_eq!(without_dividers.len(), 4); // 3 items + object 0 + assert_eq!(with_dividers.len(), 3); // 2 items + object 0 + } + #[test] fn visible_objects_includes_linked_clients() { let (mut state, wirehose) = init(); @@ -818,7 +879,7 @@ mod tests { let rect = Rect::new(0, 0, 80, height + 2); let object_list = ObjectList::new(ListKind::Node(NodeKind::All), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert_eq!(visible.len(), 3); assert!(visible.contains(&ObjectId::from_raw_id(0))); assert!(visible.contains(&ObjectId::from_raw_id(1))); @@ -877,7 +938,7 @@ mod tests { let rect = Rect::new(0, 0, 80, height + 2); let object_list = ObjectList::new(ListKind::Node(NodeKind::All), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert_eq!(visible.len(), 3); assert!(visible.contains(&ObjectId::from_raw_id(0))); assert!(visible.contains(&ObjectId::from_raw_id(1))); @@ -929,7 +990,7 @@ mod tests { let object_list = ObjectList::new(ListKind::Node(NodeKind::Playback), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert!(visible.contains(&stream_id)); assert!(visible.contains(&sink_id)); } @@ -998,7 +1059,7 @@ mod tests { let object_list = ObjectList::new(ListKind::Node(NodeKind::Playback), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert!(visible.contains(&stream_id)); assert!(visible.contains(&sink_id)); assert!(visible.contains(&sink_client_id)); @@ -1090,7 +1151,7 @@ mod tests { let object_list = ObjectList::new(ListKind::Node(NodeKind::Playback), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert!(visible.contains(&stream_id)); assert!(visible.contains(&sink_id)); assert!(visible.contains(&sink_device_id)); @@ -1136,7 +1197,7 @@ mod tests { let object_list = ObjectList::new(ListKind::Node(NodeKind::Playback), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert!(visible.contains(&stream_id)); assert!(visible.contains(&sink_id)); } @@ -1181,7 +1242,7 @@ mod tests { let object_list = ObjectList::new(ListKind::Node(NodeKind::Recording), None); - let visible = object_list.visible_objects(&rect, &view); + let visible = object_list.visible_objects(&rect, &view, false); assert!(visible.contains(&stream_id)); assert!(visible.contains(&source_id)); } @@ -1213,7 +1274,15 @@ mod tests { render_divider(&mut buf, &config, object_area, list_area); - let divider_row_start = buf.index_of(0, 3); + // Row 3 (immediately below the item) stays blank - the divider is + // centered on row 4, leaving one blank row above it and (assuming + // the caller reserved the usual 3-row gap) one below. + let blank_row_start = buf.index_of(0, 3); + for cell in buf.content[blank_row_start..blank_row_start + 10].iter() { + assert_eq!(cell.symbol(), " "); + } + + let divider_row_start = buf.index_of(0, 4); for cell in buf.content[divider_row_start..divider_row_start + 10].iter() { diff --git a/wiremix.toml b/wiremix.toml index 28aeb39..7bf18c6 100644 --- a/wiremix.toml +++ b/wiremix.toml @@ -29,10 +29,12 @@ char_set = "default" # Theme to use (see Themes section) theme = "default" -# If true, draw a divider line in the gap between items, styled by the -# divider theme key and using the divider char_set glyph. Off by default - -# the gap between items already exists either way, so this only changes -# what's drawn in space that was already blank, never the list's layout. +# If true, draw a divider line centered in the gap between items - one +# blank row above it, one below - styled by the divider theme key and +# using the divider char_set glyph. This reserves one extra row per item +# to center the divider symmetrically, so fewer items fit on screen at +# once than with this off. Off by default, leaving the list's layout +# exactly as it is otherwise. show_dividers = false # Initial tab