Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7e7037a
Add show_channel_volumes to render paired channels as radiating bars
HoneyHazard Aug 10, 2026
3a7d3e3
Add node_widget-level render tests for show_channel_volumes dispatch
HoneyHazard Aug 10, 2026
82497ce
Add Action::SetChannelRelativeVolume as the relative counterpart to S…
HoneyHazard Aug 10, 2026
229e3eb
Document SetChannelAbsoluteVolume/SetChannelRelativeVolume as illustr…
HoneyHazard Aug 10, 2026
bb6aa24
Add Channel mode selection model: a second navigation dimension for p…
HoneyHazard Aug 10, 2026
78a2227
Build stacked-row Channel mode rendering
HoneyHazard Aug 10, 2026
39c153d
Make channel-mode up/down navigation direction-symmetric
HoneyHazard Aug 10, 2026
a1af1fa
Label Channel mode rows with real channel names instead of raw indices
HoneyHazard Aug 10, 2026
4b1cc20
Verify lazy_capture needs no changes for Channel mode's variable-heig…
HoneyHazard Aug 10, 2026
f2e4534
Fix clicking a channel row leaving a stale selected_channel
HoneyHazard Aug 10, 2026
f0ecebe
Fix four Channel/Linked-mode display and volume-op issues from live t…
HoneyHazard Aug 11, 2026
b34f69d
Replace show_channel_volumes with a full channel_display/unified_imba…
HoneyHazard Aug 11, 2026
5501941
Split peak meter per-channel row in Channel mode
HoneyHazard Aug 11, 2026
bf58eb3
Implement unified_imbalance = "cycle": temporal per-channel indicator
HoneyHazard Aug 11, 2026
1c5c2a6
Fix Radiating mode dropping channels on multi-pair devices, other fee…
HoneyHazard Aug 11, 2026
ce765fe
Unify radiating/unpaired row layout, add configurable pair labels
HoneyHazard Aug 11, 2026
0cec59f
Align volume bar start column across every node, not just within a sp…
HoneyHazard Aug 11, 2026
998ed2d
Fix cycling label, char collisions, and wasted middle padding
HoneyHazard Aug 11, 2026
d11b0b1
Revert volume_channel_* - misread the request as wanting new volume g…
HoneyHazard Aug 11, 2026
3e1e876
Make a paired row's monitor gauge use meter_channel_*, not meter_left…
HoneyHazard Aug 11, 2026
d0577b2
Remove meter_channel_* entirely - reuse stock meter glyphs everywhere
HoneyHazard Aug 11, 2026
1761acb
Change pair-label separator from L/R to L|R
HoneyHazard Aug 11, 2026
3c83dd9
Fix meter-column offset, cycling label alignment, selector span; add …
HoneyHazard Aug 11, 2026
73312a0
Add meter_width_percent and right_margin config settings
HoneyHazard Aug 11, 2026
fa60e6a
Add optional per-row meter theme colors and Unified/Linked/Channels v…
HoneyHazard Aug 12, 2026
1039209
Make Unified view reproduce stock wiremix's layout exactly
HoneyHazard Aug 12, 2026
a7d51bc
Add per-view meter layout settings (unified/linked/channels)
HoneyHazard Aug 12, 2026
ccd70b1
Small universal gap/right_margin defaults; implement space-reclaiming…
HoneyHazard Aug 12, 2026
b9609c6
Fix unpaired fill direction, cycling column jitter; carve gap/margin …
HoneyHazard Aug 12, 2026
b04119a
Restore per-row cycling width; make stereo-pair label reclaim actuall…
HoneyHazard Aug 12, 2026
c23c54f
Fix wasted-space bug, scale the volume/meter gap, rewrite mute placement
HoneyHazard Aug 13, 2026
19b3287
Achieve true Unified-view alignment by squeezing the label, not widen…
HoneyHazard Aug 13, 2026
a0f0adc
Give the Unified-view cycling label its own fixed, non-overlapping su…
HoneyHazard Aug 13, 2026
7cd7690
Rebase split-meter glyphs on view instead of widget, ship as real def…
HoneyHazard Aug 13, 2026
ce6f9bd
Remove misplaced pulsemixer volume-bar example from wiremix.toml
HoneyHazard Aug 13, 2026
df036c3
Fix Stacked-block rows vanishing mid-block when scrolled area is short
HoneyHazard Aug 13, 2026
067a90c
Revert meter_split_* glyph defaults to unset (match Unified view)
HoneyHazard Aug 14, 2026
ea7ad0d
Fix unified_imbalance = "cycle" freezing when otherwise idle
HoneyHazard Aug 14, 2026
6bf0ce7
Make Channel mode's selector one continuous bracket, not three segments
HoneyHazard Aug 14, 2026
a9aad7a
Restore disjoint split-view glyphs for compat char_set only
HoneyHazard Aug 14, 2026
953e7c7
Revert "Make Channel mode's selector one continuous bracket, not thre…
HoneyHazard Aug 14, 2026
f3df60d
Fix meter L/R width asymmetry; fix tests broken by the selector revert
HoneyHazard Aug 14, 2026
8fc6ff5
Replace channel_mode/channel_display with a single initial_view: Chan…
HoneyHazard Aug 19, 2026
09b024d
Default unified_imbalance to "cycle"; make cycle interval configurable
HoneyHazard Aug 19, 2026
0d51e4e
Remove dangling NOTES-multichannel.md references; clarify view field …
HoneyHazard Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 354 additions & 7 deletions src/app.rs

Large diffs are not rendered by default.

401 changes: 401 additions & 0 deletions src/channel_pairing.rs

Large diffs are not rendered by default.

564 changes: 564 additions & 0 deletions src/config.rs

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions src/config/char_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ pub struct CharSetOverlay {
meter_center_left_active: Option<String>,
meter_center_right_inactive: Option<String>,
meter_center_right_active: Option<String>,
meter_split_left_inactive: Option<String>,
meter_split_left_active: Option<String>,
meter_split_left_overload: Option<String>,
meter_split_right_inactive: Option<String>,
meter_split_right_active: Option<String>,
meter_split_right_overload: Option<String>,
meter_split_center_left_inactive: Option<String>,
meter_split_center_left_active: Option<String>,
meter_split_center_right_inactive: Option<String>,
meter_split_center_right_active: Option<String>,
dropdown_icon: Option<String>,
dropdown_selector: Option<String>,
dropdown_more: Option<String>,
Expand Down Expand Up @@ -95,6 +105,26 @@ impl TryFrom<CharSetOverlay> for CharSet {
};
}

// Same as `validate_and_set!`, but for the optional
// `meter_split_*` fields, whose unset state (`None`) is itself
// meaningful (falls back to the corresponding stock
// `meter_left`/`meter_right`/`meter_center_*` field at render
// time) rather than just "use the built-in default character".
macro_rules! validate_and_set_optional {
($field:ident, $length:expr) => {
if let Some(value) = overlay.$field {
if $length > 0 && Span::raw(&value).width() != $length {
anyhow::bail!(
"{} must be {} characters wide",
stringify!($field),
$length
);
}
char_set.$field = Some(value);
}
};
}

validate_and_set!(default_device, 1);
validate_and_set!(default_stream, 1);
validate_and_set!(selector_top, 1);
Expand All @@ -115,6 +145,16 @@ impl TryFrom<CharSetOverlay> for CharSet {
validate_and_set!(meter_center_left_active, 1);
validate_and_set!(meter_center_right_inactive, 1);
validate_and_set!(meter_center_right_active, 1);
validate_and_set_optional!(meter_split_left_inactive, 1);
validate_and_set_optional!(meter_split_left_active, 1);
validate_and_set_optional!(meter_split_left_overload, 1);
validate_and_set_optional!(meter_split_right_inactive, 1);
validate_and_set_optional!(meter_split_right_active, 1);
validate_and_set_optional!(meter_split_right_overload, 1);
validate_and_set_optional!(meter_split_center_left_inactive, 1);
validate_and_set_optional!(meter_split_center_left_active, 1);
validate_and_set_optional!(meter_split_center_right_inactive, 1);
validate_and_set_optional!(meter_split_center_right_active, 1);
validate_and_set!(dropdown_icon, 1);
validate_and_set!(dropdown_selector, 1);
validate_and_set!(dropdown_more, 0);
Expand Down Expand Up @@ -155,6 +195,24 @@ impl Default for CharSet {
meter_center_left_active: String::from("▮"),
meter_center_right_inactive: String::from("▮"),
meter_center_right_active: String::from("▮"),
// Unset here - this char_set's Unified-view meter is already
// a row of individually-inset filled rectangles ("▮"), so
// there's no seamless/disjoint distinction left to draw:
// Linked/Channels-view meters use exactly the same glyph,
// and the volume-bar split plus per-channel label remain the
// only visual cue a view has changed. Still overridable
// per-user. See CharSet::compat()'s own meter_split_*
// comment for the char_set where this distinction matters.
meter_split_left_inactive: None,
meter_split_left_active: None,
meter_split_left_overload: None,
meter_split_right_inactive: None,
meter_split_right_active: None,
meter_split_right_overload: None,
meter_split_center_left_inactive: None,
meter_split_center_left_active: None,
meter_split_center_right_inactive: None,
meter_split_center_right_active: None,
dropdown_icon: String::from("▼"),
dropdown_selector: String::from(">"),
dropdown_more: String::from("•••"),
Expand Down Expand Up @@ -196,6 +254,24 @@ impl CharSet {
meter_center_left_active: String::from("█"),
meter_center_right_inactive: String::from("█"),
meter_center_right_active: String::from("█"),
// Unlike CharSet::default()'s filled-block glyphs, this
// char_set's Unified-view meter is a *solid, seamlessly
// connected* line (box-drawing "┃"/"█" render with no gap
// between repeated cells). Linked/Channels views use a
// lighter-weight glyph pair in the same thin-bar/block family
// that renders with a natural vertical gap between repeated
// cells ("❘"/"▇"), so a split view is still visually distinct
// from Unified without introducing an unrelated shape.
meter_split_left_inactive: Some(String::from("❘")),
meter_split_left_active: Some(String::from("❘")),
meter_split_left_overload: Some(String::from("❘")),
meter_split_right_inactive: Some(String::from("❘")),
meter_split_right_active: Some(String::from("❘")),
meter_split_right_overload: Some(String::from("❘")),
meter_split_center_left_inactive: Some(String::from("▇")),
meter_split_center_left_active: Some(String::from("▇")),
meter_split_center_right_inactive: Some(String::from("▇")),
meter_split_center_right_active: Some(String::from("▇")),
dropdown_icon: String::from("▼"),
dropdown_selector: String::from(">"),
dropdown_more: String::from("•••"),
Expand Down Expand Up @@ -227,6 +303,16 @@ impl CharSet {
meter_center_left_active: String::from("["),
meter_center_right_inactive: String::from("]"),
meter_center_right_active: String::from("]"),
meter_split_left_inactive: None,
meter_split_left_active: None,
meter_split_left_overload: None,
meter_split_right_inactive: None,
meter_split_right_active: None,
meter_split_right_overload: None,
meter_split_center_left_inactive: None,
meter_split_center_left_active: None,
meter_split_center_right_inactive: None,
meter_split_center_right_active: None,
dropdown_icon: String::from("\\"),
dropdown_selector: String::from(">"),
dropdown_more: String::from("~~~"),
Expand Down
1 change: 1 addition & 0 deletions src/config/keybinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ impl Keybinding {
(event(KeyCode::Char('9')), Action::SetAbsoluteVolume(0.90)),
(event(KeyCode::Char('0')), Action::SetAbsoluteVolume(1.00)),
(event(KeyCode::Char('?')), Action::Help),
(event(KeyCode::Char(' ')), Action::CycleView),
])
}

Expand Down
61 changes: 61 additions & 0 deletions src/config/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub struct ThemeOverlay {
meter_overload: Option<StyleDef>,
meter_center_inactive: Option<StyleDef>,
meter_center_active: Option<StyleDef>,
meter_split_inactive: Option<StyleDef>,
meter_split_active: Option<StyleDef>,
meter_split_overload: Option<StyleDef>,
meter_split_center_inactive: Option<StyleDef>,
meter_split_center_active: Option<StyleDef>,
config_device: Option<StyleDef>,
config_profile: Option<StyleDef>,
dropdown_icon: Option<StyleDef>,
Expand Down Expand Up @@ -89,6 +94,18 @@ impl TryFrom<ThemeOverlay> for Theme {
};
}

// Same as `set!`, but for the optional `meter_split_*` fields,
// whose unset state (`None`) is itself meaningful (falls back to
// the corresponding stock `meter_*` field at render time) rather
// than just "use the built-in default".
macro_rules! set_optional {
($field:ident) => {
if let Some($field) = overlay.$field {
theme.$field = Some($field.into());
}
};
}

set!(default_device);
set!(default_stream);
set!(selector);
Expand All @@ -106,6 +123,11 @@ impl TryFrom<ThemeOverlay> for Theme {
set!(meter_overload);
set!(meter_center_inactive);
set!(meter_center_active);
set_optional!(meter_split_inactive);
set_optional!(meter_split_active);
set_optional!(meter_split_overload);
set_optional!(meter_split_center_inactive);
set_optional!(meter_split_center_active);
set!(config_device);
set!(config_profile);
set!(dropdown_icon);
Expand Down Expand Up @@ -141,6 +163,11 @@ impl Default for Theme {
meter_overload: Style::default().fg(Color::Red),
meter_center_inactive: Style::default().fg(Color::DarkGray),
meter_center_active: Style::default().fg(Color::LightGreen),
meter_split_inactive: None,
meter_split_active: None,
meter_split_overload: None,
meter_split_center_inactive: None,
meter_split_center_active: None,
config_device: Style::default(),
config_profile: Style::default(),
dropdown_icon: Style::default(),
Expand Down Expand Up @@ -185,6 +212,11 @@ impl Theme {
meter_overload: Style::default().add_modifier(Modifier::BOLD),
meter_center_inactive: Style::default().add_modifier(Modifier::DIM),
meter_center_active: Style::default().add_modifier(Modifier::BOLD),
meter_split_inactive: None,
meter_split_active: None,
meter_split_overload: None,
meter_split_center_inactive: None,
meter_split_center_active: None,
config_device: Style::default(),
config_profile: Style::default(),
dropdown_icon: Style::default(),
Expand Down Expand Up @@ -218,6 +250,11 @@ impl Theme {
meter_overload: Style::default(),
meter_center_inactive: Style::default(),
meter_center_active: Style::default(),
meter_split_inactive: None,
meter_split_active: None,
meter_split_overload: None,
meter_split_center_inactive: None,
meter_split_center_active: None,
config_device: Style::default(),
config_profile: Style::default(),
dropdown_icon: Style::default(),
Expand Down Expand Up @@ -309,4 +346,28 @@ mod tests {
assert_eq!(theme.selector, builtin.selector);
}
}

#[test]
fn meter_split_colors_are_unset_by_default_and_configurable() {
for builtin in Theme::defaults().values() {
assert_eq!(builtin.meter_split_inactive, None);
assert_eq!(builtin.meter_split_active, None);
assert_eq!(builtin.meter_split_overload, None);
assert_eq!(builtin.meter_split_center_inactive, None);
assert_eq!(builtin.meter_split_center_active, None);
}

let config = r#"
meter_split_inactive = { fg = "Black" }
"#;
let overlay = toml::from_str::<ThemeOverlay>(config).unwrap();
let theme = Theme::try_from(overlay).unwrap();
assert_eq!(
theme.meter_split_inactive,
Some(Style::default().fg(Color::Black))
);
// Only the configured field changes - everything else, including
// the other meter_split_* fields, stays unset.
assert_eq!(theme.meter_split_active, None);
}
}
14 changes: 13 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod app;
pub mod atomic_f32;
pub mod channel_pairing;
pub mod config;
pub mod device_kind;
pub mod device_widget;
Expand All @@ -24,10 +25,15 @@ mod mock {
use std::collections::VecDeque;
use std::sync::{atomic::AtomicBool, Arc};

// Variants intentionally share the `Node` prefix, mirroring
// CommandSender's own node_capture_start/node_capture_stop/
// node_volumes method names.
#[allow(clippy::enum_variant_names)]
#[derive(Debug, PartialEq)]
pub enum MockCommand {
NodeCaptureStart(ObjectId),
NodeCaptureStop(ObjectId),
NodeVolumes(ObjectId, Vec<f32>),
}

#[derive(Default)]
Expand Down Expand Up @@ -68,7 +74,13 @@ mod mock {
}
}
fn node_mute(&self, _object_id: ObjectId, _mute: bool) {}
fn node_volumes(&self, _object_id: ObjectId, _volumes: Vec<f32>) {}
fn node_volumes(&self, object_id: ObjectId, volumes: Vec<f32>) {
if let Some(commands) = self.commands {
commands
.borrow_mut()
.push_back(MockCommand::NodeVolumes(object_id, volumes));
}
}
fn device_mute(
&self,
_object_id: ObjectId,
Expand Down
Loading
Loading