From 82077a6e1e83da7f1fc7edb8065c00fac5fabf03 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Mon, 1 Jan 2024 23:05:43 +0100 Subject: [PATCH] commit --- Cargo.lock | 311 +++++++++++++++++- deckster/Cargo.toml | 7 +- deckster/examples/full/key-pages/default.toml | 2 +- deckster/examples/full/key-pages/numpad.toml | 6 +- .../examples/full/knob-pages/default.toml | 21 +- deckster/src/main.rs | 152 +++++---- .../model/{files/deckster.rs => config.rs} | 28 +- deckster/src/model/files/knob_page.rs | 22 -- deckster/src/model/files/mod.rs | 3 - deckster/src/model/icon_descriptor.rs | 13 +- deckster/src/model/image_filter.rs | 2 +- deckster/src/model/{files => }/key_page.rs | 13 +- deckster/src/model/knob_page.rs | 55 ++++ deckster/src/model/mod.rs | 39 ++- deckster/src/runner/command.rs | 6 + deckster/src/runner/mod.rs | 72 ++++ deckster/src/runner/state/mod.rs | 34 ++ 17 files changed, 656 insertions(+), 130 deletions(-) rename deckster/src/model/{files/deckster.rs => config.rs} (59%) delete mode 100644 deckster/src/model/files/knob_page.rs delete mode 100644 deckster/src/model/files/mod.rs rename deckster/src/model/{files => }/key_page.rs (82%) create mode 100644 deckster/src/model/knob_page.rs create mode 100644 deckster/src/runner/command.rs create mode 100644 deckster/src/runner/mod.rs create mode 100644 deckster/src/runner/state/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6bff92a..841722d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -129,9 +177,49 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets", + "windows-targets 0.48.5", ] +[[package]] +name = "clap" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "color-eyre" version = "0.6.2" @@ -159,6 +247,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -223,8 +317,12 @@ dependencies = [ name = "deckster" version = "0.1.0" dependencies = [ + "clap", "color-eyre", + "enum-map", + "env_logger", "humantime-serde", + "log", "loupedeck_serial", "piet", "regex", @@ -234,6 +332,7 @@ dependencies = [ "serde_with", "thiserror", "toml", + "walkdir", ] [[package]] @@ -246,6 +345,26 @@ dependencies = [ "serde", ] +[[package]] +name = "enum-map" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2a23ad36148a32085addb3ef1aa39805d044d4532ff258360d523a4eff38e5" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "1.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44600091ce205df4f8b661e98617d49c37b2dd609e449ec82b0fb5d7b33e2eeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -287,12 +406,35 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" 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" @@ -327,6 +469,18 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hex" version = "0.4.3" @@ -416,6 +570,17 @@ 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" @@ -472,6 +637,12 @@ 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 = "log" version = "0.4.20" @@ -652,12 +823,34 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[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 = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -788,6 +981,15 @@ dependencies = [ "unicode-ident", ] +[[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" @@ -988,12 +1190,28 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -1064,6 +1282,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1076,7 +1303,16 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -1085,13 +1321,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -1100,42 +1351,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.31" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index f851d8a..cca6121 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -14,4 +14,9 @@ serde_regex = "1.1.0" serde_with = "3.4.0" thiserror = "1.0.52" toml = "0.8.8" -regex = "1.10.2" \ No newline at end of file +regex = "1.10.2" +log = "0.4.20" +env_logger = "0.10.1" +clap = { version = "4.4.12", features = ["derive"] } +enum-map = "3.0.0-beta.2" +walkdir = "2.4.0" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index 148f22c..0eccbb0 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -7,7 +7,7 @@ mode.media__play_pause.icon.playing = "@ph/pause" [keys.1x2] icon = "@fad/shuffle" mode.vibrate.pattern = "low" -mode.spotify__shuffle.icon.on = "@fad/shuffle[color=#58fc11]" +mode.spotify__shuffle.icon.active = "@fad/shuffle[color=#58fc11]" [keys.2x1] icon = "@ph/timer" diff --git a/deckster/examples/full/key-pages/numpad.toml b/deckster/examples/full/key-pages/numpad.toml index d9ce18f..39dab2a 100644 --- a/deckster/examples/full/key-pages/numpad.toml +++ b/deckster/examples/full/key-pages/numpad.toml @@ -22,14 +22,14 @@ mode.keyboard.key = "5" label = "4" mode.keyboard.key = "4" -[keys.1x4] +[keys.3x4] label = "3" mode.keyboard.key = "3" -[keys.1x3] +[keys.3x3] label = "2" mode.keyboard.key = "2" -[keys.1x2] +[keys.3x2] label = "1" mode.keyboard.key = "1" \ 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 9965704..fe82365 100644 --- a/deckster/examples/full/knob-pages/default.toml +++ b/deckster/examples/full/knob-pages/default.toml @@ -1,29 +1,32 @@ [knobs.left-top] icon = "@ph/microphone-light" +indicator.circle.color = "#ffffff" +indicator.circle.width = 2 +indicator.circle.radius = 40 + mode.audio_volume.direction = "input" mode.audio_volume.regex = "Microphone" mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@ph/microphone-slash-light[alpha=90|color=#fc4646]" -mode.audio_volume.circle_indicator.color = "#ffffff" -mode.audio_volume.circle_indicator.width = 2 -mode.audio_volume.circle_indicator.radius = 40 +mode.audio_volume.icon.inactive = "@ph/microphone-slash-light[alpha=0.9|color=#fc4646]" mode.audio_volume.disable_press_to_unmute = true mode.audio_volume.muted_turn_action = "unmute-at-zero" -[knobs.left-center] +[knobs.left-middle] icon = "@apps/discord" +indicator.bar.color = "#ffffff" + mode.audio_volume.regex = "Discord" mode.audio_volume.label.inactive = "" mode.audio_volume.label.active = "{percentage}%" mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@apps/discord[grayscale,opacity=90]" -mode.audio_volume.bar_indicator.color = "#ffffff" +mode.audio_volume.icon.inactive = "@apps/discord[grayscale|alpha=0.9]" [knobs.left-bottom] icon = "@apps/spotify" +indicator.bar.color = "#ffffff" + mode.audio_volume.regex = "Spotify" mode.audio_volume.label.inactive = "" mode.audio_volume.label.active = "{percentage}%" mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@apps/spotify[grayscale,opacity=90]" -mode.audio_volume.bar_indicator.color = "#ffffff" \ No newline at end of file +mode.audio_volume.icon.inactive = "@apps/spotify[grayscale|alpha=0.9]" \ No newline at end of file diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 9ed1fa8..cdd55aa 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -1,83 +1,109 @@ -use std::thread::sleep; -use std::time::{Duration, Instant}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; -use color_eyre::eyre::ContextCompat; +use clap::{Parser, Subcommand}; +use color_eyre::eyre::{OptionExt, WrapErr}; use color_eyre::Result; -use rgb::RGB8; +use walkdir::WalkDir; -use loupedeck_serial::commands::VibrationPattern; -use loupedeck_serial::device::LoupedeckDevice; -use loupedeck_serial::events::LoupedeckEvent; +use crate::model::config::WithFallbackId; mod model; +mod runner; -fn main() -> Result<()> { - let available_devices = LoupedeckDevice::discover()?; +#[derive(Debug, Parser)] +#[command(name = "deckster")] +#[command(about = "Use Loupedeck device under Linux.")] +struct Cli { + #[command(subcommand)] + command: Command, +} - let device = available_devices.first().wrap_err("at least one device should be connected")?.connect()?; +#[derive(Debug, Subcommand)] +enum Command { + Run { + #[arg(long, required = true)] + config: PathBuf, + }, +} - println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number()); - device.set_brightness(1.0); +pub fn main() -> Result<()> { + env_logger::init(); + let cli = Cli::parse(); - // run_vibrations(&device)?; - run_rainbow(&device)?; + match cli.command { + Command::Run { config: config_path } => { + let deckster_file = read_and_deserialize::(config_path.join("./deckster.toml").as_path())?; + let config_path = config_path.canonicalize()?; + + let key_pages_by_id: HashMap = + read_and_deserialize_from_directory::(config_path.join("./key-pages").as_path())? + .into_iter() + .map(|p| model::key_page::Page { + id: p.inner.id.clone().unwrap_or(p.fallback_id), + keys: p.inner.keys, + scrolling: p.inner.scrolling, + }) + .map(|p| (p.id.clone(), p)) + .collect(); + + let knob_pages_by_id: HashMap = + read_and_deserialize_from_directory::(config_path.join("./knob-pages").as_path())? + .into_iter() + .map(|p| model::knob_page::Page { + id: p.inner.id.clone().unwrap_or(p.fallback_id), + knobs: p.inner.knobs.into_iter().collect(), + }) + .map(|p| (p.id.clone(), p)) + .collect(); + + let config = model::config::Config { + key_pages_by_id, + knob_pages_by_id, + buttons: deckster_file.buttons.into_iter().collect(), + icon_packs: deckster_file.icon_packs, + initial: deckster_file.initial, + active_button_color: deckster_file.active_button_color, + inactive_button_color: deckster_file.inactive_button_color, + }; + + runner::start(config)? + } + }; Ok(()) } -fn run_rainbow(device: &LoupedeckDevice) -> Result<()> { - let interval = Duration::from_millis(50); - let start = Instant::now(); - let mut iteration = 0; +fn read_and_deserialize(path: &Path) -> Result { + let content = fs::read_to_string(path).wrap_err_with(|| format!("Reading of {} failed.", path.to_string_lossy()))?; - let buttons = device - .characteristics() - .available_buttons - .iter() - .filter(|b| b.supports_color()) - .collect::>(); - - 'outer: loop { - let ms = start.elapsed().as_millis() as u64; - - if !device.events_channel().is_empty() { - for event in device.events_channel() { - if let LoupedeckEvent::Disconnected = event { - break 'outer; - } - - if device.events_channel().is_empty() { - break; - } - } - } - - for (index, button) in buttons.iter().enumerate() { - let t = (ms + (index * 100) as u64) as f32; - - device.set_button_color( - *button, - RGB8::new( - (((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, - (((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, - (((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, - ), - )?; - } - - sleep((iteration + 1) * interval - start.elapsed()); - iteration += 1; - } - - Ok(()) + toml::from_str::(&content).wrap_err_with(|| format!("Parsing of {} failed.", path.to_string_lossy())) } -fn run_vibrations(device: &LoupedeckDevice) -> Result<()> { - for event in device.events_channel() { - if let LoupedeckEvent::Touch { is_end: false, .. } = event { - device.vibrate(VibrationPattern::Low) +fn read_and_deserialize_from_directory(base: &Path) -> Result>> { + let mut result: Vec> = Vec::new(); + + for entry in WalkDir::new(base).follow_links(true) { + let entry = entry?; + if !entry.file_type().is_file() { + continue; + } + + let path = entry.path(); + if let Some(fallback_id) = path + .strip_prefix(base) + .unwrap() + .to_str() + .expect("Paths must be valid UTF-8") + .strip_suffix(".toml") + { + result.push(WithFallbackId { + fallback_id: fallback_id.to_owned(), + inner: read_and_deserialize::(path)?, + }); } } - Ok(()) + Ok(result) } diff --git a/deckster/src/model/files/deckster.rs b/deckster/src/model/config.rs similarity index 59% rename from deckster/src/model/files/deckster.rs rename to deckster/src/model/config.rs index 01771bc..ad597fa 100644 --- a/deckster/src/model/files/deckster.rs +++ b/deckster/src/model/config.rs @@ -1,19 +1,39 @@ +use std::collections::HashMap; + +use enum_map::EnumMap; use rgb::RGB8; use serde::Deserialize; +use crate::model; use crate::model::image_filter::ImageFilter; use crate::model::rgb::DeserializableRGB8; +use crate::model::ButtonPosition; #[derive(Debug, Deserialize)] pub struct File { - pub key_pages: Vec, - pub knob_pages: Vec, pub icon_packs: Vec, #[serde(default = "inactive_button_color_default")] pub inactive_button_color: DeserializableRGB8, #[serde(default = "active_button_color_default")] pub active_button_color: DeserializableRGB8, - pub buttons: [Option; 8], + pub buttons: HashMap, // EnumMap + pub initial: InitialConfig, +} + +#[derive(Debug)] +pub struct WithFallbackId { + pub fallback_id: String, + pub inner: T, +} + +#[derive(Debug)] +pub struct Config { + pub key_pages_by_id: HashMap, + pub knob_pages_by_id: HashMap, + pub icon_packs: Vec, + pub inactive_button_color: DeserializableRGB8, + pub active_button_color: DeserializableRGB8, + pub buttons: EnumMap, pub initial: InitialConfig, } @@ -25,7 +45,7 @@ fn active_button_color_default() -> DeserializableRGB8 { DeserializableRGB8(RGB8::new(0, 255, 0)) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Default)] pub struct ButtonConfig { pub key_page: Option, pub knob_page: Option, diff --git a/deckster/src/model/files/knob_page.rs b/deckster/src/model/files/knob_page.rs deleted file mode 100644 index bffa5b2..0000000 --- a/deckster/src/model/files/knob_page.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::collections::HashMap; - -use serde::Deserialize; - -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::{knob_modes, KnobPosition}; - -#[derive(Debug, Deserialize)] -pub struct File { - pub knobs: HashMap, -} - -#[derive(Debug, Deserialize)] -pub struct Knob { - pub icon: IconDescriptor, - pub mode: KnobModes, -} - -#[derive(Debug, Deserialize)] -pub struct KnobModes { - pub audio_volume: knob_modes::audio_volume::Config, -} diff --git a/deckster/src/model/files/mod.rs b/deckster/src/model/files/mod.rs deleted file mode 100644 index 76a823b..0000000 --- a/deckster/src/model/files/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod deckster; -pub mod key_page; -pub mod knob_page; diff --git a/deckster/src/model/icon_descriptor.rs b/deckster/src/model/icon_descriptor.rs index 8073cae..3b4202b 100644 --- a/deckster/src/model/icon_descriptor.rs +++ b/deckster/src/model/icon_descriptor.rs @@ -6,7 +6,7 @@ use thiserror::Error; use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; -#[derive(Debug, DeserializeFromStr)] +#[derive(Debug, Default, Clone, DeserializeFromStr)] pub struct IconDescriptor { pub source: IconDescriptorSource, pub filter: Option, @@ -30,7 +30,7 @@ impl FromStr for IconDescriptor { fn from_str(s: &str) -> Result { let (raw_source, raw_filter) = s.split_once('[').unwrap_or((s, "")); - let source = if raw_source.starts_with('@') { + let source = if let Some(raw_source) = raw_source.strip_prefix('@') { let (pack_id, icon_id) = raw_source .split_once('/') .ok_or_else(|| IconDescriptorFromStrError::InvalidIconPackSource(raw_source.to_string()))?; @@ -57,8 +57,13 @@ impl FromStr for IconDescriptor { } } -#[derive(Debug)] +#[derive(Debug, Default, Clone)] pub enum IconDescriptorSource { - IconPack { pack_id: String, icon_id: String }, + #[default] + None, + IconPack { + pack_id: String, + icon_id: String, + }, Path(PathBuf), } diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 7126343..1960eb5 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::model::geometry::parse_positive_rect_from_str; use crate::model::rgb::parse_rgb8_from_hex_str; -#[derive(Debug, DeserializeFromStr)] +#[derive(Debug, Clone, DeserializeFromStr)] pub struct ImageFilter { pub crop_original: Option, // applied before scale and rotate pub scale: f32, diff --git a/deckster/src/model/files/key_page.rs b/deckster/src/model/key_page.rs similarity index 82% rename from deckster/src/model/files/key_page.rs rename to deckster/src/model/key_page.rs index 636cb59..b092783 100644 --- a/deckster/src/model/files/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -8,7 +8,15 @@ use crate::model::{key_modes, KnobPosition}; #[derive(Debug, Deserialize)] pub struct File { - pub scrolling: ScrollingConfig, + pub id: Option, + pub scrolling: Option, + pub keys: HashMap, +} + +#[derive(Debug)] +pub struct Page { + pub id: String, + pub scrolling: Option, pub keys: HashMap, } @@ -27,7 +35,8 @@ pub struct ScrollingKeysConfig { pub next: UIntVec2, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum ScrollingConfigAxis { #[default] Vertical, diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs new file mode 100644 index 0000000..c7f84b2 --- /dev/null +++ b/deckster/src/model/knob_page.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use enum_map::EnumMap; +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::rgb::DeserializableRGB8WithOptionalAlpha; +use crate::model::{knob_modes, KnobPosition}; + +#[derive(Debug, Deserialize)] +pub struct File { + pub id: Option, + pub knobs: HashMap, +} + +#[derive(Debug)] +pub struct Page { + pub id: String, + pub knobs: EnumMap, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Knob { + #[serde(default)] + pub label: String, + #[serde(default)] + pub icon: IconDescriptor, + #[serde(default)] + pub indicator: KnobIndicators, + #[serde(default)] + pub mode: KnobModes, +} + +#[derive(Debug, Default, Deserialize)] +pub struct KnobIndicators { + pub bar: Option, + pub circle: Option, +} + +#[derive(Debug, Deserialize)] +pub struct KnobIndicatorBarConfig { + pub color: DeserializableRGB8WithOptionalAlpha, +} + +#[derive(Debug, Deserialize)] +pub struct KnobIndicatorCircleConfig { + pub color: DeserializableRGB8WithOptionalAlpha, + pub width: u8, + pub radius: u8, +} + +#[derive(Debug, Default, Deserialize)] +pub struct KnobModes { + pub audio_volume: Option, +} diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index fb1c44e..fee147c 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -1,14 +1,17 @@ +use enum_map::Enum; use serde::Deserialize; -mod files; -mod geometry; -mod icon_descriptor; -mod image_filter; -mod key_modes; -mod knob_modes; -mod rgb; +pub mod config; +pub mod geometry; +pub mod icon_descriptor; +pub mod image_filter; +pub mod key_modes; +pub mod key_page; +pub mod knob_modes; +pub mod knob_page; +pub mod rgb; -#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] #[serde(rename_all = "kebab-case")] pub enum KnobPosition { LeftTop, @@ -18,3 +21,23 @@ pub enum KnobPosition { RightMiddle, RightBottom, } + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] +pub enum ButtonPosition { + #[serde(rename = "0")] + N0, + #[serde(rename = "1")] + N1, + #[serde(rename = "2")] + N2, + #[serde(rename = "3")] + N3, + #[serde(rename = "4")] + N4, + #[serde(rename = "5")] + N5, + #[serde(rename = "6")] + N6, + #[serde(rename = "7")] + N7, +} diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs new file mode 100644 index 0000000..8d12efe --- /dev/null +++ b/deckster/src/runner/command.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub enum RendererCommand { + SetButton, +} diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs new file mode 100644 index 0000000..301ba55 --- /dev/null +++ b/deckster/src/runner/mod.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use color_eyre::eyre::{ContextCompat, WrapErr}; +use color_eyre::Result; + +use loupedeck_serial::commands::VibrationPattern; +use loupedeck_serial::device::LoupedeckDevice; +use state::State; + +use crate::model; + +mod command; +mod state; + +pub fn start(config: model::config::Config) -> Result<()> { + let state = create_state(&config)?; + + let device = LoupedeckDevice::discover()? + .first() + .wrap_err("No device connected.")? + .connect() + .wrap_err("Connecting to the device failed.")?; + + device.vibrate(VibrationPattern::RiseFall); + + Ok(()) +} + +fn create_state(config: &model::config::Config) -> Result { + let key_pages_by_id: HashMap<_, _> = config + .key_pages_by_id + .iter() + .map(|(id, p)| state::KeyPage { id: id.clone() }) + .map(|p| (p.id.clone(), Rc::new(p))) + .collect(); + + let knob_pages_by_id: HashMap<_, _> = config + .knob_pages_by_id + .iter() + .map(|(id, p)| state::KnobPage { + id: id.clone(), + knobs_by_position: p + .knobs + .iter() + .map(|(p, k)| state::Knob { + id: p, + label: k.label.clone(), + icon: k.icon.clone(), + value: 0.0, + }) + .map(|k| (k.id, Some(k))) + .collect(), + }) + .map(|p| (p.id.clone(), Rc::new(p))) + .collect(); + + Ok(State { + active_key_page: Rc::clone( + key_pages_by_id + .get(&config.initial.key_page) + .wrap_err_with(|| format!("There is no key page with the ID specified at initial.key_page: {}", &config.initial.key_page))?, + ), + active_knob_page: Rc::clone( + knob_pages_by_id + .get(&config.initial.knob_page) + .wrap_err_with(|| format!("There is no key page with the ID specified at initial.knob_page: {}", &config.initial.key_page))?, + ), + key_pages_by_id, + knob_pages_by_id, + }) +} diff --git a/deckster/src/runner/state/mod.rs b/deckster/src/runner/state/mod.rs new file mode 100644 index 0000000..687768f --- /dev/null +++ b/deckster/src/runner/state/mod.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use enum_map::EnumMap; + +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::KnobPosition; + +#[derive(Debug)] +pub struct State { + pub active_key_page: Rc, + pub active_knob_page: Rc, + pub key_pages_by_id: HashMap>, + pub knob_pages_by_id: HashMap>, +} + +#[derive(Debug)] +pub struct KeyPage { + pub id: String, +} + +#[derive(Debug)] +pub struct KnobPage { + pub id: String, + pub knobs_by_position: EnumMap>, +} + +#[derive(Debug)] +pub struct Knob { + pub id: KnobPosition, + pub icon: IconDescriptor, + pub label: String, + pub value: f32, +}