This commit is contained in:
Moritz Ruth 2024-01-09 22:10:12 +01:00
parent efb5385971
commit 521171cf85
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
20 changed files with 445 additions and 211 deletions

2
Cargo.lock generated
View file

@ -365,6 +365,7 @@ dependencies = [
"humantime-serde", "humantime-serde",
"log", "log",
"loupedeck_serial", "loupedeck_serial",
"once_cell",
"regex", "regex",
"resvg", "resvg",
"rgb", "rgb",
@ -857,6 +858,7 @@ dependencies = [
"enumset", "enumset",
"flume", "flume",
"rgb", "rgb",
"serde",
"serialport", "serialport",
"thiserror", "thiserror",
] ]

View file

@ -28,3 +28,4 @@ tiny-skia = "0.11.3"
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]} tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]}
toml = "0.8.8" toml = "0.8.8"
walkdir = "2.4.0" walkdir = "2.4.0"
once_cell = "1.19.0"

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><g fill="currentColor" fill-rule="evenodd"><path d="M109.533 197.602a1.887 1.887 0 0 1-.034 2.76l-7.583 7.066a4.095 4.095 0 0 1-5.714-.152l-32.918-34.095c-1.537-1.592-1.54-4.162-.002-5.746l33.1-34.092c1.536-1.581 4.11-1.658 5.74-.18l7.655 6.94c.82.743.833 1.952.02 2.708l-21.11 19.659s53.036.129 71.708.064c18.672-.064 33.437-16.973 33.437-34.7c0-7.214-5.578-17.64-5.578-17.64c-.498-.99-.273-2.444.483-3.229l8.61-8.94c.764-.794 1.772-.632 2.242.364c0 0 9.212 18.651 9.212 28.562c0 28.035-21.765 50.882-48.533 50.882c-26.769 0-70.921.201-70.921.201z"/><path d="M144.398 58.435a1.887 1.887 0 0 1 .034-2.76l7.583-7.066a4.095 4.095 0 0 1 5.714.152l32.918 34.095c1.537 1.592 1.54 4.162.002 5.746l-33.1 34.092c-1.536 1.581-4.11 1.658-5.74.18l-7.656-6.94c-.819-.743-.832-1.952-.02-2.708l21.111-19.659s-53.036-.129-71.708-.064c-18.672.064-33.437 16.973-33.437 34.7c0 7.214 5.578 17.64 5.578 17.64c.498.99.273 2.444-.483 3.229l-8.61 8.94c-.764.794-1.772.632-2.242-.364c0 0-9.212-18.65-9.212-28.562c0-28.035 21.765-50.882 48.533-50.882c26.769 0 70.921-.201 70.921-.201z"/><path d="m127.992 104.543l6.53.146c1.105.025 2.013.945 2.027 2.037l.398 30.313a1.97 1.97 0 0 0 2.032 1.94l4.104-.103a1.951 1.951 0 0 1 2.01 1.958l.01 4.838a2.015 2.015 0 0 1-1.99 2.024l-21.14.147a1.982 1.982 0 0 1-1.994-1.983l-.002-4.71c0-1.103.897-1.997 1.996-1.997h4.254a2.018 2.018 0 0 0 2.016-1.994l.169-16.966l-6.047 5.912l-6.118-7.501z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><g fill="currentColor" fill-rule="evenodd"><path d="M109.533 197.602a1.887 1.887 0 0 1-.034 2.76l-7.583 7.066a4.095 4.095 0 0 1-5.714-.152l-32.918-34.095c-1.537-1.592-1.54-4.162-.002-5.746l33.1-34.092c1.536-1.581 4.11-1.658 5.74-.18l7.655 6.94c.82.743.833 1.952.02 2.708l-21.11 19.659s53.036.129 71.708.064c18.672-.064 33.437-16.973 33.437-34.7c0-7.214-5.578-17.64-5.578-17.64c-.498-.99-.273-2.444.483-3.229l8.61-8.94c.764-.794 1.772-.632 2.242.364c0 0 9.212 18.651 9.212 28.562c0 28.035-21.765 50.882-48.533 50.882c-26.769 0-70.921.201-70.921.201z"/><path d="M144.398 58.435a1.887 1.887 0 0 1 .034-2.76l7.583-7.066a4.095 4.095 0 0 1 5.714.152l32.918 34.095c1.537 1.592 1.54 4.162.002 5.746l-33.1 34.092c-1.536 1.581-4.11 1.658-5.74.18l-7.656-6.94c-.819-.743-.832-1.952-.02-2.708l21.111-19.659s-53.036-.129-71.708-.064c-18.672.064-33.437 16.973-33.437 34.7c0 7.214 5.578 17.64 5.578 17.64c.498.99.273 2.444-.483 3.229l-8.61 8.94c-.764.794-1.772.632-2.242-.364c0 0-9.212-18.65-9.212-28.562c0-28.035 21.765-50.882 48.533-50.882c26.769 0 70.921-.201 70.921-.201z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M184 64v128a8 8 0 0 1-16 0V64a8 8 0 0 1 16 0Zm40-8a8 8 0 0 0-8 8v128a8 8 0 0 0 16 0V64a8 8 0 0 0-8-8Zm-80 72a15.76 15.76 0 0 1-7.33 13.34l-88.19 56.15A15.91 15.91 0 0 1 24 184.15V71.85a15.91 15.91 0 0 1 24.48-13.34l88.19 56.15A15.76 15.76 0 0 1 144 128Zm-16.18 0L40 72.08v111.85Z"/></svg>

After

Width:  |  Height:  |  Size: 402 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M199.81 34a16 16 0 0 0-16.24.43L64 109.23V40a8 8 0 0 0-16 0v176a8 8 0 0 0 16 0v-69.23l119.57 74.78A15.95 15.95 0 0 0 208 208.12V47.88A15.86 15.86 0 0 0 199.81 34ZM192 208L64.16 128L192 48.07Z"/></svg>

After

Width:  |  Height:  |  Size: 314 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M200 32a8 8 0 0 0-8 8v69.23L72.43 34.45A15.95 15.95 0 0 0 48 47.88v160.24a16 16 0 0 0 24.43 13.43L192 146.77V216a8 8 0 0 0 16 0V40a8 8 0 0 0-8-8ZM64 207.93V48.05l127.84 80Z"/></svg>

After

Width:  |  Height:  |  Size: 295 B

View file

@ -1,17 +1,31 @@
[keys.1x1] [keys.1x1]
icon = "@ph/play[alpha=0.4]" icon = "@ph/skip-back"
mode.vibrate.pattern = "low" mode.playerctl__button.command = "previous"
mode.media__play_pause.style.paused.icon = "@ph/play" mode.playerctl__button.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
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]"
[keys.2x1] [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]" icon = "@ph/timer[color=#ff0000]"
mode.vibrate.pattern = "low"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"] mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_when_finished = true mode.timer.vibrate_when_finished = true
mode.timer.needy = true mode.timer.needy = true
@ -20,13 +34,11 @@ mode.timer.needy = true
icon = "@fad/thunderbolt" icon = "@fad/thunderbolt"
label = "Dock" label = "Dock"
border= "#00ff00" border= "#00ff00"
mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock" mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock"
mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]" mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]"
[keys.4x3] [keys.4x3]
icon = "@ph/computer-tower" icon = "@ph/computer-tower"
label = "Tower PC unnötig lang" label = "Tower PC unnötig lang"
mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.mwin" mode.home_assistant__switch.name = "switch.mwin"
mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]" mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]"

View file

@ -47,15 +47,15 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
result.insert(d.clone()); 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); 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); 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); 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<IconDescriptor> {
if let Some(c) = &key.mode.home_assistant__switch { if let Some(c) = &key.mode.home_assistant__switch {
insert_all_from_key_style_by_state_map(&mut result, &c.style); 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);
}
} }
} }

View file

@ -76,11 +76,9 @@ pub struct KeyConfig {
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct KeyModes { pub struct KeyModes {
pub vibrate: Option<Arc<modes::key::vibrate::Config>>, pub vibrate: Option<Arc<modes::key::vibrate::Config>>,
pub media__play_pause: Option<Arc<modes::key::media::PlayPauseConfig>>, pub playerctl__button: Option<Arc<modes::key::playerctl::ButtonConfig>>,
pub media__previous: Option<Arc<modes::key::media::PreviousAndNextConfig>>, pub playerctl__shuffle: Option<Arc<modes::key::playerctl::ShuffleConfig>>,
pub media__next: Option<Arc<modes::key::media::PreviousAndNextConfig>>, pub playerctl__loop: Option<Arc<modes::key::playerctl::LoopConfig>>,
pub spotify__shuffle: Option<Arc<modes::key::spotify::ShuffleConfig>>,
pub spotify__repeat: Option<Arc<modes::key::spotify::RepeatConfig>>,
pub home_assistant__switch: Option<Arc<modes::key::home_assistant::SwitchConfig>>, pub home_assistant__switch: Option<Arc<modes::key::home_assistant::SwitchConfig>>,
pub home_assistant__button: Option<Arc<modes::key::home_assistant::ButtonConfig>>, pub home_assistant__button: Option<Arc<modes::key::home_assistant::ButtonConfig>>,
} }

View file

@ -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<PlayPauseState>,
#[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<PreviousAndNextState>,
}
#[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<PlayPauseConfig>, mut events: Receiver<KeyEvent>, commands: Sender<StateChangeCommand>) {
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();
}
}
}

View file

@ -5,31 +5,50 @@ use tokio::sync::broadcast;
use crate::model; use crate::model;
use crate::model::position::KeyPath; use crate::model::position::KeyPath;
use crate::runner::state::StateChangeCommand; use crate::runner::command::IoWorkerCommand;
pub mod home_assistant; pub mod home_assistant;
pub mod media; pub mod playerctl;
pub mod spotify;
pub mod timer; pub mod timer;
pub mod vibrate; pub mod vibrate;
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum KeyTouchEventKind {
Start,
Move,
End,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KeyEvent { pub enum KeyEvent {
Press, Press,
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
VisibilityChange { is_visible: bool }, VisibilityChange { is_visible: bool },
} }
pub fn start_handlers( pub fn start_handlers(
keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::KeyConfig>)>, keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::KeyConfig>)>,
events: broadcast::Sender<(KeyPath, KeyEvent)>, events: broadcast::Sender<(KeyPath, KeyEvent)>,
commands: flume::Sender<StateChangeCommand>, commands: flume::Sender<IoWorkerCommand>,
) { ) {
for (path, config) in keys { for (path, config) in keys {
let mut events = events.subscribe(); let mut events = events.subscribe();
let own_events = broadcast::Sender::new(5); let own_events = broadcast::Sender::new(5);
if let Some(c) = &config.mode.media__play_pause { if let Some(c) = &config.mode.playerctl__button {
tokio::spawn(media::handle(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone())); 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 { tokio::spawn(async move {

View file

@ -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<ButtonState>,
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<ShuffleState>,
}
#[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<LoopState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LoopState {
Inactive,
None,
Single,
All,
}
struct PlayerctlStateWatcher<S: Clone + Debug + Send + Sync + 'static> {
state: Arc<RwLock<S>>,
new_states: broadcast::Sender<S>,
}
impl<S: Clone + Debug + Send + Sync + 'static> PlayerctlStateWatcher<S> {
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<S> {
let r = self.new_states.subscribe();
self.new_states.send(self.state.read().unwrap().clone()).unwrap();
r
}
}
static STATE_WATCHER_PLAYING: Lazy<PlayerctlStateWatcher<ButtonState>> = 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<PlayerctlStateWatcher<ShuffleState>> = 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<PlayerctlStateWatcher<LoopState>> = 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<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
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<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
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<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
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)
}
}
}
}
}
}

View file

@ -1,30 +0,0 @@
use serde::Deserialize;
use crate::model::key_page::StyleByStateMap;
#[derive(Debug, Deserialize)]
pub struct ShuffleConfig {
#[serde(default)]
pub style: StyleByStateMap<ShuffleState>,
}
#[derive(Debug, Deserialize)]
pub struct RepeatConfig {
#[serde(default)]
pub style: StyleByStateMap<RepeatState>,
}
#[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,
}

View file

@ -1,39 +1,25 @@
use std::sync::Arc;
use serde::Deserialize; 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)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub pattern: VibrationPattern, pub pattern: VibrationPattern,
} }
#[derive(Debug, Eq, PartialEq, Deserialize)] pub async fn handle(_: KeyPath, config: Arc<Config>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
#[serde(rename_all = "kebab-case")] while let Ok(event) = events.recv().await {
pub enum VibrationPattern { if let KeyEvent::Touch { kind, .. } = event {
Short, if kind == KeyTouchEventKind::Start {
Medium, commands.send(IoWorkerCommand::Vibrate { pattern: config.pattern }).unwrap();
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,
} }

View file

@ -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<KeyStyle> },
}

View file

@ -1,5 +1,5 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
@ -9,11 +9,12 @@ use color_eyre::eyre::{ContextCompat, WrapErr};
use color_eyre::Result; use color_eyre::Result;
use enum_map::EnumMap; use enum_map::EnumMap;
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use log::{debug, info, trace}; use log::{info, trace};
use rgb::RGB8; use rgb::RGB8;
use tiny_skia::IntSize; use tiny_skia::IntSize;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use command::IoWorkerCommand;
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics}; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics};
use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice; 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;
use crate::model::knob_page::KnobStyle; use crate::model::knob_page::KnobStyle;
use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; 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::labels::LabelRenderer;
use crate::runner::graphics::{render_key, GraphicsContext}; 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; mod graphics;
pub mod state; 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.set_brightness(0.5);
device.vibrate(VibrationPattern::RiseFall); device.vibrate(VibrationPattern::RiseFall);
let (commands_sender, commands_receiver) = flume::bounded::<StateChangeCommand>(20); let (commands_sender, commands_receiver) = flume::bounded::<IoWorkerCommand>(20);
let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20); let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20);
commands_sender commands_sender
.send(StateChangeCommand::SetActivePages { .send(IoWorkerCommand::SetActivePages {
knob_page_id: config.initial.knob_page.clone(), knob_page_id: config.initial.knob_page.clone(),
key_page_id: config.initial.key_page.clone(), key_page_id: config.initial.key_page.clone(),
}) })
@ -157,7 +159,7 @@ fn create_state(config: &model::config::Config) -> State {
enum IoWork { enum IoWork {
Event(LoupedeckEvent), Event(LoupedeckEvent),
Command(StateChangeCommand), Command(IoWorkerCommand),
} }
struct IoWorkerContext { struct IoWorkerContext {
@ -165,6 +167,7 @@ struct IoWorkerContext {
device: LoupedeckDevice, device: LoupedeckDevice,
state: State, state: State,
graphics: GraphicsContext, graphics: GraphicsContext,
active_touch_ids: HashSet<u8>,
} }
fn do_io_work( fn do_io_work(
@ -172,8 +175,8 @@ fn do_io_work(
icons: LoadedIconsMap, icons: LoadedIconsMap,
device: LoupedeckDevice, device: LoupedeckDevice,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
commands_sender: flume::Sender<StateChangeCommand>, commands_sender: flume::Sender<IoWorkerCommand>,
commands_receiver: flume::Receiver<StateChangeCommand>, commands_receiver: flume::Receiver<IoWorkerCommand>,
) { ) {
let state = create_state(&config); let state = create_state(&config);
let buffer_endianness = device.characteristics().key_grid.display.endianness; let buffer_endianness = device.characteristics().key_grid.display.endianness;
@ -197,6 +200,7 @@ fn do_io_work(
label_renderer, label_renderer,
global_icon_filter_by_pack_id, global_icon_filter_by_pack_id,
}, },
active_touch_ids: HashSet::new(),
}; };
loop { loop {
@ -207,7 +211,7 @@ fn do_io_work(
match a { match a {
IoWork::Event(event) => { 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; break;
} }
} }
@ -217,8 +221,8 @@ fn do_io_work(
} }
fn handle_event( fn handle_event(
context: &IoWorkerContext, context: &mut IoWorkerContext,
commands_sender: &flume::Sender<StateChangeCommand>, commands_sender: &flume::Sender<IoWorkerCommand>,
key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>, key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>,
event: LoupedeckEvent, event: LoupedeckEvent,
) -> bool { ) -> bool {
@ -236,14 +240,13 @@ fn handle_event(
let button_config = &context.config.buttons[position]; let button_config = &context.config.buttons[position];
commands_sender 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(), 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(), knob_page_id: button_config.knob_page.as_ref().unwrap_or(&context.state.active_knob_page_id).clone(),
}) })
.unwrap() .unwrap()
} }
LoupedeckEvent::Touch { x, y, is_end, .. } => { LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
if is_end {
let characteristics = context.device.characteristics(); let characteristics = context.device.characteristics();
let display = characteristics.get_display_at_coordinates(x, y); let display = characteristics.get_display_at_coordinates(x, y);
@ -261,7 +264,32 @@ fn handle_event(
position, 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 true
} }
fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) { fn handle_command(context: &mut IoWorkerContext, command: IoWorkerCommand) {
debug!("Handling command: {:?}", &command); trace!("Handling command: {:?}", &command);
match 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_key_page_id = key_page_id;
context.state.active_knob_page_id = knob_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(); 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| { context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
k.style = value; k.style = value;
}); });

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use enum_map::EnumMap; use enum_map::EnumMap;
use log::error; use log::error;
use serde::{Deserialize, Serialize};
use crate::model::key_page::KeyStyle; use crate::model::key_page::KeyStyle;
use crate::model::knob_page::KnobStyle; use crate::model::knob_page::KnobStyle;
@ -29,10 +28,6 @@ impl State {
None 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 { pub fn active_key_page(&self) -> &KeyPage {
&self.key_pages_by_id[&self.active_key_page_id] &self.key_pages_by_id[&self.active_key_page_id]
} }
@ -68,10 +63,3 @@ pub struct Knob {
pub style: KnobStyle, pub style: KnobStyle,
pub value: f32, 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<KeyStyle> },
}

View file

@ -11,3 +11,4 @@ bytes = "1.5.0"
thiserror = "1.0.52" thiserror = "1.0.52"
rgb = "0.8.37" rgb = "0.8.37"
flume = "0.11.0" flume = "0.11.0"
serde = { version = "1.0.195", features = ["derive"] }

View file

@ -1,11 +1,13 @@
use bytes::Bytes; use bytes::Bytes;
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use rgb::RGB8; use rgb::RGB8;
use serde::{Deserialize, Serialize};
use crate::characteristics::LoupedeckButton; use crate::characteristics::LoupedeckButton;
#[derive(Debug, Ordinalize)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Ordinalize)]
#[repr(u8)] #[repr(u8)]
#[serde(rename_all = "kebab-case")]
pub enum VibrationPattern { pub enum VibrationPattern {
Short = 0x01, Short = 0x01,
Medium = 0x0a, Medium = 0x0a,