This commit is contained in:
Moritz Ruth 2024-01-09 01:50:25 +01:00
parent 2719b7afb8
commit efb5385971
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
21 changed files with 200 additions and 84 deletions

View file

@ -20,7 +20,7 @@ loupedeck_serial = { path = "../loupedeck_serial" }
regex = "1.10.2" regex = "1.10.2"
resvg = "0.37.0" resvg = "0.37.0"
rgb = "0.8.37" 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_regex = "1.1.0"
serde_with = "3.4.0" serde_with = "3.4.0"
thiserror = "1.0.52" thiserror = "1.0.52"

View file

@ -1,8 +1,8 @@
[keys.1x1] [keys.1x1]
icon = "@apps/spotify[scale=2.0|invert]" icon = "@ph/play[alpha=0.4]"
mode.vibrate.pattern = "low" mode.vibrate.pattern = "low"
mode.media__play_pause.icon.paused = "@ph/play" mode.media__play_pause.style.paused.icon = "@ph/play"
mode.media__play_pause.icon.playing = "@ph/pause" mode.media__play_pause.style.playing.icon = "@ph/pause"
[keys.1x2] [keys.1x2]
icon = "@fad/shuffle[alpha=0.6]" icon = "@fad/shuffle[alpha=0.6]"

View file

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use color_eyre::eyre::WrapErr; use color_eyre::eyre::WrapErr;
@ -11,6 +12,7 @@ use crate::model::config::WithFallbackId;
mod icons; mod icons;
mod model; mod model;
mod modes;
mod runner; mod runner;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -44,7 +46,7 @@ pub async fn main() -> Result<()> {
.into_iter() .into_iter()
.map(|p| model::key_page::Page { .map(|p| model::key_page::Page {
id: p.inner.id.clone().unwrap_or(p.fallback_id), 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, scrolling: p.inner.scrolling,
}) })
.map(|p| (p.id.clone(), p)) .map(|p| (p.id.clone(), p))

View file

@ -1,41 +0,0 @@
use serde::Deserialize;
use crate::model::key_page::StyleByStateMap;
#[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, 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,
}

View file

@ -1,5 +0,0 @@
pub mod home_assistant;
pub mod media;
pub mod spotify;
pub mod timer;
pub mod vibrate;

View file

@ -1,12 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::model::geometry::UIntVec2; use crate::model::geometry::UIntVec2;
use crate::model::icon_descriptor::IconDescriptor; use crate::model::icon_descriptor::IconDescriptor;
use crate::model::key_modes;
use crate::model::position::{KeyPosition, KnobPosition}; use crate::model::position::{KeyPosition, KnobPosition};
use crate::model::rgb::RGB8WithOptionalA; use crate::model::rgb::RGB8WithOptionalA;
use crate::modes;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct File { pub struct File {
@ -19,7 +20,7 @@ pub struct File {
pub struct Page { pub struct Page {
pub id: String, pub id: String,
pub scrolling: Option<ScrollingConfig>, pub scrolling: Option<ScrollingConfig>,
pub keys: HashMap<KeyPosition, KeyConfig>, pub keys: HashMap<KeyPosition, Arc<KeyConfig>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -74,14 +75,14 @@ pub struct KeyConfig {
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct KeyModes { pub struct KeyModes {
pub vibrate: Option<key_modes::vibrate::Config>, pub vibrate: Option<Arc<modes::key::vibrate::Config>>,
pub media__play_pause: Option<key_modes::media::PlayPauseConfig>, pub media__play_pause: Option<Arc<modes::key::media::PlayPauseConfig>>,
pub media__previous: Option<key_modes::media::PreviousAndNextConfig>, pub media__previous: Option<Arc<modes::key::media::PreviousAndNextConfig>>,
pub media__next: Option<key_modes::media::PreviousAndNextConfig>, pub media__next: Option<Arc<modes::key::media::PreviousAndNextConfig>>,
pub spotify__shuffle: Option<key_modes::spotify::ShuffleConfig>, pub spotify__shuffle: Option<Arc<modes::key::spotify::ShuffleConfig>>,
pub spotify__repeat: Option<key_modes::spotify::RepeatConfig>, pub spotify__repeat: Option<Arc<modes::key::spotify::RepeatConfig>>,
pub home_assistant__switch: Option<key_modes::home_assistant::SwitchConfig>, pub home_assistant__switch: Option<Arc<modes::key::home_assistant::SwitchConfig>>,
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>, pub home_assistant__button: Option<Arc<modes::key::home_assistant::ButtonConfig>>,
} }
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>; pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;

View file

@ -4,9 +4,9 @@ use enum_map::EnumMap;
use serde::Deserialize; use serde::Deserialize;
use crate::model::icon_descriptor::IconDescriptor; use crate::model::icon_descriptor::IconDescriptor;
use crate::model::knob_modes;
use crate::model::position::KnobPosition; use crate::model::position::KnobPosition;
use crate::model::rgb::RGB8WithOptionalA; use crate::model::rgb::RGB8WithOptionalA;
use crate::modes;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct File { pub struct File {
@ -55,7 +55,7 @@ pub struct KnobStyle {
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct KnobModes { pub struct KnobModes {
pub audio_volume: Option<knob_modes::audio_volume::Config>, pub audio_volume: Option<modes::knob::audio_volume::Config>,
} }
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>; pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -2,9 +2,7 @@ pub mod config;
pub mod geometry; pub mod geometry;
pub mod icon_descriptor; pub mod icon_descriptor;
pub mod image_filter; pub mod image_filter;
pub mod key_modes;
pub mod key_page; pub mod key_page;
pub mod knob_modes;
pub mod knob_page; pub mod knob_page;
pub mod position; pub mod position;
pub mod rgb; pub mod rgb;

View file

@ -49,6 +49,12 @@ pub struct KeyPath {
pub position: KeyPosition, 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)] #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum KnobPosition { pub enum KnobPosition {

View file

@ -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<PlayPauseState>,
#[serde(default)]
pub action: PlayPauseAction,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PlayPauseAction {
#[default]
Toggle,
Play,
Pause,
}
#[derive(Debug, Deserialize)]
pub struct PreviousAndNextConfig {
#[serde(default)]
pub style: StyleByStateMap<PreviousAndNextState>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PlayPauseState {
Inactive,
Playing,
Paused,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PreviousAndNextState {
Inactive,
Active,
}
pub async fn handle(path: KeyPath, config: Arc<PlayPauseConfig>, mut events: Receiver<KeyEvent>, commands: Sender<StateChangeCommand>) {
let mut state = PlayPauseState::Inactive;
while let Ok(event) = events.recv().await {
if event == KeyEvent::Press {
state = match state {
PlayPauseState::Playing => PlayPauseState::Paused,
_ => PlayPauseState::Playing,
};
commands
.send(StateChangeCommand::SetKeyStyle {
path: path.clone(),
value: config.style.get(&state).cloned(),
})
.unwrap();
}
}
}

View file

@ -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<Item = (KeyPath, Arc<model::key_page::KeyConfig>)>,
events: broadcast::Sender<(KeyPath, KeyEvent)>,
commands: flume::Sender<StateChangeCommand>,
) {
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;
}
}
}
});
}
}

View file

@ -0,0 +1,2 @@
pub mod key;
pub mod knob;

View file

@ -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(); let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap();
if let Some(state) = state { 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)) { 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) icon.filter.merge_over(global_filter)
} else { } 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() { if !label.is_empty() {
context.label_renderer.borrow_mut().render(&mut pixmap, &label); context.label_renderer.borrow_mut().render(&mut pixmap, &label);
} }

View file

@ -9,10 +9,10 @@ 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 flume::{Receiver, Sender};
use log::{debug, info, trace}; use log::{debug, info, trace};
use rgb::RGB8; use rgb::RGB8;
use tiny_skia::IntSize; use tiny_skia::IntSize;
use tokio::sync::broadcast;
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics}; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics};
use loupedeck_serial::commands::VibrationPattern; 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::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap};
use crate::model; use crate::model;
use crate::model::key_page::KeyStyle;
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::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, StateChangeCommand};
mod graphics; mod graphics;
mod state; pub mod state;
pub async fn start(config_directory: &Path, config: model::config::Config) -> Result<()> { pub async fn start(config_directory: &Path, config: model::config::Config) -> Result<()> {
let config = Arc::new(config); 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…"); info!("Connecting to the device…");
let device = available_device.connect().wrap_err("Connecting to the device failed.")?; let device = available_device.connect().wrap_err("Connecting to the device failed.")?;
info!("Connected"); info!("Connected");
let key_grid = &device.characteristics().key_grid; let key_grid = &device.characteristics().key_grid;
let used_icon_descriptors = get_used_icon_descriptors(&config); let used_icon_descriptors = get_used_icon_descriptors(&config);
let start_time = Instant::now(); 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.set_brightness(0.5);
device.vibrate(VibrationPattern::RiseFall); device.vibrate(VibrationPattern::RiseFall);
let events_receiver = device.events();
let (commands_sender, commands_receiver) = flume::bounded::<StateChangeCommand>(20); let (commands_sender, commands_receiver) = flume::bounded::<StateChangeCommand>(20);
let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20);
commands_sender commands_sender
.send(StateChangeCommand::SetActivePages { .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_config = Arc::clone(&config);
let cloned_commands_sender = commands_sender.clone(); let cloned_commands_sender = commands_sender.clone();
let cloned_key_events_sender = key_events_sender.clone();
let io_worker_thread = thread::Builder::new() let io_worker_thread = thread::Builder::new()
.name("deckster IO worker".to_owned()) .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")?; .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."); info!("Ready.");
io_worker_thread.join().unwrap(); io_worker_thread.join().unwrap();
@ -93,7 +117,7 @@ fn create_state(config: &model::config::Config) -> State {
position: *position, position: *position,
}, },
base_style: k.base_style.clone(), base_style: k.base_style.clone(),
style: KeyStyle::default(), style: None,
}) })
.map(|k| (k.path.position, k)) .map(|k| (k.path.position, k))
.collect(), .collect(),
@ -147,9 +171,9 @@ fn do_io_work(
config: Arc<model::config::Config>, config: Arc<model::config::Config>,
icons: LoadedIconsMap, icons: LoadedIconsMap,
device: LoupedeckDevice, device: LoupedeckDevice,
events_receiver: Receiver<LoupedeckEvent>, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
commands_sender: Sender<StateChangeCommand>, commands_sender: flume::Sender<StateChangeCommand>,
commands_receiver: Receiver<StateChangeCommand>, commands_receiver: flume::Receiver<StateChangeCommand>,
) { ) {
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;
@ -161,6 +185,8 @@ fn do_io_work(
let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref())); let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref()));
let device_events_receiver = device.events();
let mut context = IoWorkerContext { let mut context = IoWorkerContext {
config, config,
device, device,
@ -175,13 +201,13 @@ fn do_io_work(
loop { loop {
let a = flume::Selector::new() 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())) .recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
.wait(); .wait();
match a { match a {
IoWork::Event(event) => { IoWork::Event(event) => {
if !handle_event(&context, &commands_sender, event) { if !handle_event(&context, &commands_sender, &key_events_sender, event) {
break; break;
} }
} }
@ -190,9 +216,19 @@ fn do_io_work(
} }
} }
fn handle_event(context: &IoWorkerContext, commands_sender: &Sender<StateChangeCommand>, event: LoupedeckEvent) -> bool { fn handle_event(
context: &IoWorkerContext,
commands_sender: &flume::Sender<StateChangeCommand>,
key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>,
event: LoupedeckEvent,
) -> bool {
trace!("Handling event: {:?}", &event); 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 { match event {
LoupedeckEvent::Disconnected => return false, LoupedeckEvent::Disconnected => return false,
LoupedeckEvent::ButtonDown { button } => { LoupedeckEvent::ButtonDown { button } => {
@ -224,6 +260,8 @@ fn handle_event(context: &IoWorkerContext, commands_sender: &Sender<StateChangeC
page_id: context.state.active_key_page_id.clone(), page_id: context.state.active_key_page_id.clone(),
position, position,
}; };
send_key_event(path, KeyEvent::Press);
} }
} }
} }

View file

@ -58,7 +58,7 @@ pub struct KnobPage {
pub struct Key { pub struct Key {
pub path: KeyPath, pub path: KeyPath,
pub base_style: KeyStyle, pub base_style: KeyStyle,
pub style: KeyStyle, pub style: Option<KeyStyle>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -73,5 +73,5 @@ pub struct Knob {
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
pub enum StateChangeCommand { pub enum StateChangeCommand {
SetActivePages { key_page_id: String, knob_page_id: String }, SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyStyle { path: KeyPath, value: KeyStyle }, SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
} }