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",
|
||||
"log",
|
||||
"loupedeck_serial",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"resvg",
|
||||
"rgb",
|
||||
|
@ -857,6 +858,7 @@ dependencies = [
|
|||
"enumset",
|
||||
"flume",
|
||||
"rgb",
|
||||
"serde",
|
||||
"serialport",
|
||||
"thiserror",
|
||||
]
|
||||
|
|
|
@ -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"
|
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]
|
||||
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]"
|
|
@ -47,15 +47,15 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
|
|||
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<IconDescriptor> {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,11 +76,9 @@ pub struct KeyConfig {
|
|||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct KeyModes {
|
||||
pub vibrate: Option<Arc<modes::key::vibrate::Config>>,
|
||||
pub media__play_pause: Option<Arc<modes::key::media::PlayPauseConfig>>,
|
||||
pub media__previous: Option<Arc<modes::key::media::PreviousAndNextConfig>>,
|
||||
pub media__next: Option<Arc<modes::key::media::PreviousAndNextConfig>>,
|
||||
pub spotify__shuffle: Option<Arc<modes::key::spotify::ShuffleConfig>>,
|
||||
pub spotify__repeat: Option<Arc<modes::key::spotify::RepeatConfig>>,
|
||||
pub playerctl__button: Option<Arc<modes::key::playerctl::ButtonConfig>>,
|
||||
pub playerctl__shuffle: Option<Arc<modes::key::playerctl::ShuffleConfig>>,
|
||||
pub playerctl__loop: Option<Arc<modes::key::playerctl::LoopConfig>>,
|
||||
pub home_assistant__switch: Option<Arc<modes::key::home_assistant::SwitchConfig>>,
|
||||
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::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<Item = (KeyPath, Arc<model::key_page::KeyConfig>)>,
|
||||
events: broadcast::Sender<(KeyPath, KeyEvent)>,
|
||||
commands: flume::Sender<StateChangeCommand>,
|
||||
commands: flume::Sender<IoWorkerCommand>,
|
||||
) {
|
||||
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 {
|
||||
|
|
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 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<Config>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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::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::<StateChangeCommand>(20);
|
||||
let (commands_sender, commands_receiver) = flume::bounded::<IoWorkerCommand>(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<u8>,
|
||||
}
|
||||
|
||||
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<StateChangeCommand>,
|
||||
commands_receiver: flume::Receiver<StateChangeCommand>,
|
||||
commands_sender: flume::Sender<IoWorkerCommand>,
|
||||
commands_receiver: flume::Receiver<IoWorkerCommand>,
|
||||
) {
|
||||
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<StateChangeCommand>,
|
||||
context: &mut IoWorkerContext,
|
||||
commands_sender: &flume::Sender<IoWorkerCommand>,
|
||||
key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>,
|
||||
event: LoupedeckEvent,
|
||||
) -> bool {
|
||||
|
@ -236,14 +240,13 @@ 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 {
|
||||
LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
|
||||
let characteristics = context.device.characteristics();
|
||||
let display = characteristics.get_display_at_coordinates(x, y);
|
||||
|
||||
|
@ -261,7 +264,32 @@ fn handle_event(
|
|||
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;
|
||||
});
|
||||
|
|
|
@ -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<KeyStyle> },
|
||||
}
|
||||
|
|
|
@ -11,3 +11,4 @@ bytes = "1.5.0"
|
|||
thiserror = "1.0.52"
|
||||
rgb = "0.8.37"
|
||||
flume = "0.11.0"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue