diff --git a/deckster/examples/full/icons/apps/youtube.svg b/deckster/examples/full/icons/apps/youtube.svg
index 01652bc..8d62a5d 100644
--- a/deckster/examples/full/icons/apps/youtube.svg
+++ b/deckster/examples/full/icons/apps/youtube.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml
index 7677da0..03c9c78 100644
--- a/deckster/examples/full/key-pages/default.toml
+++ b/deckster/examples/full/key-pages/default.toml
@@ -1,43 +1,36 @@
-[keys.1x1]
+[keys.1x2]
icon = "@ph/skip-back"
mode.playerctl__button.command = "previous"
mode.playerctl__button.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
-[keys.2x1]
+[keys.2x2]
icon = "@ph/play-pause[alpha=0.4]"
mode.playerctl__button.command = "play-pause"
mode.playerctl__button.style.paused.icon = "@ph/play"
mode.playerctl__button.style.playing.icon = "@ph/pause"
-[keys.3x1]
+[keys.3x2]
icon = "@ph/skip-forward"
mode.playerctl__button.command = "next"
mode.playerctl__button.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
-[keys.1x2]
+[keys.1x3]
icon = "@fad/shuffle[alpha=0.4]"
mode.playerctl__shuffle.style.on.icon = "@fad/shuffle[color=#58fc11]"
-[keys.2x2]
+[keys.2x3]
icon = "@fad/repeat[alpha=0.4]"
mode.playerctl__loop.style.single.icon = "@fad/repeat-one[color=#58fc11]"
mode.playerctl__loop.style.all.icon = "@fad/repeat[color=#58fc11]"
-[keys.4x1]
+[keys.3x3]
icon = "@ph/timer[color=#ff0000]"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_when_finished = true
mode.timer.needy = true
-[keys.3x3]
-icon = "@fad/thunderbolt"
-label = "Dock"
-border= "#00ff00"
-mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock"
-mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]"
-
[keys.4x3]
icon = "@ph/computer-tower"
-label = "Tower PC unnötig lang"
+label = "Gaming PC"
mode.home_assistant__switch.name = "switch.mwin"
mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]"
\ No newline at end of file
diff --git a/deckster/examples/full/knob-pages/default.toml b/deckster/examples/full/knob-pages/default.toml
index 38c33a2..bbcbbf1 100644
--- a/deckster/examples/full/knob-pages/default.toml
+++ b/deckster/examples/full/knob-pages/default.toml
@@ -5,8 +5,7 @@ indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05
mode.audio_volume.target.type = "input"
mode.audio_volume.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }]
-mode.audio_volume.disable_press_to_unmute = true
-mode.audio_volume.muted_turn_action = "unmute-at-zero"
+mode.audio_volume.muted_turn_action = "normal"
mode.audio_volume.style.active.label = "{percentage}%"
mode.audio_volume.style.muted.label = "Muted"
@@ -27,23 +26,25 @@ mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
mode.audio_volume.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]"
[knobs.left-middle]
-icon = "@apps/youtube[scale=1.3|color=#ff0000]"
+icon = "@apps/youtube[scale=1.3]"
indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05
+mode.audio_volume.muted_turn_action = "unmute"
mode.audio_volume.target.type = "application"
mode.audio_volume.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }]
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
-mode.audio_volume.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale|alpha=0.8]"
+mode.audio_volume.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale]"
[knobs.left-bottom]
icon = "@apps/spotify[scale=1.2]"
indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05
+mode.audio_volume.muted_turn_action = "unmute-at-zero"
mode.audio_volume.target.type = "application"
mode.audio_volume.target.predicates = [{ property = "application-name", value = "spotify" }]
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690"
-mode.audio_volume.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.8]"
\ No newline at end of file
+mode.audio_volume.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]"
\ No newline at end of file
diff --git a/deckster/src/modes/knob/audio_volume.rs b/deckster/src/modes/knob/audio_volume.rs
index f41f831..b58f2d9 100644
--- a/deckster/src/modes/knob/audio_volume.rs
+++ b/deckster/src/modes/knob/audio_volume.rs
@@ -80,8 +80,9 @@ pub enum TargetPredicateProperty {
pub enum MutedTurnAction {
#[default]
Ignore,
+ Normal,
UnmuteAtZero,
- UnmuteAndRestore,
+ Unmute,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
@@ -102,14 +103,10 @@ pub enum State {
static PA_VOLUME_INTERFACE: Lazy = Lazy::new(|| PaVolumeInterface::spawn_thread("deckster".to_owned()));
-fn get_volume_cv(channel_volumes: &[f32]) -> f32 {
+fn get_volume_from_cv(channel_volumes: &[f32]) -> f32 {
*channel_volumes.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
}
-fn get_volume_es(entity_state: &Option>) -> f32 {
- entity_state.as_ref().map(|s| get_volume_cv(&s.channel_volumes())).unwrap_or(0.0)
-}
-
fn state_matches(target: &Target, state: &PaEntityState) -> bool {
if !match target.kind {
TargetKind::Input => state.kind() == PaEntityKind::Source,
@@ -158,22 +155,28 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::
let pa_volume_interface = &PA_VOLUME_INTERFACE;
let (initial_state, mut volume_states) = pa_volume_interface.subscribe_to_state();
- if let Some(state) = initial_state {
- entity_state = state
- .entities_by_id()
- .values()
- .find(|entity| state_matches(&config.target, &entity))
- .map(Arc::clone);
+ let update_knob_value = {
+ let commands = commands.clone();
+ let config = Arc::clone(&config);
+ let path = path.clone();
- commands
- .send(IoWorkerCommand::SetKnobValue {
- path: path.clone(),
- value: get_volume_es(&entity_state),
- })
- .unwrap();
- }
+ move |entity_state: &Option>| {
+ commands
+ .send(IoWorkerCommand::SetKnobValue {
+ path: path.clone(),
+ value: entity_state.as_ref().map(|s| {
+ if s.is_muted() && config.muted_turn_action == MutedTurnAction::UnmuteAtZero {
+ 0.0
+ } else {
+ get_volume_from_cv(&s.channel_volumes())
+ }
+ }),
+ })
+ .unwrap();
+ }
+ };
- let update_style = {
+ let update_knob_style = {
let commands = commands.clone();
let config = Arc::clone(&config);
let path = path.clone();
@@ -188,9 +191,13 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::
let mut style = config.style.get(&state).cloned();
if let Some(ref mut s) = &mut style {
- let v = get_volume_es(entity_state);
+ let v = entity_state.as_ref().map(|s| get_volume_from_cv(&s.channel_volumes()));
+
if let Some(ref mut label) = &mut s.label {
- *label = label.replace("{percentage}", &((v * 100.0).round() as u32).to_string());
+ if let Some(v) = v {
+ // v is only None when state is State::Inactive
+ *label = label.replace("{percentage}", &((v * 100.0).round() as u32).to_string());
+ }
}
}
@@ -203,13 +210,20 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::
}
};
+ if let Some(state) = initial_state {
+ entity_state = state
+ .entities_by_id()
+ .values()
+ .find(|entity| state_matches(&config.target, &entity))
+ .map(Arc::clone);
+ }
+
loop {
select! {
Ok(volume_state) = volume_states.recv() => {
entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, &entity)).map(Arc::clone);
- update_style(&entity_state);
-
- commands.send(IoWorkerCommand::SetKnobValue { path: path.clone(), value: get_volume_es(&entity_state) }).unwrap()
+ update_knob_style(&entity_state);
+ update_knob_value(&entity_state);
}
Ok((event_path, event)) = events.recv() => {
@@ -220,17 +234,37 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::
if let Some(entity_state) = &entity_state {
match event {
KnobEvent::Rotate { direction } => {
- let factor = match direction {
+ let factor: f32 = match direction {
RotationDirection::Clockwise => 1.0,
RotationDirection::Counterclockwise => -1.0,
};
- let current_v = get_volume_cv(&entity_state.channel_volumes());
- let v = (current_v + (factor * config.delta.unwrap_or(0.01))).clamp(0.0, 1.0);
- pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![v; entity_state.channel_volumes().len()]);
+ let mut current_v = get_volume_from_cv(&entity_state.channel_volumes());
+
+ if entity_state.is_muted() {
+ match config.muted_turn_action {
+ MutedTurnAction::Ignore => continue,
+ MutedTurnAction::UnmuteAtZero => {
+ current_v = 0.0
+ }
+ MutedTurnAction::Unmute => {},
+ MutedTurnAction::Normal => {}
+ }
+ }
+
+ let new_v = (current_v + (factor * config.delta.unwrap_or(0.01))).clamp(0.0, 1.0);
+ if new_v > 0.0 && matches!(config.muted_turn_action, MutedTurnAction::Unmute | MutedTurnAction::UnmuteAtZero) {
+ pa_volume_interface.set_is_muted(*entity_state.id(), false);
+ }
+
+ pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![new_v; entity_state.channel_volumes().len()]);
}
KnobEvent::Press => {
- pa_volume_interface.set_is_muted(*entity_state.id(), !entity_state.is_muted())
+ let is_muted = entity_state.is_muted();
+
+ if (is_muted && !config.disable_press_to_unmute) || (!is_muted && !config.disable_press_to_mute) {
+ pa_volume_interface.set_is_muted(*entity_state.id(), !is_muted)
+ }
}
_ => {}
}
diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs
index 0e4bd61..7e6b045 100644
--- a/deckster/src/runner/command.rs
+++ b/deckster/src/runner/command.rs
@@ -12,5 +12,5 @@ pub enum IoWorkerCommand {
SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyStyle { path: KeyPath, value: Option },
SetKnobStyle { path: KnobPath, value: Option },
- SetKnobValue { path: KnobPath, value: f32 },
+ SetKnobValue { path: KnobPath, value: Option },
}
diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs
index 49594e1..4593627 100644
--- a/deckster/src/runner/graphics.rs
+++ b/deckster/src/runner/graphics.rs
@@ -84,9 +84,14 @@ pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Optio
const WIDTH: f32 = 5.0;
const PADDING_X: f32 = 5.0;
const PADDING_Y: f32 = 20.0;
+ const HEIGHT_AT_ZERO: f32 = 2.0;
let max_height: f32 = pixmap.height() as f32 - PADDING_Y * 2.0;
- let height = state.value * max_height;
+ let height = if let Some(value) = &state.value {
+ HEIGHT_AT_ZERO + value * (max_height - HEIGHT_AT_ZERO)
+ } else {
+ 0.0
+ };
let x = if state.path.position.is_left() {
PADDING_X
diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs
index a379db9..41f7937 100644
--- a/deckster/src/runner/mod.rs
+++ b/deckster/src/runner/mod.rs
@@ -346,15 +346,18 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
}
IoWorkerCommand::SetKnobValue { path, value } => {
- if !(0.0..=1.0).contains(&value) {
- error!("Received SetKnobValue with an out-of-range value: {}", value)
- } else {
- state.mutate_knob_for_command("SetKnobValue", &path, |k| {
- k.value = value;
- });
-
- draw_knob_at_path_if_visible(context, state, path);
+ if let Some(v) = value {
+ if !(0.0..=1.0).contains(&v) {
+ error!("Received SetKnobValue with an out-of-range value: {}", v);
+ return;
+ }
}
+
+ state.mutate_knob_for_command("SetKnobValue", &path, |k| {
+ k.value = value;
+ });
+
+ draw_knob_at_path_if_visible(context, state, path);
}
}
}
diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs
index 4fb2fc4..880ad50 100644
--- a/deckster/src/runner/state.rs
+++ b/deckster/src/runner/state.rs
@@ -57,7 +57,7 @@ impl State {
},
base_style: knob_config.base_style.clone(),
style: None,
- value: 0.0,
+ value: None,
}
}),
})
@@ -127,5 +127,5 @@ pub struct Knob {
pub path: KnobPath,
pub base_style: KnobStyle,
pub style: Option,
- pub value: f32,
+ pub value: Option,
}