commit
This commit is contained in:
parent
2719b7afb8
commit
efb5385971
21 changed files with 200 additions and 84 deletions
|
@ -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"
|
||||
|
|
|
@ -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]"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
pub mod home_assistant;
|
||||
pub mod media;
|
||||
pub mod spotify;
|
||||
pub mod timer;
|
||||
pub mod vibrate;
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
68
deckster/src/modes/key/media.rs
Normal file
68
deckster/src/modes/key/media.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
46
deckster/src/modes/key/mod.rs
Normal file
46
deckster/src/modes/key/mod.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
2
deckster/src/modes/mod.rs
Normal file
2
deckster/src/modes/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod key;
|
||||
pub mod knob;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> },
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue