diff --git a/Cargo.lock b/Cargo.lock
index 416e122..811a844 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -365,6 +365,7 @@ dependencies = [
"humantime-serde",
"log",
"loupedeck_serial",
+ "once_cell",
"regex",
"resvg",
"rgb",
@@ -857,6 +858,7 @@ dependencies = [
"enumset",
"flume",
"rgb",
+ "serde",
"serialport",
"thiserror",
]
diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml
index 9c03d19..713e51c 100644
--- a/deckster/Cargo.toml
+++ b/deckster/Cargo.toml
@@ -28,3 +28,4 @@ tiny-skia = "0.11.3"
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]}
toml = "0.8.8"
walkdir = "2.4.0"
+once_cell = "1.19.0"
\ No newline at end of file
diff --git a/deckster/examples/full/icons/fad/repeat-one.svg b/deckster/examples/full/icons/fad/repeat-one.svg
new file mode 100644
index 0000000..f46d69a
--- /dev/null
+++ b/deckster/examples/full/icons/fad/repeat-one.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/deckster/examples/full/icons/fad/repeat.svg b/deckster/examples/full/icons/fad/repeat.svg
new file mode 100644
index 0000000..35871f2
--- /dev/null
+++ b/deckster/examples/full/icons/fad/repeat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/deckster/examples/full/icons/ph/play-pause.svg b/deckster/examples/full/icons/ph/play-pause.svg
new file mode 100644
index 0000000..c055180
--- /dev/null
+++ b/deckster/examples/full/icons/ph/play-pause.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/deckster/examples/full/icons/ph/skip-back.svg b/deckster/examples/full/icons/ph/skip-back.svg
new file mode 100644
index 0000000..67ffd93
--- /dev/null
+++ b/deckster/examples/full/icons/ph/skip-back.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/deckster/examples/full/icons/ph/skip-forward.svg b/deckster/examples/full/icons/ph/skip-forward.svg
new file mode 100644
index 0000000..466801e
--- /dev/null
+++ b/deckster/examples/full/icons/ph/skip-forward.svg
@@ -0,0 +1 @@
+
\ 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 6edc056..2f9b22a 100644
--- a/deckster/examples/full/key-pages/default.toml
+++ b/deckster/examples/full/key-pages/default.toml
@@ -1,17 +1,31 @@
[keys.1x1]
-icon = "@ph/play[alpha=0.4]"
-mode.vibrate.pattern = "low"
-mode.media__play_pause.style.paused.icon = "@ph/play"
-mode.media__play_pause.style.playing.icon = "@ph/pause"
-
-[keys.1x2]
-icon = "@fad/shuffle[alpha=0.6]"
-mode.vibrate.pattern = "low"
-mode.spotify__shuffle.icon.active = "@fad/shuffle[color=#58fc11]"
+icon = "@ph/skip-back"
+mode.playerctl__button.command = "previous"
+mode.playerctl__button.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
[keys.2x1]
+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]
+icon = "@ph/skip-forward"
+mode.playerctl__button.command = "next"
+mode.playerctl__button.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
+
+[keys.1x2]
+icon = "@fad/shuffle[alpha=0.4]"
+mode.playerctl__shuffle.style.on.icon = "@fad/shuffle[color=#58fc11]"
+mode.playerctl__shuffle.style.off.icon = "@fad/shuffle"
+
+[keys.2x2]
+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]
icon = "@ph/timer[color=#ff0000]"
-mode.vibrate.pattern = "low"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_when_finished = true
mode.timer.needy = true
@@ -20,13 +34,11 @@ mode.timer.needy = true
icon = "@fad/thunderbolt"
label = "Dock"
border= "#00ff00"
-mode.vibrate.pattern = "low"
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"
-mode.vibrate.pattern = "low"
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/src/icons/mod.rs b/deckster/src/icons/mod.rs
index efe64bd..a867999 100644
--- a/deckster/src/icons/mod.rs
+++ b/deckster/src/icons/mod.rs
@@ -47,15 +47,15 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet {
result.insert(d.clone());
}
- if let Some(c) = &key.mode.media__next {
+ if let Some(c) = &key.mode.playerctl__button {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
- if let Some(c) = &key.mode.media__play_pause {
+ if let Some(c) = &key.mode.playerctl__shuffle {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
- if let Some(c) = &key.mode.media__previous {
+ if let Some(c) = &key.mode.playerctl__loop {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
@@ -66,14 +66,6 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet {
if let Some(c) = &key.mode.home_assistant__switch {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
-
- if let Some(c) = &key.mode.spotify__repeat {
- insert_all_from_key_style_by_state_map(&mut result, &c.style);
- }
-
- if let Some(c) = &key.mode.spotify__shuffle {
- insert_all_from_key_style_by_state_map(&mut result, &c.style);
- }
}
}
diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs
index a95558b..ca3cddd 100644
--- a/deckster/src/model/key_page.rs
+++ b/deckster/src/model/key_page.rs
@@ -76,11 +76,9 @@ pub struct KeyConfig {
#[derive(Debug, Default, Deserialize)]
pub struct KeyModes {
pub vibrate: Option>,
- pub media__play_pause: Option>,
- pub media__previous: Option>,
- pub media__next: Option>,
- pub spotify__shuffle: Option>,
- pub spotify__repeat: Option>,
+ pub playerctl__button: Option>,
+ pub playerctl__shuffle: Option>,
+ pub playerctl__loop: Option>,
pub home_assistant__switch: Option>,
pub home_assistant__button: Option>,
}
diff --git a/deckster/src/modes/key/media.rs b/deckster/src/modes/key/media.rs
deleted file mode 100644
index 03ca5bf..0000000
--- a/deckster/src/modes/key/media.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::sync::Arc;
-
-use flume::Sender;
-use serde::Deserialize;
-use tokio::sync::broadcast::Receiver;
-
-use crate::model::key_page::StyleByStateMap;
-use crate::model::position::KeyPath;
-use crate::modes::key::KeyEvent;
-use crate::runner::state::StateChangeCommand;
-
-#[derive(Debug, Deserialize)]
-pub struct PlayPauseConfig {
- #[serde(default)]
- pub style: StyleByStateMap,
- #[serde(default)]
- pub action: PlayPauseAction,
-}
-
-#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum PlayPauseAction {
- #[default]
- Toggle,
- Play,
- Pause,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct PreviousAndNextConfig {
- #[serde(default)]
- pub style: StyleByStateMap,
-}
-
-#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum PlayPauseState {
- Inactive,
- Playing,
- Paused,
-}
-
-#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum PreviousAndNextState {
- Inactive,
- Active,
-}
-
-pub async fn handle(path: KeyPath, config: Arc, mut events: Receiver, commands: Sender) {
- let mut state = PlayPauseState::Inactive;
-
- while let Ok(event) = events.recv().await {
- if event == KeyEvent::Press {
- state = match state {
- PlayPauseState::Playing => PlayPauseState::Paused,
- _ => PlayPauseState::Playing,
- };
-
- commands
- .send(StateChangeCommand::SetKeyStyle {
- path: path.clone(),
- value: config.style.get(&state).cloned(),
- })
- .unwrap();
- }
- }
-}
diff --git a/deckster/src/modes/key/mod.rs b/deckster/src/modes/key/mod.rs
index d8ceeae..b469169 100644
--- a/deckster/src/modes/key/mod.rs
+++ b/deckster/src/modes/key/mod.rs
@@ -5,31 +5,50 @@ use tokio::sync::broadcast;
use crate::model;
use crate::model::position::KeyPath;
-use crate::runner::state::StateChangeCommand;
+use crate::runner::command::IoWorkerCommand;
pub mod home_assistant;
-pub mod media;
-pub mod spotify;
+pub mod playerctl;
pub mod timer;
pub mod vibrate;
+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
+pub enum KeyTouchEventKind {
+ Start,
+ Move,
+ End,
+}
+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KeyEvent {
Press,
+ Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
VisibilityChange { is_visible: bool },
}
pub fn start_handlers(
keys: impl Iterator- )>,
events: broadcast::Sender<(KeyPath, KeyEvent)>,
- commands: flume::Sender,
+ commands: flume::Sender,
) {
for (path, config) in keys {
let mut events = events.subscribe();
let own_events = broadcast::Sender::new(5);
- if let Some(c) = &config.mode.media__play_pause {
- tokio::spawn(media::handle(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
+ if let Some(c) = &config.mode.playerctl__button {
+ tokio::spawn(playerctl::handle_button(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
+ }
+
+ if let Some(c) = &config.mode.playerctl__shuffle {
+ tokio::spawn(playerctl::handle_shuffle(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
+ }
+
+ if let Some(c) = &config.mode.playerctl__loop {
+ tokio::spawn(playerctl::handle_loop(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
+ }
+
+ if let Some(c) = &config.mode.vibrate {
+ tokio::spawn(vibrate::handle(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
}
tokio::spawn(async move {
diff --git a/deckster/src/modes/key/playerctl.rs b/deckster/src/modes/key/playerctl.rs
new file mode 100644
index 0000000..0fe2a3f
--- /dev/null
+++ b/deckster/src/modes/key/playerctl.rs
@@ -0,0 +1,282 @@
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::io::{BufRead, BufReader};
+use std::process::Stdio;
+use std::sync::{Arc, RwLock};
+use std::thread;
+use std::thread::sleep;
+use std::time::Duration;
+
+use log::{error, warn};
+use once_cell::sync::Lazy;
+use serde::Deserialize;
+use tokio::select;
+use tokio::sync::broadcast;
+use tokio::sync::broadcast::error::RecvError;
+
+use crate::model::key_page::StyleByStateMap;
+use crate::model::position::KeyPath;
+use crate::modes::key::KeyEvent;
+use crate::runner::command::IoWorkerCommand;
+
+#[derive(Debug, Deserialize)]
+pub struct ButtonConfig {
+ #[serde(default)]
+ pub style: StyleByStateMap,
+ pub command: ButtonCommand,
+}
+
+#[derive(Debug, Eq, PartialEq, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ButtonCommand {
+ PlayPause,
+ Play,
+ Pause,
+ Previous,
+ Next,
+}
+
+#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ButtonState {
+ Inactive,
+ Playing,
+ Paused,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ShuffleConfig {
+ #[serde(default)]
+ pub style: StyleByStateMap,
+}
+
+#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ShuffleState {
+ Inactive,
+ Off,
+ On,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct LoopConfig {
+ #[serde(default)]
+ pub style: StyleByStateMap,
+}
+
+#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum LoopState {
+ Inactive,
+ None,
+ Single,
+ All,
+}
+
+struct PlayerctlStateWatcher {
+ state: Arc>,
+ new_states: broadcast::Sender
,
+}
+
+impl PlayerctlStateWatcher {
+ pub fn new(initial_state: S, subcommand: &'static str, parse_state: impl 'static + Fn(&String) -> S + Send) -> Self {
+ let state = Arc::new(RwLock::new(initial_state));
+ let cloned_state = Arc::clone(&state);
+
+ let new_states = broadcast::Sender::new(1);
+ let cloned_new_states = new_states.clone();
+
+ thread::spawn(move || {
+ loop {
+ let players = std::process::Command::new("playerctl")
+ .args(["-s", "-l"])
+ .stderr(Stdio::null())
+ .stdin(Stdio::null())
+ .output()
+ .unwrap();
+
+ if players.stdout.iter().filter(|c| **c == b'\n').count() > 0 {
+ break;
+ }
+
+ sleep(Duration::from_secs(1));
+ }
+
+ let command = std::process::Command::new("playerctl")
+ .args(["-s", "-F", subcommand])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .stdin(Stdio::null())
+ .spawn()
+ .unwrap();
+
+ let stdout = BufReader::new(command.stdout.unwrap());
+
+ for line in stdout.lines() {
+ let line = line.unwrap();
+ let new_state = parse_state(&line);
+
+ {
+ let mut g = cloned_state.write().unwrap();
+ *g = new_state.clone();
+ if cloned_new_states.send(new_state).is_err() {
+ warn!("No one is listening to player state changes")
+ }
+ }
+ }
+ });
+
+ PlayerctlStateWatcher { state, new_states }
+ }
+
+ fn subscribe_to_state(&self) -> broadcast::Receiver {
+ let r = self.new_states.subscribe();
+ self.new_states.send(self.state.read().unwrap().clone()).unwrap();
+ r
+ }
+}
+
+static STATE_WATCHER_PLAYING: Lazy> = Lazy::new(|| {
+ PlayerctlStateWatcher::new(ButtonState::Inactive, "status", |s| match s.as_ref() {
+ "Playing" => ButtonState::Playing,
+ "Paused" => ButtonState::Paused,
+ "" | "Stopped" => ButtonState::Inactive,
+ _ => panic!("Unknown state: {}", s),
+ })
+});
+
+static STATE_WATCHER_SHUFFLE: Lazy> = Lazy::new(|| {
+ PlayerctlStateWatcher::new(ShuffleState::Inactive, "shuffle", |s| match s.as_ref() {
+ "Off" => ShuffleState::Off,
+ "On" => ShuffleState::On,
+ "" => ShuffleState::Inactive,
+ _ => panic!("Unknown state: {}", s),
+ })
+});
+
+static STATE_WATCHER_LOOP: Lazy> = Lazy::new(|| {
+ PlayerctlStateWatcher::new(LoopState::Inactive, "loop", |s| match s.as_ref() {
+ "Track" => LoopState::Single,
+ "Playlist" => LoopState::All,
+ "None" => LoopState::None,
+ "" => LoopState::Inactive,
+ _ => panic!("Unknown state: {}", s),
+ })
+});
+
+pub async fn handle_button(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) {
+ let mut is_active = false;
+ let mut state = STATE_WATCHER_PLAYING.subscribe_to_state();
+
+ let command = match config.command {
+ ButtonCommand::PlayPause => "play-pause",
+ ButtonCommand::Play => "play",
+ ButtonCommand::Pause => "pause",
+ ButtonCommand::Previous => "previous",
+ ButtonCommand::Next => "next",
+ };
+
+ loop {
+ select! {
+ result = state.recv() => {
+ match result {
+ Err(RecvError::Closed) => { result.unwrap(); },
+ Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
+ Ok(state) => {
+ is_active = state != ButtonState::Inactive;
+
+ commands.send(IoWorkerCommand::SetKeyStyle {
+ path: path.clone(),
+ value: config.style.get(&state).cloned()
+ }).unwrap();
+ }
+ }
+ }
+
+ Ok(event) = events.recv() => {
+ if event == KeyEvent::Press && is_active {
+ let status = std::process::Command::new("playerctl").arg(command).status().unwrap();
+
+ if !status.success() {
+ error!("`playerctl {}` failed with exit code {}", command, status)
+ }
+ }
+ }
+ }
+ }
+}
+
+pub async fn handle_shuffle(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) {
+ let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state();
+
+ loop {
+ select! {
+ result = state.recv() => {
+ match result {
+ Err(RecvError::Closed) => { result.unwrap(); },
+ Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
+ Ok(state) => {
+ commands.send(IoWorkerCommand::SetKeyStyle {
+ path: path.clone(),
+ value: config.style.get(&state).cloned()
+ }).unwrap();
+ }
+ }
+ }
+
+ Ok(event) = events.recv() => {
+ if event == KeyEvent::Press {
+ let current = *STATE_WATCHER_SHUFFLE.state.read().unwrap();
+ let new = match current {
+ ShuffleState::Inactive | ShuffleState::Off => "On",
+ ShuffleState::On => "Off",
+ };
+
+ let status = std::process::Command::new("playerctl").args(["shuffle", new]).status().unwrap();
+
+ if !status.success() {
+ error!("`playerctl shuffle {}` failed with exit code {}", new, status)
+ }
+ }
+ }
+ }
+ }
+}
+
+pub async fn handle_loop(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) {
+ let mut state = STATE_WATCHER_LOOP.subscribe_to_state();
+
+ loop {
+ select! {
+ result = state.recv() => {
+ match result {
+ Err(RecvError::Closed) => { result.unwrap(); },
+ Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
+ Ok(state) => {
+ commands.send(IoWorkerCommand::SetKeyStyle {
+ path: path.clone(),
+ value: config.style.get(&state).cloned()
+ }).unwrap();
+ }
+ }
+ }
+
+ Ok(event) = events.recv() => {
+ if event == KeyEvent::Press {
+ let current = *STATE_WATCHER_LOOP.state.read().unwrap();
+ let new = match current {
+ LoopState::Inactive | LoopState::None => "Playlist",
+ LoopState::All => "Track",
+ LoopState::Single => "None",
+ };
+
+ let status = std::process::Command::new("playerctl").args(["loop", new]).status().unwrap();
+
+ if !status.success() {
+ error!("`playerctl loop {}` failed with exit code {}", new, status)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/deckster/src/modes/key/spotify.rs b/deckster/src/modes/key/spotify.rs
deleted file mode 100644
index b2b14fd..0000000
--- a/deckster/src/modes/key/spotify.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use serde::Deserialize;
-
-use crate::model::key_page::StyleByStateMap;
-
-#[derive(Debug, Deserialize)]
-pub struct ShuffleConfig {
- #[serde(default)]
- pub style: StyleByStateMap,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct RepeatConfig {
- #[serde(default)]
- pub style: StyleByStateMap,
-}
-
-#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum ShuffleState {
- Inactive,
- Active,
-}
-
-#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum RepeatState {
- Inactive,
- Single,
- All,
-}
diff --git a/deckster/src/modes/key/vibrate.rs b/deckster/src/modes/key/vibrate.rs
index 928ba84..1bff1ac 100644
--- a/deckster/src/modes/key/vibrate.rs
+++ b/deckster/src/modes/key/vibrate.rs
@@ -1,39 +1,25 @@
+use std::sync::Arc;
+
use serde::Deserialize;
+use tokio::sync::broadcast;
+
+use loupedeck_serial::commands::VibrationPattern;
+
+use crate::model::position::KeyPath;
+use crate::modes::key::{KeyEvent, KeyTouchEventKind};
+use crate::runner::command::IoWorkerCommand;
#[derive(Debug, Deserialize)]
pub struct Config {
pub pattern: VibrationPattern,
}
-#[derive(Debug, Eq, PartialEq, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum VibrationPattern {
- Short,
- Medium,
- Long,
- Low,
- ShortLow,
- ShortLower,
- Lower,
- Lowest,
- DescendSlow,
- DescendMed,
- DescendFast,
- AscendSlow,
- AscendMed,
- AscendFast,
- RevSlowest,
- RevSlow,
- RevMed,
- RevFast,
- RevFaster,
- RevFastest,
- RiseFall,
- Buzz,
- Rumble5,
- Rumble4,
- Rumble3,
- Rumble2,
- Rumble1,
- VeryLong,
+pub async fn handle(_: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) {
+ while let Ok(event) = events.recv().await {
+ if let KeyEvent::Touch { kind, .. } = event {
+ if kind == KeyTouchEventKind::Start {
+ commands.send(IoWorkerCommand::Vibrate { pattern: config.pattern }).unwrap();
+ }
+ }
+ }
}
diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs
new file mode 100644
index 0000000..31e284b
--- /dev/null
+++ b/deckster/src/runner/command.rs
@@ -0,0 +1,13 @@
+use serde::{Deserialize, Serialize};
+
+use loupedeck_serial::commands::VibrationPattern;
+
+use crate::model::key_page::KeyStyle;
+use crate::model::position::KeyPath;
+
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
+pub enum IoWorkerCommand {
+ Vibrate { pattern: VibrationPattern },
+ SetActivePages { key_page_id: String, knob_page_id: String },
+ SetKeyStyle { path: KeyPath, value: Option },
+}
diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs
index 1a73d53..99519bc 100644
--- a/deckster/src/runner/mod.rs
+++ b/deckster/src/runner/mod.rs
@@ -1,5 +1,5 @@
use std::cell::RefCell;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;
use std::thread;
@@ -9,11 +9,12 @@ use color_eyre::eyre::{ContextCompat, WrapErr};
use color_eyre::Result;
use enum_map::EnumMap;
use enum_ordinalize::Ordinalize;
-use log::{debug, info, trace};
+use log::{info, trace};
use rgb::RGB8;
use tiny_skia::IntSize;
use tokio::sync::broadcast;
+use command::IoWorkerCommand;
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics};
use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice;
@@ -23,11 +24,12 @@ use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap};
use crate::model;
use crate::model::knob_page::KnobStyle;
use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath};
-use crate::modes::key::{start_handlers, KeyEvent};
+use crate::modes::key::{start_handlers, KeyEvent, KeyTouchEventKind};
use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::graphics::{render_key, GraphicsContext};
-use crate::runner::state::{Key, State, StateChangeCommand};
+use crate::runner::state::{Key, State};
+pub mod command;
mod graphics;
pub mod state;
@@ -53,11 +55,11 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re
device.set_brightness(0.5);
device.vibrate(VibrationPattern::RiseFall);
- let (commands_sender, commands_receiver) = flume::bounded::(20);
+ let (commands_sender, commands_receiver) = flume::bounded::(20);
let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20);
commands_sender
- .send(StateChangeCommand::SetActivePages {
+ .send(IoWorkerCommand::SetActivePages {
knob_page_id: config.initial.knob_page.clone(),
key_page_id: config.initial.key_page.clone(),
})
@@ -157,7 +159,7 @@ fn create_state(config: &model::config::Config) -> State {
enum IoWork {
Event(LoupedeckEvent),
- Command(StateChangeCommand),
+ Command(IoWorkerCommand),
}
struct IoWorkerContext {
@@ -165,6 +167,7 @@ struct IoWorkerContext {
device: LoupedeckDevice,
state: State,
graphics: GraphicsContext,
+ active_touch_ids: HashSet,
}
fn do_io_work(
@@ -172,8 +175,8 @@ fn do_io_work(
icons: LoadedIconsMap,
device: LoupedeckDevice,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
- commands_sender: flume::Sender,
- commands_receiver: flume::Receiver,
+ commands_sender: flume::Sender,
+ commands_receiver: flume::Receiver,
) {
let state = create_state(&config);
let buffer_endianness = device.characteristics().key_grid.display.endianness;
@@ -197,6 +200,7 @@ fn do_io_work(
label_renderer,
global_icon_filter_by_pack_id,
},
+ active_touch_ids: HashSet::new(),
};
loop {
@@ -207,7 +211,7 @@ fn do_io_work(
match a {
IoWork::Event(event) => {
- if !handle_event(&context, &commands_sender, &key_events_sender, event) {
+ if !handle_event(&mut context, &commands_sender, &key_events_sender, event) {
break;
}
}
@@ -217,8 +221,8 @@ fn do_io_work(
}
fn handle_event(
- context: &IoWorkerContext,
- commands_sender: &flume::Sender,
+ context: &mut IoWorkerContext,
+ commands_sender: &flume::Sender,
key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>,
event: LoupedeckEvent,
) -> bool {
@@ -236,32 +240,56 @@ fn handle_event(
let button_config = &context.config.buttons[position];
commands_sender
- .send(StateChangeCommand::SetActivePages {
+ .send(IoWorkerCommand::SetActivePages {
key_page_id: button_config.key_page.as_ref().unwrap_or(&context.state.active_key_page_id).clone(),
knob_page_id: button_config.knob_page.as_ref().unwrap_or(&context.state.active_knob_page_id).clone(),
})
.unwrap()
}
- LoupedeckEvent::Touch { x, y, is_end, .. } => {
- if is_end {
- let characteristics = context.device.characteristics();
- let display = characteristics.get_display_at_coordinates(x, y);
+ LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
+ let characteristics = context.device.characteristics();
+ let display = characteristics.get_display_at_coordinates(x, y);
- if let Some(display) = display {
- if display.name == characteristics.key_grid.display.name {
- let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y);
- if let Some(key_index) = key_index {
- let position = KeyPosition {
- x: (key_index % characteristics.key_grid.columns) as u16 + 1,
- y: (key_index / characteristics.key_grid.columns) as u16 + 1,
- };
+ if let Some(display) = display {
+ if display.name == characteristics.key_grid.display.name {
+ let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y);
+ if let Some(key_index) = key_index {
+ let position = KeyPosition {
+ x: (key_index % characteristics.key_grid.columns) as u16 + 1,
+ y: (key_index / characteristics.key_grid.columns) as u16 + 1,
+ };
- let path = KeyPath {
- page_id: context.state.active_key_page_id.clone(),
- position,
- };
+ let path = KeyPath {
+ page_id: context.state.active_key_page_id.clone(),
+ position,
+ };
- send_key_event(path, KeyEvent::Press);
+ let (top_left_x, top_left_y, _, _) = characteristics.key_grid.get_local_key_rect_xywh(key_index).unwrap();
+
+ let kind = if is_end {
+ context.active_touch_ids.remove(&touch_id);
+ KeyTouchEventKind::End
+ } else {
+ let is_new = context.active_touch_ids.insert(touch_id);
+ if is_new {
+ KeyTouchEventKind::Start
+ } else {
+ KeyTouchEventKind::Move
+ }
+ };
+
+ send_key_event(
+ path.clone(),
+ KeyEvent::Touch {
+ touch_id,
+ x: x - top_left_x,
+ y: y - top_left_y,
+ kind,
+ },
+ );
+
+ if kind == KeyTouchEventKind::Start {
+ send_key_event(path.clone(), KeyEvent::Press);
}
}
}
@@ -273,11 +301,14 @@ fn handle_event(
true
}
-fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) {
- debug!("Handling command: {:?}", &command);
+fn handle_command(context: &mut IoWorkerContext, command: IoWorkerCommand) {
+ trace!("Handling command: {:?}", &command);
match command {
- StateChangeCommand::SetActivePages { key_page_id, knob_page_id } => {
+ IoWorkerCommand::Vibrate { pattern } => {
+ context.device.vibrate(pattern);
+ }
+ IoWorkerCommand::SetActivePages { key_page_id, knob_page_id } => {
context.state.active_key_page_id = key_page_id;
context.state.active_knob_page_id = knob_page_id;
@@ -294,7 +325,7 @@ fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) {
context.device.refresh_display(&key_grid.display).unwrap();
}
- StateChangeCommand::SetKeyStyle { path, value } => {
+ IoWorkerCommand::SetKeyStyle { path, value } => {
context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
k.style = value;
});
diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs
index 10b5e64..35febfc 100644
--- a/deckster/src/runner/state.rs
+++ b/deckster/src/runner/state.rs
@@ -2,7 +2,6 @@ use std::collections::HashMap;
use enum_map::EnumMap;
use log::error;
-use serde::{Deserialize, Serialize};
use crate::model::key_page::KeyStyle;
use crate::model::knob_page::KnobStyle;
@@ -29,10 +28,6 @@ impl State {
None
}
- pub fn get_key(&self, path: &KeyPath) -> &Key {
- &self.key_pages_by_id[&path.page_id].keys_by_position[&path.position]
- }
-
pub fn active_key_page(&self) -> &KeyPage {
&self.key_pages_by_id[&self.active_key_page_id]
}
@@ -68,10 +63,3 @@ pub struct Knob {
pub style: KnobStyle,
pub value: f32,
}
-
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
-#[allow(clippy::enum_variant_names)]
-pub enum StateChangeCommand {
- SetActivePages { key_page_id: String, knob_page_id: String },
- SetKeyStyle { path: KeyPath, value: Option },
-}
diff --git a/loupedeck_serial/Cargo.toml b/loupedeck_serial/Cargo.toml
index fcbc3a3..5c5a5e6 100644
--- a/loupedeck_serial/Cargo.toml
+++ b/loupedeck_serial/Cargo.toml
@@ -10,4 +10,5 @@ enumset = "1.1.3"
bytes = "1.5.0"
thiserror = "1.0.52"
rgb = "0.8.37"
-flume = "0.11.0"
\ No newline at end of file
+flume = "0.11.0"
+serde = { version = "1.0.195", features = ["derive"] }
\ No newline at end of file
diff --git a/loupedeck_serial/src/commands.rs b/loupedeck_serial/src/commands.rs
index 9d3580f..d8a2b4e 100644
--- a/loupedeck_serial/src/commands.rs
+++ b/loupedeck_serial/src/commands.rs
@@ -1,11 +1,13 @@
use bytes::Bytes;
use enum_ordinalize::Ordinalize;
use rgb::RGB8;
+use serde::{Deserialize, Serialize};
use crate::characteristics::LoupedeckButton;
-#[derive(Debug, Ordinalize)]
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Ordinalize)]
#[repr(u8)]
+#[serde(rename_all = "kebab-case")]
pub enum VibrationPattern {
Short = 0x01,
Medium = 0x0a,