commit
This commit is contained in:
parent
eb0983e79d
commit
1904e3e96a
38 changed files with 555 additions and 569 deletions
125
Cargo.lock
generated
125
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"deckster",
|
||||
"deckster_mode",
|
||||
"deckster_shared",
|
||||
"loupedeck_serial",
|
||||
"pa-volume-interface"
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]"
|
||||
|
||||
handler = "home-assistant switch"
|
||||
config.name = "switch.mwin"
|
||||
config.style.on.icon = "@ph/computer-tower[color=#58fc11]"
|
|
@ -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 😇"
|
|
@ -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"
|
|
@ -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 →"
|
|
@ -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]"
|
||||
config.style.muted.indicators.bar.color = "#fc464690"
|
||||
config.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]"
|
|
@ -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<Pixmap> {
|
||||
let mut result = original.clone();
|
||||
|
|
|
@ -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<IconDescriptor> {
|
||||
let mut result: HashSet<IconDescriptor> = HashSet::new();
|
||||
|
||||
fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &key_page::StyleByStateMap<T>) {
|
||||
fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &KeyStyleByStateMap<T>) {
|
||||
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<IconDescriptor> {
|
|||
});
|
||||
}
|
||||
|
||||
fn insert_all_from_knob_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &knob_page::StyleByStateMap<T>) {
|
||||
fn insert_all_from_knob_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &KnobStyleByStateMap<T>) {
|
||||
map.values().for_each(|v| {
|
||||
if let Some(icon) = &v.icon {
|
||||
result.insert(icon.clone());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub border: Option<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
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<Arc<modes::key::playerctl::ButtonConfig>>,
|
||||
pub playerctl__shuffle: Option<Arc<modes::key::playerctl::ShuffleConfig>>,
|
||||
pub playerctl__loop: Option<Arc<modes::key::playerctl::LoopConfig>>,
|
||||
pub vibrate: Option<Arc<modes::key::vibrate::Config>>,
|
||||
}
|
||||
|
||||
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;
|
||||
|
|
|
@ -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<KnobIndicatorBarConfig>,
|
||||
pub circle: Option<KnobIndicatorCircleConfig>,
|
||||
}
|
||||
|
||||
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<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
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<RGB8WithOptionalA>,
|
||||
pub width: Option<u8>,
|
||||
pub radius: Option<u8>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub indicators: Option<KnobIndicators>,
|
||||
}
|
||||
|
||||
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<Arc<modes::knob::audio_volume::Config>>,
|
||||
}
|
||||
|
||||
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <x> and <y> separated by an 'x'")]
|
||||
pub struct KeyPositionFromStrError {}
|
||||
|
||||
impl FromStr for KeyPosition {
|
||||
type Err = KeyPositionFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<LoupedeckKnob> 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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<SwitchState>,
|
||||
pub style: KeyStyleByStateMap<SwitchState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ButtonConfig {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub style: StyleByStateMap<ButtonState>,
|
||||
pub style: KeyStyleByStateMap<ButtonState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||
|
|
|
@ -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<Item = (KeyPath, Arc<model::key_page::Key>)>,
|
||||
events: broadcast::Sender<(KeyPath, KeyEvent)>,
|
||||
commands: flume::Sender<IoWorkerCommand>,
|
||||
commands: flume::Sender<HandlerCommand>,
|
||||
) {
|
||||
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)]
|
||||
|
|
|
@ -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<ButtonState>,
|
||||
pub style: KeyStyleByStateMap<ButtonState>,
|
||||
pub command: ButtonCommand,
|
||||
}
|
||||
|
||||
|
@ -47,7 +48,7 @@ pub enum ButtonState {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct ShuffleConfig {
|
||||
#[serde(default)]
|
||||
pub style: StyleByStateMap<ShuffleState>,
|
||||
pub style: KeyStyleByStateMap<ShuffleState>,
|
||||
}
|
||||
|
||||
#[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<LoopState>,
|
||||
pub style: KeyStyleByStateMap<LoopState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
|
||||
|
@ -164,7 +165,7 @@ static STATE_WATCHER_LOOP: Lazy<PlayerctlStateWatcher<LoopState>> = Lazy::new(||
|
|||
})
|
||||
});
|
||||
|
||||
pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
|
||||
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<ButtonConfig>, 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<ButtonConfig>, mut events:
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
|
||||
let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state();
|
||||
|
||||
loop {
|
||||
|
@ -216,7 +217,7 @@ pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, 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<ShuffleConfig>, mut event
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
|
||||
let mut state = STATE_WATCHER_LOOP.subscribe_to_state();
|
||||
|
||||
loop {
|
||||
|
@ -253,7 +254,7 @@ pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, 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();
|
||||
|
|
|
@ -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<Config>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<State>,
|
||||
pub style: KnobStyleByStateMap<State>,
|
||||
}
|
||||
|
||||
#[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<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<IoWorkerCommand>) {
|
||||
pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<HandlerCommand>) {
|
||||
let mut entity_state: Option<Arc<PaEntityState>> = None;
|
||||
|
||||
let pa_volume_interface = &PA_VOLUME_INTERFACE;
|
||||
|
@ -162,7 +161,7 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
|
|||
|
||||
move |entity_state: &Option<Arc<PaEntityState>>| {
|
||||
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<Config>, mut events: broadcast::
|
|||
}
|
||||
|
||||
commands
|
||||
.send(IoWorkerCommand::SetKnobStyle {
|
||||
.send(HandlerCommand::SetKnobStyle {
|
||||
path: path.clone(),
|
||||
value: style,
|
||||
})
|
||||
|
|
|
@ -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<Item = (KnobPath, Arc<model::knob_page::Knob>)>,
|
||||
events: broadcast::Sender<(KnobPath, KnobEvent)>,
|
||||
commands: flume::Sender<IoWorkerCommand>,
|
||||
commands: flume::Sender<HandlerCommand>,
|
||||
) {
|
||||
for (path, config) in knobs {
|
||||
if let Some(c) = &config.mode.audio_volume {
|
||||
|
|
|
@ -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<KeyStyle> },
|
||||
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
|
||||
SetKnobValue { path: KnobPath, value: Option<f32> },
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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::<IoWorkerCommand>(5);
|
||||
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(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<model::config::Config>,
|
||||
device: LoupedeckDevice,
|
||||
commands_sender: flume::Sender<IoWorkerCommand>,
|
||||
commands_sender: flume::Sender<HandlerCommand>,
|
||||
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
|
||||
knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>,
|
||||
graphics: GraphicsContext,
|
||||
|
@ -137,7 +138,7 @@ impl IoWorkerContext {
|
|||
config: Arc<model::config::Config>,
|
||||
icons: LoadedIconsMap,
|
||||
device: LoupedeckDevice,
|
||||
commands_sender: flume::Sender<IoWorkerCommand>,
|
||||
commands_sender: flume::Sender<HandlerCommand>,
|
||||
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<IoWorkerCommand>) {
|
||||
fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<KnobPosition, Knob>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Key {
|
||||
pub path: KeyPath,
|
||||
pub base_style: KeyStyle,
|
||||
pub style: Option<KeyStyle>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Knob {
|
||||
pub path: KnobPath,
|
||||
pub base_style: KnobStyle,
|
||||
pub style: Option<KnobStyle>,
|
||||
pub value: Option<f32>,
|
||||
}
|
||||
|
|
11
deckster_mode/Cargo.toml
Normal file
11
deckster_mode/Cargo.toml
Normal file
|
@ -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"
|
44
deckster_mode/src/lib.rs
Normal file
44
deckster_mode/src/lib.rs
Normal file
|
@ -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<H: DecksterHandler, I: FnOnce(im::HashMap<KeyPath, (String, toml::Value)>, im::HashMap<KnobPath, (String, toml::Value)>) -> H>(
|
||||
init_handler: I,
|
||||
) -> Result<(), RunError> {
|
||||
let mut handler: Either<H, I> = 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(())
|
||||
}
|
15
deckster_shared/Cargo.toml
Normal file
15
deckster_shared/Cargo.toml
Normal file
|
@ -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 }
|
55
deckster_shared/src/handler_communication.rs
Normal file
55
deckster_shared/src/handler_communication.rs
Normal file
|
@ -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<KeyStyle> },
|
||||
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
|
||||
SetKnobValue { path: KnobPath, value: Option<f32> },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct InitialHandlerMessage<KeyConfig: Clone, KnobConfig: Clone> {
|
||||
key_configs: im::HashMap<KeyPath, (String, KeyConfig)>,
|
||||
knob_configs: im::HashMap<KnobPath, (String, KnobConfig)>,
|
||||
}
|
|
@ -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),
|
|
@ -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,
|
||||
}
|
7
deckster_shared/src/lib.rs
Normal file
7
deckster_shared/src/lib.rs
Normal file
|
@ -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;
|
78
deckster_shared/src/path.rs
Normal file
78
deckster_shared/src/path.rs
Normal file
|
@ -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 <x> and <y> separated by an 'x'")]
|
||||
pub struct KeyPositionFromStrError {}
|
||||
|
||||
impl FromStr for KeyPosition {
|
||||
type Err = KeyPositionFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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,
|
||||
}
|
22
deckster_shared/src/state.rs
Normal file
22
deckster_shared/src/state.rs
Normal file
|
@ -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<KeyStyle>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Knob {
|
||||
pub path: KnobPath,
|
||||
pub base_style: KnobStyle,
|
||||
pub style: Option<KnobStyle>,
|
||||
pub value: Option<f32>,
|
||||
}
|
||||
|
||||
pub type KeyStyleByStateMap<State> = HashMap<State, KeyStyle>;
|
||||
pub type KnobStyleByStateMap<State> = HashMap<State, KnobStyle>;
|
101
deckster_shared/src/style.rs
Normal file
101
deckster_shared/src/style.rs
Normal file
|
@ -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<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub border: Option<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub indicators: Option<KnobIndicators>,
|
||||
}
|
||||
|
||||
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<KnobIndicatorBarConfig>,
|
||||
pub circle: Option<KnobIndicatorCircleConfig>,
|
||||
}
|
||||
|
||||
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<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
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<RGB8WithOptionalA>,
|
||||
pub width: Option<u8>,
|
||||
pub radius: Option<u8>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue