commit
This commit is contained in:
parent
efb5385971
commit
521171cf85
20 changed files with 445 additions and 211 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
1
deckster/examples/full/icons/fad/repeat-one.svg
Normal file
1
deckster/examples/full/icons/fad/repeat-one.svg
Normal 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 |
1
deckster/examples/full/icons/fad/repeat.svg
Normal file
1
deckster/examples/full/icons/fad/repeat.svg
Normal 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 |
1
deckster/examples/full/icons/ph/play-pause.svg
Normal file
1
deckster/examples/full/icons/ph/play-pause.svg
Normal 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 |
1
deckster/examples/full/icons/ph/skip-back.svg
Normal file
1
deckster/examples/full/icons/ph/skip-back.svg
Normal 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 |
1
deckster/examples/full/icons/ph/skip-forward.svg
Normal file
1
deckster/examples/full/icons/ph/skip-forward.svg
Normal 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 |
|
@ -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]"
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
282
deckster/src/modes/key/playerctl.rs
Normal file
282
deckster/src/modes/key/playerctl.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
13
deckster/src/runner/command.rs
Normal file
13
deckster/src/runner/command.rs
Normal 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> },
|
||||||
|
}
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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> },
|
|
||||||
}
|
|
||||||
|
|
|
@ -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"] }
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue