diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index 2afffcf..9c03d19 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -20,7 +20,7 @@ loupedeck_serial = { path = "../loupedeck_serial" } regex = "1.10.2" resvg = "0.37.0" rgb = "0.8.37" -serde = { version = "1.0.193", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive", "rc"] } serde_regex = "1.1.0" serde_with = "3.4.0" thiserror = "1.0.52" diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index b111805..6edc056 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -1,8 +1,8 @@ [keys.1x1] -icon = "@apps/spotify[scale=2.0|invert]" +icon = "@ph/play[alpha=0.4]" mode.vibrate.pattern = "low" -mode.media__play_pause.icon.paused = "@ph/play" -mode.media__play_pause.icon.playing = "@ph/pause" +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]" diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 37de021..fd5bb89 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Arc; use clap::{Parser, Subcommand}; use color_eyre::eyre::WrapErr; @@ -11,6 +12,7 @@ use crate::model::config::WithFallbackId; mod icons; mod model; +mod modes; mod runner; #[derive(Debug, Parser)] @@ -44,7 +46,7 @@ pub async fn main() -> Result<()> { .into_iter() .map(|p| model::key_page::Page { id: p.inner.id.clone().unwrap_or(p.fallback_id), - keys: p.inner.keys, + keys: p.inner.keys.into_iter().map(|(p, k)| (p, Arc::new(k))).collect(), scrolling: p.inner.scrolling, }) .map(|p| (p.id.clone(), p)) diff --git a/deckster/src/model/key_modes/media.rs b/deckster/src/model/key_modes/media.rs deleted file mode 100644 index 6470f76..0000000 --- a/deckster/src/model/key_modes/media.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::Deserialize; - -use crate::model::key_page::StyleByStateMap; - -#[derive(Debug, Deserialize)] -pub struct PlayPauseConfig { - #[serde(default)] - pub style: StyleByStateMap, - #[serde(default)] - pub action: PlayPauseAction, -} - -#[derive(Debug, Default, Eq, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum PlayPauseAction { - #[default] - Toggle, - Play, - Pause, -} - -#[derive(Debug, Deserialize)] -pub struct PreviousAndNextConfig { - #[serde(default)] - pub style: StyleByStateMap, -} - -#[derive(Debug, 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, -} diff --git a/deckster/src/model/key_modes/mod.rs b/deckster/src/model/key_modes/mod.rs deleted file mode 100644 index 9febb99..0000000 --- a/deckster/src/model/key_modes/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod home_assistant; -pub mod media; -pub mod spotify; -pub mod timer; -pub mod vibrate; diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index f22e4cf..a95558b 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; +use std::sync::Arc; use serde::{Deserialize, Serialize}; use crate::model::geometry::UIntVec2; use crate::model::icon_descriptor::IconDescriptor; -use crate::model::key_modes; use crate::model::position::{KeyPosition, KnobPosition}; use crate::model::rgb::RGB8WithOptionalA; +use crate::modes; #[derive(Debug, Deserialize)] pub struct File { @@ -19,7 +20,7 @@ pub struct File { pub struct Page { pub id: String, pub scrolling: Option, - pub keys: HashMap, + pub keys: HashMap>, } #[derive(Debug, Deserialize)] @@ -74,14 +75,14 @@ pub struct KeyConfig { #[allow(non_snake_case)] #[derive(Debug, Default, Deserialize)] pub struct KeyModes { - pub vibrate: Option, - pub media__play_pause: Option, - pub media__previous: Option, - pub media__next: Option, - pub spotify__shuffle: Option, - pub spotify__repeat: Option, - pub home_assistant__switch: Option, - pub home_assistant__button: Option, + pub vibrate: Option>, + pub media__play_pause: Option>, + pub media__previous: Option>, + pub media__next: Option>, + pub spotify__shuffle: Option>, + pub spotify__repeat: Option>, + pub home_assistant__switch: Option>, + pub home_assistant__button: Option>, } pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index 858a767..b7eb924 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -4,9 +4,9 @@ use enum_map::EnumMap; use serde::Deserialize; use crate::model::icon_descriptor::IconDescriptor; -use crate::model::knob_modes; use crate::model::position::KnobPosition; use crate::model::rgb::RGB8WithOptionalA; +use crate::modes; #[derive(Debug, Deserialize)] pub struct File { @@ -55,7 +55,7 @@ pub struct KnobStyle { #[derive(Debug, Default, Deserialize)] pub struct KnobModes { - pub audio_volume: Option, + pub audio_volume: Option, } pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index c45e844..dc0a1c1 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -2,9 +2,7 @@ pub mod config; pub mod geometry; pub mod icon_descriptor; pub mod image_filter; -pub mod key_modes; pub mod key_page; -pub mod knob_modes; pub mod knob_page; pub mod position; pub mod rgb; diff --git a/deckster/src/model/position.rs b/deckster/src/model/position.rs index 1e0aeb9..cafdc67 100644 --- a/deckster/src/model/position.rs +++ b/deckster/src/model/position.rs @@ -49,6 +49,12 @@ pub struct KeyPath { pub position: KeyPosition, } +impl Display for KeyPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}/{}", &self.page_id, &self.position)) + } +} + #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)] #[serde(rename_all = "kebab-case")] pub enum KnobPosition { diff --git a/deckster/src/model/key_modes/home_assistant.rs b/deckster/src/modes/key/home_assistant.rs similarity index 100% rename from deckster/src/model/key_modes/home_assistant.rs rename to deckster/src/modes/key/home_assistant.rs diff --git a/deckster/src/modes/key/media.rs b/deckster/src/modes/key/media.rs new file mode 100644 index 0000000..03ca5bf --- /dev/null +++ b/deckster/src/modes/key/media.rs @@ -0,0 +1,68 @@ +use std::sync::Arc; + +use flume::Sender; +use serde::Deserialize; +use tokio::sync::broadcast::Receiver; + +use crate::model::key_page::StyleByStateMap; +use crate::model::position::KeyPath; +use crate::modes::key::KeyEvent; +use crate::runner::state::StateChangeCommand; + +#[derive(Debug, Deserialize)] +pub struct PlayPauseConfig { + #[serde(default)] + pub style: StyleByStateMap, + #[serde(default)] + pub action: PlayPauseAction, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PlayPauseAction { + #[default] + Toggle, + Play, + Pause, +} + +#[derive(Debug, Deserialize)] +pub struct PreviousAndNextConfig { + #[serde(default)] + pub style: StyleByStateMap, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PlayPauseState { + Inactive, + Playing, + Paused, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PreviousAndNextState { + Inactive, + Active, +} + +pub async fn handle(path: KeyPath, config: Arc, mut events: Receiver, commands: Sender) { + let mut state = PlayPauseState::Inactive; + + while let Ok(event) = events.recv().await { + if event == KeyEvent::Press { + state = match state { + PlayPauseState::Playing => PlayPauseState::Paused, + _ => PlayPauseState::Playing, + }; + + commands + .send(StateChangeCommand::SetKeyStyle { + path: path.clone(), + value: config.style.get(&state).cloned(), + }) + .unwrap(); + } + } +} diff --git a/deckster/src/modes/key/mod.rs b/deckster/src/modes/key/mod.rs new file mode 100644 index 0000000..d8ceeae --- /dev/null +++ b/deckster/src/modes/key/mod.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; + +use crate::model; +use crate::model::position::KeyPath; +use crate::runner::state::StateChangeCommand; + +pub mod home_assistant; +pub mod media; +pub mod spotify; +pub mod timer; +pub mod vibrate; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum KeyEvent { + Press, + VisibilityChange { is_visible: bool }, +} + +pub fn start_handlers( + keys: impl Iterator)>, + events: broadcast::Sender<(KeyPath, KeyEvent)>, + commands: flume::Sender, +) { + for (path, config) in keys { + let mut events = events.subscribe(); + let own_events = broadcast::Sender::new(5); + + if let Some(c) = &config.mode.media__play_pause { + tokio::spawn(media::handle(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone())); + } + + tokio::spawn(async move { + while let Ok((p, e)) = events.recv().await { + #[allow(clippy::collapsible_if)] + if p == path { + if own_events.send(e).is_err() { + break; + } + } + } + }); + } +} diff --git a/deckster/src/model/key_modes/spotify.rs b/deckster/src/modes/key/spotify.rs similarity index 100% rename from deckster/src/model/key_modes/spotify.rs rename to deckster/src/modes/key/spotify.rs diff --git a/deckster/src/model/key_modes/timer.rs b/deckster/src/modes/key/timer.rs similarity index 100% rename from deckster/src/model/key_modes/timer.rs rename to deckster/src/modes/key/timer.rs diff --git a/deckster/src/model/key_modes/vibrate.rs b/deckster/src/modes/key/vibrate.rs similarity index 100% rename from deckster/src/model/key_modes/vibrate.rs rename to deckster/src/modes/key/vibrate.rs diff --git a/deckster/src/model/knob_modes/audio_volume.rs b/deckster/src/modes/knob/audio_volume.rs similarity index 100% rename from deckster/src/model/knob_modes/audio_volume.rs rename to deckster/src/modes/knob/audio_volume.rs diff --git a/deckster/src/model/knob_modes/mod.rs b/deckster/src/modes/knob/mod.rs similarity index 100% rename from deckster/src/model/knob_modes/mod.rs rename to deckster/src/modes/knob/mod.rs diff --git a/deckster/src/modes/mod.rs b/deckster/src/modes/mod.rs new file mode 100644 index 0000000..f5682d4 --- /dev/null +++ b/deckster/src/modes/mod.rs @@ -0,0 +1,2 @@ +pub mod key; +pub mod knob; diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index c7bffa3..76f6ce1 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -26,9 +26,10 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap(); if let Some(state) = state { - let style = state.style.merge_over(&state.base_style); + let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); + let style = style.as_ref().unwrap_or(&state.base_style); - if let Some(icon) = style.icon { + if let Some(icon) = &style.icon { let filter = if let Some(global_filter) = icon.source.pack_id().and_then(|i| context.global_icon_filter_by_pack_id.get(i)) { icon.filter.merge_over(global_filter) } else { @@ -62,7 +63,7 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K ); } - if let Some(label) = style.label { + if let Some(label) = &style.label { if !label.is_empty() { context.label_renderer.borrow_mut().render(&mut pixmap, &label); } diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 4c626c5..1a73d53 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -9,10 +9,10 @@ use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; use enum_map::EnumMap; use enum_ordinalize::Ordinalize; -use flume::{Receiver, Sender}; use log::{debug, info, trace}; use rgb::RGB8; use tiny_skia::IntSize; +use tokio::sync::broadcast; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics}; use loupedeck_serial::commands::VibrationPattern; @@ -21,15 +21,15 @@ use loupedeck_serial::events::LoupedeckEvent; use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap}; use crate::model; -use crate::model::key_page::KeyStyle; use crate::model::knob_page::KnobStyle; use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; +use crate::modes::key::{start_handlers, KeyEvent}; use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::{render_key, GraphicsContext}; use crate::runner::state::{Key, State, StateChangeCommand}; mod graphics; -mod state; +pub mod state; pub async fn start(config_directory: &Path, config: model::config::Config) -> Result<()> { let config = Arc::new(config); @@ -40,11 +40,9 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re info!("Connecting to the deviceā€¦"); let device = available_device.connect().wrap_err("Connecting to the device failed.")?; - info!("Connected"); let key_grid = &device.characteristics().key_grid; - let used_icon_descriptors = get_used_icon_descriptors(&config); let start_time = Instant::now(); @@ -55,8 +53,8 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re device.set_brightness(0.5); device.vibrate(VibrationPattern::RiseFall); - let events_receiver = device.events(); let (commands_sender, commands_receiver) = flume::bounded::(20); + let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20); commands_sender .send(StateChangeCommand::SetActivePages { @@ -67,11 +65,37 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re let cloned_config = Arc::clone(&config); let cloned_commands_sender = commands_sender.clone(); + let cloned_key_events_sender = key_events_sender.clone(); let io_worker_thread = thread::Builder::new() .name("deckster IO worker".to_owned()) - .spawn(move || do_io_work(cloned_config, icons, device, events_receiver, cloned_commands_sender, commands_receiver)) + .spawn(move || { + do_io_work( + cloned_config, + icons, + device, + cloned_key_events_sender, + cloned_commands_sender, + commands_receiver, + ) + }) .wrap_err("Could not spawn the worker thread")?; + start_handlers( + config.key_pages_by_id.iter().flat_map(|(page_id, page)| { + page.keys.iter().map(|(position, key)| { + ( + KeyPath { + page_id: page_id.clone(), + position: *position, + }, + Arc::clone(key), + ) + }) + }), + key_events_sender, + commands_sender, + ); + info!("Ready."); io_worker_thread.join().unwrap(); @@ -93,7 +117,7 @@ fn create_state(config: &model::config::Config) -> State { position: *position, }, base_style: k.base_style.clone(), - style: KeyStyle::default(), + style: None, }) .map(|k| (k.path.position, k)) .collect(), @@ -147,9 +171,9 @@ fn do_io_work( config: Arc, icons: LoadedIconsMap, device: LoupedeckDevice, - events_receiver: Receiver, - commands_sender: Sender, - commands_receiver: Receiver, + key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, + commands_sender: flume::Sender, + commands_receiver: flume::Receiver, ) { let state = create_state(&config); let buffer_endianness = device.characteristics().key_grid.display.endianness; @@ -161,6 +185,8 @@ fn do_io_work( let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref())); + let device_events_receiver = device.events(); + let mut context = IoWorkerContext { config, device, @@ -175,13 +201,13 @@ fn do_io_work( loop { let a = flume::Selector::new() - .recv(&events_receiver, |e| IoWork::Event(e.unwrap())) + .recv(&device_events_receiver, |e| IoWork::Event(e.unwrap())) .recv(&commands_receiver, |c| IoWork::Command(c.unwrap())) .wait(); match a { IoWork::Event(event) => { - if !handle_event(&context, &commands_sender, event) { + if !handle_event(&context, &commands_sender, &key_events_sender, event) { break; } } @@ -190,9 +216,19 @@ fn do_io_work( } } -fn handle_event(context: &IoWorkerContext, commands_sender: &Sender, event: LoupedeckEvent) -> bool { +fn handle_event( + context: &IoWorkerContext, + commands_sender: &flume::Sender, + key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>, + event: LoupedeckEvent, +) -> bool { trace!("Handling event: {:?}", &event); + let send_key_event = |path: KeyPath, event: KeyEvent| { + trace!("Sending key event ({}): {:?}", &path, &event); + key_events_sender.send((path, event)).unwrap(); + }; + match event { LoupedeckEvent::Disconnected => return false, LoupedeckEvent::ButtonDown { button } => { @@ -224,6 +260,8 @@ fn handle_event(context: &IoWorkerContext, commands_sender: &Sender, } #[derive(Debug)] @@ -73,5 +73,5 @@ pub struct Knob { #[allow(clippy::enum_variant_names)] pub enum StateChangeCommand { SetActivePages { key_page_id: String, knob_page_id: String }, - SetKeyStyle { path: KeyPath, value: KeyStyle }, + SetKeyStyle { path: KeyPath, value: Option }, }