diff --git a/Cargo.lock b/Cargo.lock index 1887d24..1091078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -197,9 +197,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.13" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -207,9 +207,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -365,6 +365,7 @@ dependencies = [ "clap", "color-eyre", "cosmic-text", + "deckster_shared", "derive_more", "encode_unicode", "enum-map", @@ -390,6 +391,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "deckster_mode" +version = "0.1.0" +dependencies = [ + "deckster_shared", + "either", + "im", + "thiserror", + "toml", +] + +[[package]] +name = "deckster_shared" +version = "0.1.0" +dependencies = [ + "derive_more", + "enum-map", + "enum-ordinalize", + "im", + "rgb", + "serde", + "serde_with", + "thiserror", + "toml", +] + [[package]] name = "deranged" version = "0.3.11" @@ -413,6 +440,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -481,16 +514,26 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.10.1" +name = "env_filter" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eeb342678d785662fd2514be38c459bb925f02b68dd2a3e0f21d7ef82d979dd" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] @@ -499,16 +542,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "eyre" version = "0.6.11" @@ -724,6 +757,7 @@ dependencies = [ "bitmaps", "rand_core", "rand_xoshiro", + "serde", "sized-chunks", "typenum", "version_check", @@ -773,17 +807,6 @@ dependencies = [ "mach2", ] -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - [[package]] name = "itoa" version = "1.0.10" @@ -879,12 +902,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" - [[package]] name = "lock_api" version = "0.4.11" @@ -1276,19 +1293,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" -dependencies = [ - "bitflags 2.4.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "rustybuzz" version = "0.11.0" @@ -1606,29 +1610,20 @@ dependencies = [ "libc", ] -[[package]] -name = "termcolor" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index eb06c9f..15cd9d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [workspace] members = [ "deckster", + "deckster_mode", + "deckster_shared", "loupedeck_serial", "pa-volume-interface" ] diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index 09fce1b..396aba0 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -12,12 +12,13 @@ derive_more = "0.99.17" encode_unicode = "1.0.0" enum-map = "3.0.0-beta.2" enum-ordinalize = "4.3.0" -env_logger = "0.10.1" +env_logger = "0.11.0" flume = "0.11.0" humantime-serde = "1.1.1" log = "0.4.20" loupedeck_serial = { path = "../loupedeck_serial" } pa-volume-interface = { path = "../pa-volume-interface" } +deckster_shared = { path = "../deckster_shared" } regex = "1.10.2" resvg = "0.37.0" rgb = "0.8.37" diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index 03c9c78..5b09c1f 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -1,36 +1,48 @@ [keys.1x2] icon = "@ph/skip-back" -mode.playerctl__button.command = "previous" -mode.playerctl__button.style.inactive.icon = "@ph/skip-back[alpha=0.4]" + +handler = "playerctl previous" +config.mode = "previous" +config.style.inactive.icon = "@ph/skip-back[alpha=0.4]" [keys.2x2] 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" + +handler = "playerctl play-pause" +config.style.paused.icon = "@ph/play" +config.style.playing.icon = "@ph/pause" [keys.3x2] icon = "@ph/skip-forward" -mode.playerctl__button.command = "next" -mode.playerctl__button.style.inactive.icon = "@ph/skip-forward[alpha=0.4]" + +handler = "playerctl next" +config.style.inactive.icon = "@ph/skip-forward[alpha=0.4]" [keys.1x3] icon = "@fad/shuffle[alpha=0.4]" -mode.playerctl__shuffle.style.on.icon = "@fad/shuffle[color=#58fc11]" + +handler = "playerctl shuffle" +config.style.on.icon = "@fad/shuffle[color=#58fc11]" [keys.2x3] 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]" + +handler = "playerctl loop" +config.style.single.icon = "@fad/repeat-one[color=#58fc11]" +config.style.all.icon = "@fad/repeat[color=#58fc11]" [keys.3x3] icon = "@ph/timer[color=#ff0000]" -mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"] -mode.timer.vibrate_when_finished = true -mode.timer.needy = true + +handler = "timer" +config.durations = ["60s", "5m", "10m", "15m", "30m"] +config.vibrate_when_finished = true +config.needy = true [keys.4x3] icon = "@ph/computer-tower" label = "Gaming PC" -mode.home_assistant__switch.name = "switch.mwin" -mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]" \ No newline at end of file + +handler = "home-assistant switch" +config.name = "switch.mwin" +config.style.on.icon = "@ph/computer-tower[color=#58fc11]" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/emojis.toml b/deckster/examples/full/key-pages/emojis.toml deleted file mode 100644 index d6d9199..0000000 --- a/deckster/examples/full/key-pages/emojis.toml +++ /dev/null @@ -1,28 +0,0 @@ -[scrolling] -knob = "right-bottom" -axis = "vertical" -delta = 1 - -[keys.4x1] -label = "πŸ˜€" -mode.command.command = "wtype πŸ˜€" - -[keys.4x2] -label = "πŸ˜„" -mode.command.command = "wtype πŸ˜„" - -[keys.4x3] -label = "πŸ˜…" -mode.command.command = "wtype πŸ˜…" - -[keys.4x4] -label = "πŸ˜‚" -mode.command.command = "wtype πŸ˜‚" - -[keys.4x5] -label = "😁" -mode.command.command = "wtype 😁" - -[keys.4x6] -label = "πŸ˜‡" -mode.command.command = "wtype πŸ˜‡" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/numpad.toml b/deckster/examples/full/key-pages/numpad.toml deleted file mode 100644 index cf96fec..0000000 --- a/deckster/examples/full/key-pages/numpad.toml +++ /dev/null @@ -1,35 +0,0 @@ -[keys.4x1] -label = "9" -mode.command.command = "wtype 9" - -[keys.3x1] -label = "8" -mode.command.command = "wtype 8" - -[keys.2x1] -label = "7" -mode.command.command = "wtype 7" - -[keys.4x2] -label = "6" -mode.command.command = "wtype 6" - -[keys.3x2] -label = "5" -mode.command.command = "wtype 5" - -[keys.2x2] -label = "4" -mode.command.command = "wtype 4" - -[keys.4x3] -label = "3" -mode.command.command = "wtype 3" - -[keys.3x3] -label = "2" -mode.command.command = "wtype 2" - -[keys.2x3] -label = "1" -mode.command.command = "wtype 1" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/special_chars.toml b/deckster/examples/full/key-pages/special_chars.toml deleted file mode 100644 index c2bf749..0000000 --- a/deckster/examples/full/key-pages/special_chars.toml +++ /dev/null @@ -1,29 +0,0 @@ -[scrolling] -button.previous = "3x3" -button.next = "4x3" -axis = "horizontal" -delta = 4 - -[keys.1x1] -label = "’" -mode.command.command = "wtype ’" - -[keys.2x1] -label = "…" -mode.command.command = "wtype …" - -[keys.3x1] -label = "β€”" -mode.command.command = "wtype β€”" - -[keys.4x1] -label = "–" -mode.command.command = "wtype –" - -[keys.5x1] -label = "Β·" -mode.command.command = "wtype Β·" - -[keys.6x1] -label = "β†’" -mode.command.command = "wtype β†’" \ No newline at end of file diff --git a/deckster/examples/full/knob-pages/default.toml b/deckster/examples/full/knob-pages/default.toml index bbcbbf1..57a4ac5 100644 --- a/deckster/examples/full/knob-pages/default.toml +++ b/deckster/examples/full/knob-pages/default.toml @@ -2,49 +2,53 @@ icon = "@ph/microphone-light[scale=0.9]" indicators.bar.color = "#ffffff50" -mode.audio_volume.delta = 0.05 -mode.audio_volume.target.type = "input" -mode.audio_volume.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }] -mode.audio_volume.muted_turn_action = "normal" +handler = "audio_volume" +config.delta = 0.05 +config.target.type = "input" +config.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }] +config.muted_turn_action = "normal" -mode.audio_volume.style.active.label = "{percentage}%" -mode.audio_volume.style.muted.label = "Muted" -mode.audio_volume.style.muted.icon = "@ph/microphone-slash-light[scale=0.9|color=#fc4646]" -mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" -mode.audio_volume.style.inactive.label = "N/A" -mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[scale=0.9|alpha=0.8|color=#fc4646]" +config.style.active.label = "{percentage}%" +config.style.muted.label = "Muted" +config.style.muted.icon = "@ph/microphone-slash-light[scale=0.9|color=#fc4646]" +config.style.muted.indicators.bar.color = "#fc464690" +config.style.inactive.label = "N/A" +config.style.inactive.icon = "@ph/microphone-slash-light[scale=0.9|alpha=0.8|color=#fc4646]" [knobs.left-top] icon = "@apps/discord[scale=0.25]" indicators.bar.color = "#ffffff50" -mode.audio_volume.delta = 0.05 -mode.audio_volume.target.type = "application" -mode.audio_volume.target.predicates = [{ property = "binary-name", value = "Discord" }, { property = "description", value = "playStream" }] +handler = "audio_volume" +config.delta = 0.05 +config.target.type = "application" +config.target.predicates = [{ property = "binary-name", value = "Discord" }, { property = "description", value = "playStream" }] -mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" -mode.audio_volume.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]" +config.style.muted.indicators.bar.color = "#fc464690" +config.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]" [knobs.left-middle] icon = "@apps/youtube[scale=1.3]" indicators.bar.color = "#ffffff50" -mode.audio_volume.delta = 0.05 -mode.audio_volume.muted_turn_action = "unmute" -mode.audio_volume.target.type = "application" -mode.audio_volume.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }] +handler = "audio_volume" +config.delta = 0.05 +config.muted_turn_action = "unmute" +config.target.type = "application" +config.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }] -mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" -mode.audio_volume.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale]" +config.style.muted.indicators.bar.color = "#fc464690" +config.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale]" [knobs.left-bottom] icon = "@apps/spotify[scale=1.2]" indicators.bar.color = "#ffffff50" -mode.audio_volume.delta = 0.05 -mode.audio_volume.muted_turn_action = "unmute-at-zero" -mode.audio_volume.target.type = "application" -mode.audio_volume.target.predicates = [{ property = "application-name", value = "spotify" }] +handler = "audio_volume" +config.delta = 0.05 +config.muted_turn_action = "unmute-at-zero" +config.target.type = "application" +config.target.predicates = [{ property = "application-name", value = "spotify" }] -mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" -mode.audio_volume.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]" \ No newline at end of file +config.style.muted.indicators.bar.color = "#fc464690" +config.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]" \ No newline at end of file diff --git a/deckster/src/icons/destructive_filter.rs b/deckster/src/icons/destructive_filter.rs index 125558e..153b399 100644 --- a/deckster/src/icons/destructive_filter.rs +++ b/deckster/src/icons/destructive_filter.rs @@ -1,7 +1,7 @@ use color_eyre::Result; use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8}; -use crate::model::image_filter::ImageFilterDestructive; +use deckster_shared::image_filter::ImageFilterDestructive; pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result { let mut result = original.clone(); diff --git a/deckster/src/icons/mod.rs b/deckster/src/icons/mod.rs index fdd000c..dc79143 100644 --- a/deckster/src/icons/mod.rs +++ b/deckster/src/icons/mod.rs @@ -8,10 +8,11 @@ use resvg::usvg::tiny_skia_path::IntSize; use resvg::usvg::{TextRendering, TreeParsing, TreeTextToPath}; use tiny_skia::{BlendMode, FilterQuality, Pixmap, PixmapPaint, Transform}; +use deckster_shared::icon_descriptor::{IconDescriptor, IconDescriptorSource}; +use deckster_shared::image_filter::{ImageFilter, ImageFilterDestructive}; +use deckster_shared::state::{KeyStyleByStateMap, KnobStyleByStateMap}; + use crate::model::config::{Config, IconFormat, IconPack}; -use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; -use crate::model::image_filter::{ImageFilter, ImageFilterDestructive}; -use crate::model::{key_page, knob_page}; mod destructive_filter; @@ -26,7 +27,7 @@ pub struct LoadedIcon { pub fn get_used_icon_descriptors(config: &Config) -> HashSet { let mut result: HashSet = HashSet::new(); - fn insert_all_from_key_style_by_state_map(result: &mut HashSet, map: &key_page::StyleByStateMap) { + fn insert_all_from_key_style_by_state_map(result: &mut HashSet, map: &KeyStyleByStateMap) { map.values().for_each(|v| { if let Some(icon) = &v.icon { result.insert(icon.clone()); @@ -34,7 +35,7 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet { }); } - fn insert_all_from_knob_style_by_state_map(result: &mut HashSet, map: &knob_page::StyleByStateMap) { + fn insert_all_from_knob_style_by_state_map(result: &mut HashSet, map: &KnobStyleByStateMap) { map.values().for_each(|v| { if let Some(icon) = &v.icon { result.insert(icon.clone()); diff --git a/deckster/src/main.rs b/deckster/src/main.rs index f83e0b9..6e993fb 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use clap::{Parser, Subcommand}; use color_eyre::eyre::WrapErr; use color_eyre::Result; +use log::LevelFilter; use walkdir::WalkDir; use crate::model::config::WithFallbackId; @@ -33,7 +34,8 @@ enum Command { #[tokio::main] pub async fn main() -> Result<()> { - env_logger::init(); + env_logger::builder().filter_module("deckster", LevelFilter::Info).parse_env("RUST_LOG").init(); + let cli = Cli::parse(); match cli.command { diff --git a/deckster/src/model/config.rs b/deckster/src/model/config.rs index 6d60bb5..843a2d4 100644 --- a/deckster/src/model/config.rs +++ b/deckster/src/model/config.rs @@ -6,10 +6,11 @@ use enum_map::EnumMap; use rgb::RGB8; use serde::{Deserialize, Serialize}; +use deckster_shared::image_filter::ImageFilter; +use deckster_shared::rgb::RGB8Wrapper; + use crate::model; -use crate::model::image_filter::ImageFilter; use crate::model::position::ButtonPosition; -use crate::model::rgb::RGB8Wrapper; #[derive(Debug, Deserialize)] pub struct File { diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index 6851f56..957056e 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; use std::sync::Arc; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; + +use deckster_shared::path::{KeyPosition, KnobPosition}; +use deckster_shared::style::KeyStyle; use crate::model::geometry::UIntVec2; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::position::{KeyPosition, KnobPosition}; -use crate::model::rgb::RGB8WithOptionalA; use crate::modes; #[derive(Debug, Deserialize)] @@ -46,23 +46,6 @@ pub enum ScrollingConfigAxis { Horizontal, } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -pub struct KeyStyle { - pub label: Option, - pub icon: Option, - pub border: Option, -} - -impl KeyStyle { - pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle { - KeyStyle { - label: self.label.as_ref().or(base.label.as_ref()).cloned(), - icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), - border: self.border.or(base.border), - } - } -} - #[derive(Debug, Deserialize)] pub struct Key { #[serde(default, flatten)] @@ -81,7 +64,4 @@ pub struct KeyModes { pub playerctl__button: Option>, pub playerctl__shuffle: Option>, pub playerctl__loop: Option>, - pub vibrate: Option>, } - -pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index 0992bc6..e672a28 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use std::sync::Arc; use enum_map::EnumMap; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; + +use deckster_shared::path::KnobPosition; +use deckster_shared::style::KnobStyle; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::position::KnobPosition; -use crate::model::rgb::RGB8WithOptionalA; use crate::modes; #[derive(Debug, Deserialize)] @@ -29,89 +29,7 @@ pub struct Knob { pub mode: KnobModes, } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -pub struct KnobIndicators { - pub bar: Option, - pub circle: Option, -} - -impl KnobIndicators { - pub fn merge_over(&self, base: &Self) -> Self { - Self { - bar: self - .bar - .as_ref() - .zip(base.bar.as_ref()) - .map(|(a, b)| a.merge_over(b)) - .or_else(|| self.bar.clone()) - .or_else(|| base.bar.clone()), - circle: self - .circle - .as_ref() - .zip(base.circle.as_ref()) - .map(|(a, b)| a.merge_over(b)) - .or_else(|| self.circle.clone()) - .or_else(|| base.circle.clone()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct KnobIndicatorBarConfig { - pub color: Option, -} - -impl KnobIndicatorBarConfig { - pub fn merge_over(&self, base: &Self) -> Self { - Self { - color: self.color.as_ref().or(base.color.as_ref()).cloned(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct KnobIndicatorCircleConfig { - pub color: Option, - pub width: Option, - pub radius: Option, -} - -impl KnobIndicatorCircleConfig { - pub fn merge_over(&self, base: &Self) -> Self { - Self { - color: self.color.as_ref().or(base.color.as_ref()).cloned(), - width: self.width.as_ref().or(base.width.as_ref()).cloned(), - radius: self.radius.as_ref().or(base.radius.as_ref()).cloned(), - } - } -} - -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -pub struct KnobStyle { - pub label: Option, - pub icon: Option, - pub indicators: Option, -} - -impl KnobStyle { - pub fn merge_over(&self, base: &Self) -> Self { - Self { - label: self.label.as_ref().or(base.label.as_ref()).cloned(), - icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), - indicators: self - .indicators - .as_ref() - .zip(base.indicators.as_ref()) - .map(|(a, b)| a.merge_over(b)) - .or_else(|| self.indicators.clone()) - .or_else(|| base.indicators.clone()), - } - } -} - #[derive(Debug, Default, Deserialize)] pub struct KnobModes { pub audio_volume: Option>, } - -pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index dc0a1c1..4e78e69 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -1,8 +1,5 @@ pub mod config; pub mod geometry; -pub mod icon_descriptor; -pub mod image_filter; pub mod key_page; 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 58fb08b..bbed257 100644 --- a/deckster/src/model/position.rs +++ b/deckster/src/model/position.rs @@ -1,96 +1,9 @@ use std::fmt::{Display, Formatter}; -use std::str::FromStr; use enum_map::Enum; -use enum_ordinalize::Ordinalize; -use serde::{Deserialize, Serialize}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; -use thiserror::Error; +use serde::Deserialize; -use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckKnob}; - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] -/// One-based coordinates of a specific virtual (not physical) key. -pub struct KeyPosition { - pub x: u16, - pub y: u16, -} - -#[derive(Debug, Error)] -#[error("The input value does not match the required format of and separated by an 'x'")] -pub struct KeyPositionFromStrError {} - -impl FromStr for KeyPosition { - type Err = KeyPositionFromStrError; - - fn from_str(s: &str) -> Result { - let values = s.split_once('x'); - - if let Some((x, y)) = values { - if let Ok(x) = u16::from_str(x) { - if let Ok(y) = u16::from_str(y) { - return Ok(KeyPosition { x, y }); - } - } - } - - Err(KeyPositionFromStrError {}) - } -} - -impl Display for KeyPosition { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}x{}", self.x, self.y)) - } -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] -pub struct KeyPath { - pub page_id: String, - 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, Ordinalize)] -#[serde(rename_all = "kebab-case")] -pub enum KnobPosition { - LeftTop, - LeftMiddle, - LeftBottom, - RightTop, - RightMiddle, - RightBottom, -} - -impl KnobPosition { - pub fn is_left(&self) -> bool { - matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop) - } -} - -impl From for KnobPosition { - fn from(value: LoupedeckKnob) -> Self { - match value { - LoupedeckKnob::LeftTop => KnobPosition::LeftTop, - LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, - LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, - LoupedeckKnob::RightTop => KnobPosition::RightTop, - LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, - LoupedeckKnob::RightBottom => KnobPosition::RightBottom, - } - } -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] -pub struct KnobPath { - pub page_id: String, - pub position: KnobPosition, -} +use loupedeck_serial::characteristics::LoupedeckButton; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] pub enum ButtonPosition { diff --git a/deckster/src/modes/key/command.rs b/deckster/src/modes/key/command.rs index dc8e92b..2542e5c 100644 --- a/deckster/src/modes/key/command.rs +++ b/deckster/src/modes/key/command.rs @@ -5,8 +5,6 @@ use log::{error, trace, warn}; use serde::Deserialize; use tokio::sync::broadcast; -use crate::modes::key::KeyEvent; - #[derive(Debug, Deserialize)] pub struct Config { pub command: String, diff --git a/deckster/src/modes/key/home_assistant.rs b/deckster/src/modes/key/home_assistant.rs index 786ef68..9817006 100644 --- a/deckster/src/modes/key/home_assistant.rs +++ b/deckster/src/modes/key/home_assistant.rs @@ -1,19 +1,19 @@ use serde::Deserialize; -use crate::model::key_page::StyleByStateMap; +use deckster_shared::state::KeyStyleByStateMap; #[derive(Debug, Deserialize)] pub struct SwitchConfig { pub name: String, #[serde(default)] - pub style: StyleByStateMap, + pub style: KeyStyleByStateMap, } #[derive(Debug, Deserialize)] pub struct ButtonConfig { pub name: String, #[serde(default)] - pub style: StyleByStateMap, + pub style: KeyStyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/modes/key/mod.rs b/deckster/src/modes/key/mod.rs index a709464..3e94aa2 100644 --- a/deckster/src/modes/key/mod.rs +++ b/deckster/src/modes/key/mod.rs @@ -3,34 +3,20 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; +use deckster_shared::handler_communication::HandlerCommand; +use deckster_shared::path::KeyPath; + use crate::model; -use crate::model::position::KeyPath; -use crate::runner::command::IoWorkerCommand; pub mod command; pub mod home_assistant; 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)>, events: broadcast::Sender<(KeyPath, KeyEvent)>, - commands: flume::Sender, + commands: flume::Sender, ) { for (path, config) in keys { let mut events = events.subscribe(); @@ -52,10 +38,6 @@ pub fn start_handlers( 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(Arc::clone(c), own_events.subscribe(), commands.clone())); - } - tokio::spawn(async move { while let Ok((p, e)) = events.recv().await { #[allow(clippy::collapsible_if)] diff --git a/deckster/src/modes/key/playerctl.rs b/deckster/src/modes/key/playerctl.rs index 0fe2a3f..287843a 100644 --- a/deckster/src/modes/key/playerctl.rs +++ b/deckster/src/modes/key/playerctl.rs @@ -14,15 +14,16 @@ 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 deckster_shared::handler_communication::HandlerCommand; +use deckster_shared::path::KeyPath; +use deckster_shared::state::KeyStyleByStateMap; + use crate::modes::key::KeyEvent; -use crate::runner::command::IoWorkerCommand; #[derive(Debug, Deserialize)] pub struct ButtonConfig { #[serde(default)] - pub style: StyleByStateMap, + pub style: KeyStyleByStateMap, pub command: ButtonCommand, } @@ -47,7 +48,7 @@ pub enum ButtonState { #[derive(Debug, Deserialize)] pub struct ShuffleConfig { #[serde(default)] - pub style: StyleByStateMap, + pub style: KeyStyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)] @@ -61,7 +62,7 @@ pub enum ShuffleState { #[derive(Debug, Deserialize)] pub struct LoopConfig { #[serde(default)] - pub style: StyleByStateMap, + pub style: KeyStyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)] @@ -164,7 +165,7 @@ static STATE_WATCHER_LOOP: Lazy> = Lazy::new(|| }) }); -pub async fn handle_button(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { +pub async fn handle_button(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { let mut is_active = false; let mut state = STATE_WATCHER_PLAYING.subscribe_to_state(); @@ -185,7 +186,7 @@ pub async fn handle_button(path: KeyPath, config: Arc, mut events: Ok(state) => { is_active = state != ButtonState::Inactive; - commands.send(IoWorkerCommand::SetKeyStyle { + commands.send(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&state).cloned() }).unwrap(); @@ -206,7 +207,7 @@ pub async fn handle_button(path: KeyPath, config: Arc, mut events: } } -pub async fn handle_shuffle(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { +pub async fn handle_shuffle(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state(); loop { @@ -216,7 +217,7 @@ pub async fn handle_shuffle(path: KeyPath, config: Arc, mut event Err(RecvError::Closed) => { result.unwrap(); }, Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ }, Ok(state) => { - commands.send(IoWorkerCommand::SetKeyStyle { + commands.send(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&state).cloned() }).unwrap(); @@ -243,7 +244,7 @@ pub async fn handle_shuffle(path: KeyPath, config: Arc, mut event } } -pub async fn handle_loop(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { +pub async fn handle_loop(path: KeyPath, config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { let mut state = STATE_WATCHER_LOOP.subscribe_to_state(); loop { @@ -253,7 +254,7 @@ pub async fn handle_loop(path: KeyPath, config: Arc, mut events: bro Err(RecvError::Closed) => { result.unwrap(); }, Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ }, Ok(state) => { - commands.send(IoWorkerCommand::SetKeyStyle { + commands.send(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&state).cloned() }).unwrap(); diff --git a/deckster/src/modes/key/vibrate.rs b/deckster/src/modes/key/vibrate.rs deleted file mode 100644 index ebb78ba..0000000 --- a/deckster/src/modes/key/vibrate.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::sync::Arc; - -use serde::Deserialize; -use tokio::sync::broadcast; - -use loupedeck_serial::commands::VibrationPattern; - -use crate::modes::key::{KeyEvent, KeyTouchEventKind}; -use crate::runner::command::IoWorkerCommand; - -#[derive(Debug, Deserialize)] -pub struct Config { - pub pattern: VibrationPattern, -} - -pub async fn handle(config: Arc, mut events: broadcast::Receiver, commands: flume::Sender) { - 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(); - } - } - } -} diff --git a/deckster/src/modes/knob/audio_volume.rs b/deckster/src/modes/knob/audio_volume.rs index b58f2d9..e076eb2 100644 --- a/deckster/src/modes/knob/audio_volume.rs +++ b/deckster/src/modes/knob/audio_volume.rs @@ -9,13 +9,12 @@ use serde::Deserialize; use tokio::select; use tokio::sync::broadcast; +use deckster_shared::handler_communication::HandlerCommand; +use deckster_shared::handler_communication::{KnobEvent, RotationDirection}; +use deckster_shared::path::KnobPath; +use deckster_shared::state::KnobStyleByStateMap; use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface}; -use crate::model::knob_page::StyleByStateMap; -use crate::model::position::KnobPath; -use crate::modes::knob::{KnobEvent, RotationDirection}; -use crate::runner::command::IoWorkerCommand; - #[derive(Debug, Deserialize)] pub struct Config { pub target: Target, @@ -27,7 +26,7 @@ pub struct Config { #[serde(default)] pub muted_turn_action: MutedTurnAction, #[serde(default)] - pub style: StyleByStateMap, + pub style: KnobStyleByStateMap, } #[derive(Debug, Eq, PartialEq, Deserialize, Display)] @@ -149,7 +148,7 @@ fn state_matches(target: &Target, state: &PaEntityState) -> bool { }); } -pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender) { +pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender) { let mut entity_state: Option> = None; let pa_volume_interface = &PA_VOLUME_INTERFACE; @@ -162,7 +161,7 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast:: move |entity_state: &Option>| { commands - .send(IoWorkerCommand::SetKnobValue { + .send(HandlerCommand::SetKnobValue { path: path.clone(), value: entity_state.as_ref().map(|s| { if s.is_muted() && config.muted_turn_action == MutedTurnAction::UnmuteAtZero { @@ -202,7 +201,7 @@ pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast:: } commands - .send(IoWorkerCommand::SetKnobStyle { + .send(HandlerCommand::SetKnobStyle { path: path.clone(), value: style, }) diff --git a/deckster/src/modes/knob/mod.rs b/deckster/src/modes/knob/mod.rs index c332cae..4c5c573 100644 --- a/deckster/src/modes/knob/mod.rs +++ b/deckster/src/modes/knob/mod.rs @@ -1,33 +1,19 @@ use std::sync::Arc; -use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; +use deckster_shared::handler_communication::HandlerCommand; +use deckster_shared::handler_communication::KnobEvent; +use deckster_shared::path::KnobPath; + use crate::model; -use crate::model::position::KnobPath; -use crate::runner::command::IoWorkerCommand; pub mod audio_volume; -#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] -pub enum RotationDirection { - Clockwise, - Counterclockwise, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub enum KnobEvent { - Press, - ButtonDown, - ButtonUp, - Rotate { direction: RotationDirection }, - VisibilityChange { is_visible: bool }, -} - pub fn start_handlers( knobs: impl Iterator)>, events: broadcast::Sender<(KnobPath, KnobEvent)>, - commands: flume::Sender, + commands: flume::Sender, ) { for (path, config) in knobs { if let Some(c) = &config.mode.audio_volume { diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs deleted file mode 100644 index 7e6b045..0000000 --- a/deckster/src/runner/command.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use loupedeck_serial::commands::VibrationPattern; - -use crate::model::key_page::KeyStyle; -use crate::model::knob_page::KnobStyle; -use crate::model::position::{KeyPath, KnobPath}; - -#[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 }, - SetKnobStyle { path: KnobPath, value: Option }, - SetKnobValue { path: KnobPath, value: Option }, -} diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index 4593627..3f9e842 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -6,12 +6,12 @@ use resvg::usvg::tiny_skia_path::PathBuilder; use rgb::RGBA; use tiny_skia::{Color, IntSize, LineCap, LineJoin, Paint, Pixmap, PremultipliedColorU8, Rect, Shader, Stroke, Transform}; +use deckster_shared::image_filter::ImageFilter; +use deckster_shared::state::{Key, Knob}; use loupedeck_serial::util::Endianness; use crate::icons::{render_icon_in, LoadedIconsMap}; -use crate::model::image_filter::ImageFilter; use crate::runner::graphics::labels::LabelRenderer; -use crate::runner::state::{Key, Knob}; #[derive(Debug)] pub struct GraphicsContext { diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 41f7937..9d6c417 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -12,22 +12,23 @@ use rgb::RGB8; use tiny_skia::IntSize; use tokio::sync::broadcast; -use command::IoWorkerCommand; +use deckster_shared::handler_communication::HandlerCommand; +use deckster_shared::handler_communication::KnobEvent; +use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; +use deckster_shared::state::{Key, Knob}; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap}; -use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath, KnobPosition}; +use crate::model::position::ButtonPosition; use crate::modes::key::{KeyEvent, KeyTouchEventKind}; -use crate::modes::knob::KnobEvent; use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::{render_key, render_knob, GraphicsContext}; -use crate::runner::state::{Key, Knob, State}; +use crate::runner::state::State; use crate::{model, modes}; -pub mod command; mod graphics; pub mod state; @@ -40,7 +41,7 @@ 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"); + info!("Connected."); let key_grid = &device.characteristics().key_grid; let used_icon_descriptors = get_used_icon_descriptors(&config); @@ -50,15 +51,15 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?; info!("Finished loading {} icon(s) in {}ms", icons.len(), start_time.elapsed().as_millis()); - device.set_brightness(0.5); + device.set_brightness(0.1); device.vibrate(VibrationPattern::RiseFall); - let (commands_sender, commands_receiver) = flume::bounded::(5); + let (commands_sender, commands_receiver) = flume::bounded::(5); let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(5); let knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)> = broadcast::Sender::new(5); commands_sender - .send(IoWorkerCommand::SetActivePages { + .send(HandlerCommand::SetActivePages { knob_page_id: config.initial.knob_page.clone(), key_page_id: config.initial.key_page.clone(), }) @@ -120,13 +121,13 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re enum IoWork { Event(LoupedeckEvent), - Command(IoWorkerCommand), + Command(HandlerCommand), } struct IoWorkerContext { config: Arc, device: LoupedeckDevice, - commands_sender: flume::Sender, + commands_sender: flume::Sender, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, graphics: GraphicsContext, @@ -137,7 +138,7 @@ impl IoWorkerContext { config: Arc, icons: LoadedIconsMap, device: LoupedeckDevice, - commands_sender: flume::Sender, + commands_sender: flume::Sender, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, ) -> Self { @@ -166,7 +167,7 @@ impl IoWorkerContext { } } -fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver) { +fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver) { let mut state = State::create(&context.config); let device_events_receiver = context.device.events(); @@ -208,7 +209,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv context .commands_sender - .send(IoWorkerCommand::SetActivePages { + .send(HandlerCommand::SetActivePages { key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(), knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(), }) @@ -266,7 +267,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv } } LoupedeckEvent::KnobRotate { knob, direction } => { - let position: KnobPosition = knob.into(); + let position: KnobPosition = get_position_of_loupedeck_knob(knob); send_knob_event( KnobPath { @@ -275,14 +276,14 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv }, KnobEvent::Rotate { direction: match direction { - RotationDirection::Clockwise => modes::knob::RotationDirection::Clockwise, - RotationDirection::Counterclockwise => modes::knob::RotationDirection::Counterclockwise, + RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise, + RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise, }, }, ) } LoupedeckEvent::KnobDown { knob } => { - let position: KnobPosition = knob.into(); + let position: KnobPosition = get_position_of_loupedeck_knob(knob); send_knob_event( KnobPath { @@ -298,14 +299,11 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv true } -fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorkerCommand) { +fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) { trace!("Handling command: {:?}", &command); match command { - IoWorkerCommand::Vibrate { pattern } => { - context.device.vibrate(pattern); - } - IoWorkerCommand::SetActivePages { key_page_id, knob_page_id } => { + HandlerCommand::SetActivePages { key_page_id, knob_page_id } => { state.active_key_page_id = key_page_id; state.active_knob_page_id = knob_page_id; @@ -329,7 +327,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke context.device.refresh_display(&key_grid.display).unwrap(); } - IoWorkerCommand::SetKeyStyle { path, value } => { + HandlerCommand::SetKeyStyle { path, value } => { state.mutate_key_for_command("SetKeyStyle", &path, |k| { k.style = value; }); @@ -337,7 +335,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke draw_key_at_path_if_visible(context, state, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); } - IoWorkerCommand::SetKnobStyle { path, value } => { + HandlerCommand::SetKnobStyle { path, value } => { state.mutate_knob_for_command("SetKnobStyle", &path, |k| { k.style = value; }); @@ -345,7 +343,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke draw_knob_at_path_if_visible(context, state, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); } - IoWorkerCommand::SetKnobValue { path, value } => { + HandlerCommand::SetKnobValue { path, value } => { if let Some(v) = value { if !(0.0..=1.0).contains(&v) { error!("Received SetKnobValue with an out-of-range value: {}", v); @@ -460,3 +458,14 @@ fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: draw_knob_at_position(context, state, path.position); } } + +fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition { + match value { + LoupedeckKnob::LeftTop => KnobPosition::LeftTop, + LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, + LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, + LoupedeckKnob::RightTop => KnobPosition::RightTop, + LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, + LoupedeckKnob::RightBottom => KnobPosition::RightBottom, + } +} diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs index 880ad50..604f87f 100644 --- a/deckster/src/runner/state.rs +++ b/deckster/src/runner/state.rs @@ -3,10 +3,10 @@ use std::collections::{HashMap, HashSet}; use enum_map::EnumMap; use log::error; +use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; +use deckster_shared::state::{Key, Knob}; + use crate::model; -use crate::model::key_page::KeyStyle; -use crate::model::knob_page::KnobStyle; -use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition}; use crate::runner::state; #[derive(Debug)] @@ -114,18 +114,3 @@ pub struct KnobPage { pub id: String, pub knobs_by_position: EnumMap, } - -#[derive(Debug)] -pub struct Key { - pub path: KeyPath, - pub base_style: KeyStyle, - pub style: Option, -} - -#[derive(Debug)] -pub struct Knob { - pub path: KnobPath, - pub base_style: KnobStyle, - pub style: Option, - pub value: Option, -} diff --git a/deckster_mode/Cargo.toml b/deckster_mode/Cargo.toml new file mode 100644 index 0000000..2c626d6 --- /dev/null +++ b/deckster_mode/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "deckster_mode" +version = "0.1.0" +edition = "2021" + +[dependencies] +deckster_shared = { path = "../deckster_shared" } +thiserror = "1.0.56" +im = "15.1.0" +toml = "0.8.8" +either = "1.9.0" \ No newline at end of file diff --git a/deckster_mode/src/lib.rs b/deckster_mode/src/lib.rs new file mode 100644 index 0000000..ba50a9c --- /dev/null +++ b/deckster_mode/src/lib.rs @@ -0,0 +1,44 @@ +use std::io; + +use either::Either; +use thiserror::Error; + +use deckster_shared::handler_communication::HandlerEvent; +use deckster_shared::path::{KeyPath, KnobPath}; + +#[derive(Debug, Error)] +pub enum RunError { + #[error("A stdin line could not be read.")] + LineIo(#[from] io::Error), + + #[error("A stdin line could not be deserialized.")] + LineDeserialization { line: String, source: toml::de::Error }, +} + +pub trait DecksterHandler { + fn handle(&mut self, event: HandlerEvent); +} + +pub fn run, im::HashMap) -> H>( + init_handler: I, +) -> Result<(), RunError> { + let mut handler: Either = Either::Right(init_handler); + + for line in io::stdin().lines() { + let line = line?; + + match handler { + Either::Left(mut h) => { + let event: HandlerEvent = toml::from_str(&line).map_err(|e| RunError::LineDeserialization { line, source: e.clone() })?; + + h.handle(event); + handler = Either::Left(h); + } + Either::Right(init_handler) => { + handler = Either::Left(init_handler(im::HashMap::new(), im::HashMap::new())); + } + } + } + + Ok(()) +} diff --git a/deckster_shared/Cargo.toml b/deckster_shared/Cargo.toml new file mode 100644 index 0000000..46d642f --- /dev/null +++ b/deckster_shared/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "deckster_shared" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.195", features = ["derive", "rc"] } +serde_with = "3.4.0" +thiserror = "1.0.56" +derive_more = "0.99.17" +rgb = "0.8.37" +enum-ordinalize = "4.3.0" +enum-map = "3.0.0-beta.2" +im = { version = "15.1.0", features = ["serde"] } +toml = { version = "0.8.8", default-features = false } \ No newline at end of file diff --git a/deckster_shared/src/handler_communication.rs b/deckster_shared/src/handler_communication.rs new file mode 100644 index 0000000..86f4ea0 --- /dev/null +++ b/deckster_shared/src/handler_communication.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +use crate::path::{KeyPath, KnobPath}; +use crate::style::{KeyStyle, KnobStyle}; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum RotationDirection { + Clockwise, + Counterclockwise, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum KnobEvent { + Press, + ButtonDown, + ButtonUp, + Rotate { direction: RotationDirection }, + VisibilityChange { is_visible: bool }, +} + +#[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 }, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum HandlerEvent { + Knob { path: KnobPath, event: KnobEvent }, + Key { path: KeyPath, event: KeyEvent }, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "kebab-case")] +pub enum HandlerCommand { + SetActivePages { key_page_id: String, knob_page_id: String }, + SetKeyStyle { path: KeyPath, value: Option }, + SetKnobStyle { path: KnobPath, value: Option }, + SetKnobValue { path: KnobPath, value: Option }, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct InitialHandlerMessage { + key_configs: im::HashMap, + knob_configs: im::HashMap, +} diff --git a/deckster/src/model/icon_descriptor.rs b/deckster_shared/src/icon_descriptor.rs similarity index 95% rename from deckster/src/model/icon_descriptor.rs rename to deckster_shared/src/icon_descriptor.rs index 73d9419..03c934b 100644 --- a/deckster/src/model/icon_descriptor.rs +++ b/deckster_shared/src/icon_descriptor.rs @@ -4,9 +4,8 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; -use thiserror::Error; -use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; +use crate::image_filter::{ImageFilter, ImageFilterFromStringError}; #[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)] pub struct IconDescriptor { @@ -14,7 +13,7 @@ pub struct IconDescriptor { pub filter: ImageFilter, } -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum IconDescriptorFromStrError { #[error("Not a valid icon identifier: {0}")] InvalidIconPackSource(String), diff --git a/deckster/src/model/image_filter.rs b/deckster_shared/src/image_filter.rs similarity index 99% rename from deckster/src/model/image_filter.rs rename to deckster_shared/src/image_filter.rs index 26c5c4d..2890cff 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster_shared/src/image_filter.rs @@ -5,12 +5,12 @@ use std::str::FromStr; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use crate::model::rgb::RGB8Wrapper; +use crate::rgb::RGB8Wrapper; #[derive(Debug, PartialEq, Clone)] pub struct ImageFilterTransform { pub scale: f32, - // Must be in 0..=3 + /// Must be in 0..=3 pub clockwise_quarter_rotations: u8, pub alpha: f32, } diff --git a/deckster_shared/src/lib.rs b/deckster_shared/src/lib.rs new file mode 100644 index 0000000..0140436 --- /dev/null +++ b/deckster_shared/src/lib.rs @@ -0,0 +1,7 @@ +pub mod handler_communication; +pub mod icon_descriptor; +pub mod image_filter; +pub mod path; +pub mod rgb; +pub mod state; +pub mod style; diff --git a/deckster_shared/src/path.rs b/deckster_shared/src/path.rs new file mode 100644 index 0000000..338c355 --- /dev/null +++ b/deckster_shared/src/path.rs @@ -0,0 +1,78 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use enum_map::Enum; +use enum_ordinalize::Ordinalize; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use thiserror::Error; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] +/// One-based coordinates of a specific virtual (not physical) key. +pub struct KeyPosition { + pub x: u16, + pub y: u16, +} + +#[derive(Debug, Error)] +#[error("The input value does not match the required format of and separated by an 'x'")] +pub struct KeyPositionFromStrError {} + +impl FromStr for KeyPosition { + type Err = KeyPositionFromStrError; + + fn from_str(s: &str) -> Result { + let values = s.split_once('x'); + + if let Some((x, y)) = values { + if let Ok(x) = u16::from_str(x) { + if let Ok(y) = u16::from_str(y) { + return Ok(KeyPosition { x, y }); + } + } + } + + Err(KeyPositionFromStrError {}) + } +} + +impl Display for KeyPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}x{}", self.x, self.y)) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +pub struct KeyPath { + pub page_id: String, + 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, Ordinalize)] +#[serde(rename_all = "kebab-case")] +pub enum KnobPosition { + LeftTop, + LeftMiddle, + LeftBottom, + RightTop, + RightMiddle, + RightBottom, +} + +impl KnobPosition { + pub fn is_left(&self) -> bool { + matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +pub struct KnobPath { + pub page_id: String, + pub position: KnobPosition, +} diff --git a/deckster/src/model/rgb.rs b/deckster_shared/src/rgb.rs similarity index 100% rename from deckster/src/model/rgb.rs rename to deckster_shared/src/rgb.rs diff --git a/deckster_shared/src/state.rs b/deckster_shared/src/state.rs new file mode 100644 index 0000000..b60aee1 --- /dev/null +++ b/deckster_shared/src/state.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use crate::path::{KeyPath, KnobPath}; +use crate::style::{KeyStyle, KnobStyle}; + +#[derive(Debug)] +pub struct Key { + pub path: KeyPath, + pub base_style: KeyStyle, + pub style: Option, +} + +#[derive(Debug)] +pub struct Knob { + pub path: KnobPath, + pub base_style: KnobStyle, + pub style: Option, + pub value: Option, +} + +pub type KeyStyleByStateMap = HashMap; +pub type KnobStyleByStateMap = HashMap; diff --git a/deckster_shared/src/style.rs b/deckster_shared/src/style.rs new file mode 100644 index 0000000..9da230a --- /dev/null +++ b/deckster_shared/src/style.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; + +use crate::icon_descriptor::IconDescriptor; +use crate::rgb::RGB8WithOptionalA; + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct KeyStyle { + pub label: Option, + pub icon: Option, + pub border: Option, +} + +impl KeyStyle { + pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle { + KeyStyle { + label: self.label.as_ref().or(base.label.as_ref()).cloned(), + icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), + border: self.border.or(base.border), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct KnobStyle { + pub label: Option, + pub icon: Option, + pub indicators: Option, +} + +impl KnobStyle { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + label: self.label.as_ref().or(base.label.as_ref()).cloned(), + icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), + indicators: self + .indicators + .as_ref() + .zip(base.indicators.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.indicators.clone()) + .or_else(|| base.indicators.clone()), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct KnobIndicators { + pub bar: Option, + pub circle: Option, +} + +impl KnobIndicators { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + bar: self + .bar + .as_ref() + .zip(base.bar.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.bar.clone()) + .or_else(|| base.bar.clone()), + circle: self + .circle + .as_ref() + .zip(base.circle.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.circle.clone()) + .or_else(|| base.circle.clone()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct KnobIndicatorBarConfig { + pub color: Option, +} + +impl KnobIndicatorBarConfig { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + color: self.color.as_ref().or(base.color.as_ref()).cloned(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct KnobIndicatorCircleConfig { + pub color: Option, + pub width: Option, + pub radius: Option, +} + +impl KnobIndicatorCircleConfig { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + color: self.color.as_ref().or(base.color.as_ref()).cloned(), + width: self.width.as_ref().or(base.width.as_ref()).cloned(), + radius: self.radius.as_ref().or(base.radius.as_ref()).cloned(), + } + } +}