Skip to content
Closed
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ itertools = "0.14.0"
libspa = "0.9.2"
libspa-sys = "0.9.2"
log = "0.4.24"
nix = { version = "0.29.0", features = ["event", "term"] }
nix = { version = "0.29.0", features = ["event", "term", "inotify"] }
pipewire = { version = "0.9.2", features = ["v0_3_44"] }
pulp = "0.22.2"
ratatui = { version = "0.29.0", features = ["serde"] }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ less-intuitive mouse controls are:
| ------------- | ----------------------- |
| q | Quit |
| m | Toggle mute |
| t | Hide/show (this instance only) |
| Ctrl+t | Hide/show (permanent, synced) |
| d | Set default source/sink |
| l/Right arrow | Increment volume |
| h/Left arrow | Decrement volume |
Expand Down
896 changes: 893 additions & 3 deletions src/app.rs

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct Config {
pub tab: usize,
pub tabs: Vec<TabKind>,
pub lazy_capture: bool,
pub capture_hidden: bool,
pub filters: Vec<MatchCondition>,
}

Expand Down Expand Up @@ -88,6 +89,8 @@ struct ConfigFile {
tabs: Vec<TabKind>,
#[serde(default = "default_lazy_capture")]
lazy_capture: bool,
#[serde(default = "default_capture_hidden")]
capture_hidden: bool,
#[serde(default = "Filter::defaults", deserialize_with = "Filter::merge")]
filters: Vec<Filter>,
}
Expand Down Expand Up @@ -145,6 +148,8 @@ pub struct NameOverride {
pub struct CharSet {
pub default_device: String,
pub default_stream: String,
pub hidden_instance: String,
pub hidden_permanent: String,
pub selector_top: String,
pub selector_middle: String,
pub selector_bottom: String,
Expand Down Expand Up @@ -193,6 +198,7 @@ pub struct Theme {
pub meter_center_active: Style,
pub config_device: Style,
pub config_profile: Style,
pub row_hidden: Style,
pub dropdown_icon: Style,
pub dropdown_border: Style,
pub dropdown_item: Style,
Expand Down Expand Up @@ -270,6 +276,10 @@ fn default_lazy_capture() -> bool {
false
}

fn default_capture_hidden() -> bool {
true
}

impl ConfigFile {
/// Override configuration with command-line arguments.
pub fn apply_opt(&mut self, opt: &Opt) {
Expand Down Expand Up @@ -328,6 +338,14 @@ impl ConfigFile {
if opt.lazy_capture {
self.lazy_capture = true;
}

if opt.no_capture_hidden {
self.capture_hidden = false;
}

if opt.capture_hidden {
self.capture_hidden = true;
}
}
}

Expand Down Expand Up @@ -396,6 +414,7 @@ impl TryFrom<ConfigFile> for Config {
tab,
tabs: config_file.tabs,
lazy_capture: config_file.lazy_capture,
capture_hidden: config_file.capture_hidden,
filters,
})
}
Expand Down Expand Up @@ -481,6 +500,7 @@ pub mod strict {
tab: Option<TabKind>,
tabs: Vec<TabKind>,
lazy_capture: bool,
capture_hidden: bool,
filters: Vec<Filter>,
}

Expand All @@ -502,6 +522,7 @@ pub mod strict {
tab: strict.tab,
tabs: strict.tabs,
lazy_capture: strict.lazy_capture,
capture_hidden: strict.capture_hidden,
filters: strict.filters,
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/config/char_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct CharSetOverlay {
inherit: Option<String>,
default_device: Option<String>,
default_stream: Option<String>,
hidden_instance: Option<String>,
hidden_permanent: Option<String>,
selector_top: Option<String>,
selector_middle: Option<String>,
selector_bottom: Option<String>,
Expand Down Expand Up @@ -97,6 +99,8 @@ impl TryFrom<CharSetOverlay> for CharSet {

validate_and_set!(default_device, 1);
validate_and_set!(default_stream, 1);
validate_and_set!(hidden_instance, 0);
validate_and_set!(hidden_permanent, 0);
validate_and_set!(selector_top, 1);
validate_and_set!(selector_middle, 1);
validate_and_set!(selector_bottom, 1);
Expand Down Expand Up @@ -137,6 +141,8 @@ impl Default for CharSet {
Self {
default_device: String::from("◇"),
default_stream: String::from("◇"),
hidden_instance: String::from("[hide] "),
hidden_permanent: String::from("[HIDE] "),
selector_top: String::from("░"),
selector_middle: String::from("▒"),
selector_bottom: String::from("░"),
Expand Down Expand Up @@ -178,6 +184,8 @@ impl CharSet {
Self {
default_device: String::from("◊"),
default_stream: String::from("◊"),
hidden_instance: String::from("[hide] "),
hidden_permanent: String::from("[HIDE] "),
selector_top: String::from("░"),
selector_middle: String::from("▒"),
selector_bottom: String::from("░"),
Expand Down Expand Up @@ -209,6 +217,8 @@ impl CharSet {
Self {
default_device: String::from("*"),
default_stream: String::from("*"),
hidden_instance: String::from("[hide] "),
hidden_permanent: String::from("[HIDE] "),
selector_top: String::from("-"),
selector_middle: String::from("="),
selector_bottom: String::from("-"),
Expand Down
9 changes: 9 additions & 0 deletions src/config/keybinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ impl Keybinding {
HashMap::from([
(event(KeyCode::Char('q')), Action::Exit),
(event(KeyCode::Char('m')), Action::ToggleMute),
// `t` for "toggle hide": toggles hiding the selected item for
// this instance only. Ctrl+t is the same "toggle hide" mnemonic,
// but permanently (saved to disk and synced to other running
// instances).
(event(KeyCode::Char('t')), Action::ToggleHiddenInstance),
(
KeyEvent::new(KeyCode::Char('t'), KeyModifiers::CONTROL),
Action::ToggleHiddenPermanent,
),
(event(KeyCode::Char('d')), Action::SetDefault),
(event(KeyCode::Char('l')), Action::SetRelativeVolume(0.01)),
(event(KeyCode::Right), Action::SetRelativeVolume(0.01)),
Expand Down
94 changes: 90 additions & 4 deletions src/config/matching.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::collections::HashMap;

use regex::Regex;
use serde::Deserialize;
use serde_with::DeserializeFromStr;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};

use crate::config::property_key::{PropertyKey, PropertyResolver};
use crate::wirehose::state;

#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct MatchCondition(pub HashMap<PropertyKey, MatchValue>);

Expand All @@ -21,9 +21,24 @@ impl MatchCondition {
.iter()
.all(|(key, value)| value.matches(resolver.resolve_key(state, key)))
}

/// Builds a matcher against a node's exact `node.name` - used to
/// durably identify a node for hide-items persistence. `node.name` is
/// the same property `Filter::defaults()` already relies on to
/// identify wiremix's own capture streams by name, the established
/// "stable enough" identifier for a node in this codebase. Not
/// perfectly stable for every kind of stream (an app that varies its
/// own `node.name` between launches won't re-match), the same
/// limitation any hand-written `[[filters]]` entry already has.
pub fn from_node_name(name: &str) -> Self {
Self(HashMap::from([(
PropertyKey::Bare(String::from("node.name")),
MatchValue::Literal(String::from(name)),
)]))
}
}

#[derive(Debug, DeserializeFromStr)]
#[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay)]
pub enum MatchValue {
Literal(String),
NegatedLiteral(String),
Expand All @@ -33,6 +48,30 @@ pub enum MatchValue {
NotNull,
}

impl std::fmt::Display for MatchValue {
/// Inverse of `FromStr` - only round-trips values `FromStr` can
/// actually parse back: the literal string `"null"` is escaped as
/// `"null"` (quoted) so it isn't read back as the `Null` variant on
/// reload. Literal strings that themselves start with `!` or `~` are
/// not escaped, since `FromStr` has no quoting mechanism for that -
/// same pre-existing limitation as any hand-written `[[filters]]`
/// entry.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchValue::Null => write!(f, "null"),
MatchValue::NotNull => write!(f, "!null"),
MatchValue::Literal(s) if s == "null" => write!(f, "\"null\""),
MatchValue::Literal(s) => write!(f, "{s}"),
MatchValue::NegatedLiteral(s) if s == "null" => {
write!(f, "!\"null\"")
}
MatchValue::NegatedLiteral(s) => write!(f, "!{s}"),
MatchValue::Regex(re) => write!(f, "~{}", re.as_str()),
MatchValue::NegatedRegex(re) => write!(f, "!~{}", re.as_str()),
}
}
}

#[cfg(test)]
impl PartialEq for MatchValue {
fn eq(&self, other: &Self) -> bool {
Expand Down Expand Up @@ -213,4 +252,51 @@ mod tests {
assert!(val.matches(Some("other")));
assert!(val.matches(None));
}

fn round_trip(s: &str) {
let val = s.parse::<MatchValue>().unwrap();
assert_eq!(val.to_string(), s);
let reparsed = val.to_string().parse::<MatchValue>().unwrap();
assert_eq!(val, reparsed);
}

#[test]
fn display_round_trips_null() {
round_trip("null");
}

#[test]
fn display_round_trips_not_null() {
round_trip("!null");
}

#[test]
fn display_round_trips_quoted_null_literal() {
round_trip("\"null\"");
}

#[test]
fn display_round_trips_negated_quoted_null_literal() {
round_trip("!\"null\"");
}

#[test]
fn display_round_trips_literal() {
round_trip("hello");
}

#[test]
fn display_round_trips_negated_literal() {
round_trip("!hello");
}

#[test]
fn display_round_trips_regex() {
round_trip("~^foo.*bar$");
}

#[test]
fn display_round_trips_negated_regex() {
round_trip("!~^foo.*bar$");
}
}
25 changes: 10 additions & 15 deletions src/config/property_key.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
//! An identifier for a property on an object or a linked object

use serde_with::DeserializeFromStr;
use serde_with::{DeserializeFromStr, SerializeDisplay};

use crate::wirehose::state;

#[derive(Debug, Clone, Hash, PartialEq, Eq, DeserializeFromStr)]
#[derive(
Debug, Clone, Hash, PartialEq, Eq, DeserializeFromStr, SerializeDisplay,
)]
pub enum PropertyKey {
Device(String),
Node(String),
Client(String),
Bare(String),
}

#[allow(clippy::to_string_trait_impl)] // This is not for display.
impl ToString for PropertyKey {
fn to_string(&self) -> String {
impl std::fmt::Display for PropertyKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PropertyKey::Device(s) => {
format!("device:{s}")
}
PropertyKey::Node(s) => {
format!("node:{s}")
}
PropertyKey::Client(s) => {
format!("client:{s}")
}
PropertyKey::Bare(s) => s.to_string(),
PropertyKey::Device(s) => write!(f, "device:{s}"),
PropertyKey::Node(s) => write!(f, "node:{s}"),
PropertyKey::Client(s) => write!(f, "client:{s}"),
PropertyKey::Bare(s) => write!(f, "{s}"),
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/config/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub struct ThemeOverlay {
meter_center_active: Option<StyleDef>,
config_device: Option<StyleDef>,
config_profile: Option<StyleDef>,
row_hidden: Option<StyleDef>,
dropdown_icon: Option<StyleDef>,
dropdown_border: Option<StyleDef>,
dropdown_item: Option<StyleDef>,
Expand Down Expand Up @@ -108,6 +109,7 @@ impl TryFrom<ThemeOverlay> for Theme {
set!(meter_center_active);
set!(config_device);
set!(config_profile);
set!(row_hidden);
set!(dropdown_icon);
set!(dropdown_border);
set!(dropdown_item);
Expand Down Expand Up @@ -143,6 +145,7 @@ impl Default for Theme {
meter_center_active: Style::default().fg(Color::LightGreen),
config_device: Style::default(),
config_profile: Style::default(),
row_hidden: Style::default().fg(Color::DarkGray),
dropdown_icon: Style::default(),
dropdown_border: Style::default(),
dropdown_item: Style::default(),
Expand Down Expand Up @@ -187,6 +190,7 @@ impl Theme {
meter_center_active: Style::default().add_modifier(Modifier::BOLD),
config_device: Style::default(),
config_profile: Style::default(),
row_hidden: Style::default().add_modifier(Modifier::DIM),
dropdown_icon: Style::default(),
dropdown_border: Style::default(),
dropdown_item: Style::default(),
Expand Down Expand Up @@ -220,6 +224,7 @@ impl Theme {
meter_center_active: Style::default(),
config_device: Style::default(),
config_profile: Style::default(),
row_hidden: Style::default(),
dropdown_icon: Style::default(),
dropdown_border: Style::default(),
dropdown_item: Style::default(),
Expand Down
Loading
Loading