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"
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"

View file

@ -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]"

View file

@ -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))

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::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<ScrollingConfig>,
pub keys: HashMap<KeyPosition, KeyConfig>,
pub keys: HashMap<KeyPosition, Arc<KeyConfig>>,
}
#[derive(Debug, Deserialize)]
@ -74,14 +75,14 @@ pub struct KeyConfig {
#[allow(non_snake_case)]
#[derive(Debug, Default, Deserialize)]
pub struct KeyModes {
pub vibrate: Option<key_modes::vibrate::Config>,
pub media__play_pause: Option<key_modes::media::PlayPauseConfig>,
pub media__previous: Option<key_modes::media::PreviousAndNextConfig>,
pub media__next: Option<key_modes::media::PreviousAndNextConfig>,
pub spotify__shuffle: Option<key_modes::spotify::ShuffleConfig>,
pub spotify__repeat: Option<key_modes::spotify::RepeatConfig>,
pub home_assistant__switch: Option<key_modes::home_assistant::SwitchConfig>,
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>,
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 home_assistant__switch: Option<Arc<modes::key::home_assistant::SwitchConfig>>,
pub home_assistant__button: Option<Arc<modes::key::home_assistant::ButtonConfig>>,
}
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;

View file

@ -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<knob_modes::audio_volume::Config>,
pub audio_volume: Option<modes::knob::audio_volume::Config>,
}
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -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;

View file

@ -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 {

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();
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);
}

View file

@ -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::<StateChangeCommand>(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<model::config::Config>,
icons: LoadedIconsMap,
device: LoupedeckDevice,
events_receiver: Receiver<LoupedeckEvent>,
commands_sender: Sender<StateChangeCommand>,
commands_receiver: Receiver<StateChangeCommand>,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
commands_sender: flume::Sender<StateChangeCommand>,
commands_receiver: flume::Receiver<StateChangeCommand>,
) {
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<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);
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<StateChangeC
page_id: context.state.active_key_page_id.clone(),
position,
};
send_key_event(path, KeyEvent::Press);
}
}
}

View file

@ -58,7 +58,7 @@ pub struct KnobPage {
pub struct Key {
pub path: KeyPath,
pub base_style: KeyStyle,
pub style: KeyStyle,
pub style: Option<KeyStyle>,
}
#[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<KeyStyle> },
}