mirror of
https://codeberg.org/Fl1tzi/microdeck.git
synced 2024-05-19 19:20:20 +00:00
system fonts, spaces
This commit is contained in:
parent
69da819aa5
commit
5ddff29c87
|
@ -20,3 +20,4 @@ rusttype = "0.9.3"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
font-loader = "0.11.0"
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
use serde::Deserialize;
|
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
env,
|
env,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
fs,
|
fs,
|
||||||
|
hash::Hash,
|
||||||
io::ErrorKind,
|
io::ErrorKind,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
collections::HashMap,
|
sync::Arc,
|
||||||
sync::Arc
|
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
/// The name of the folder which holds the config
|
/// The name of the folder which holds the config
|
||||||
pub const CONFIG_FOLDER_NAME: &'static str = "microdeck";
|
pub const CONFIG_FOLDER_NAME: &'static str = "microdeck";
|
||||||
pub const CONFIG_FILE: &'static str = "config.json";
|
pub const CONFIG_FILE: &'static str = "config.json";
|
||||||
|
|
||||||
|
/// Combination of buttons acting as a folder which a device can switch to
|
||||||
|
pub type Space = Vec<Arc<Button>>;
|
||||||
|
|
||||||
|
/// CONFIGURATION
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub global: Option<GlobalConfig>,
|
pub global: Option<GlobalConfig>,
|
||||||
pub devices: Vec<DeviceConfig>,
|
pub devices: Vec<DeviceConfig>,
|
||||||
|
pub spaces: Arc<HashMap<String, Space>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// settings that effect all devices
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct GlobalConfig;
|
pub struct GlobalConfig {
|
||||||
|
pub font_family: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// configuration of a single device with its default page
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct DeviceConfig {
|
pub struct DeviceConfig {
|
||||||
pub serial: String,
|
pub serial: String,
|
||||||
|
@ -42,13 +52,18 @@ pub struct Button {
|
||||||
pub index: u8,
|
pub index: u8,
|
||||||
pub module: String,
|
pub module: String,
|
||||||
/// options which get passed to the module
|
/// options which get passed to the module
|
||||||
pub options: Option<HashMap<String, String>>,
|
#[serde(default = "new_hashmap")]
|
||||||
/// allows to overwrite what it will do on a click (executes in /bin/sh)
|
pub options: HashMap<String, String>,
|
||||||
|
/// allows to overwrite what it will do on a click
|
||||||
pub on_click: Option<String>,
|
pub on_click: Option<String>,
|
||||||
/// allows to overwrite what it will do on a release
|
/// allows to overwrite what it will do on a release
|
||||||
pub on_release: Option<String>,
|
pub on_release: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_hashmap() -> HashMap<String, String> {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn load_config() -> Result<Config, ConfigError> {
|
pub fn load_config() -> Result<Config, ConfigError> {
|
||||||
let config_file: PathBuf = match env::var_os("DACH_DECKER_CONFIG") {
|
let config_file: PathBuf = match env::var_os("DACH_DECKER_CONFIG") {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Button, ConfigError, DeviceConfig},
|
config::{Button, ConfigError, DeviceConfig, Space},
|
||||||
modules::{retrieve_module_from_name, start_module, HostEvent},
|
modules::{retrieve_module_from_name, start_module, HostEvent},
|
||||||
unwrap_or_error,
|
unwrap_or_error,
|
||||||
};
|
};
|
||||||
|
@ -41,6 +41,7 @@ pub struct Device {
|
||||||
device: Arc<AsyncStreamDeck>,
|
device: Arc<AsyncStreamDeck>,
|
||||||
modules_runtime: Option<Runtime>,
|
modules_runtime: Option<Runtime>,
|
||||||
config: DeviceConfig,
|
config: DeviceConfig,
|
||||||
|
spaces: Arc<HashMap<String, Space>>,
|
||||||
serial: String,
|
serial: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ impl Device {
|
||||||
serial: String,
|
serial: String,
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
device_conf: DeviceConfig,
|
device_conf: DeviceConfig,
|
||||||
|
spaces: Arc<HashMap<String, Space>>,
|
||||||
hid: &HidApi,
|
hid: &HidApi,
|
||||||
) -> Result<Device, DeviceError> {
|
) -> Result<Device, DeviceError> {
|
||||||
// connect to deck or continue to next
|
// connect to deck or continue to next
|
||||||
|
@ -78,6 +80,7 @@ impl Device {
|
||||||
device: deck,
|
device: deck,
|
||||||
modules_runtime: None,
|
modules_runtime: None,
|
||||||
config: device_conf,
|
config: device_conf,
|
||||||
|
spaces,
|
||||||
serial,
|
serial,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -108,16 +111,16 @@ impl Device {
|
||||||
let dev = self.device.clone();
|
let dev = self.device.clone();
|
||||||
let b = btn.clone();
|
let b = btn.clone();
|
||||||
|
|
||||||
runtime.spawn(async move {
|
runtime
|
||||||
start_module(ser, b, module, dev, module_receiver).await
|
.spawn(async move { start_module(ser, b, module, dev, module_receiver).await });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// if the receiver already dropped the listener then just directly insert none.
|
// if the receiver already dropped the listener then just directly insert none.
|
||||||
// Optimizes performance because the key_listener just does not try to send the event.
|
// Optimizes performance because the key_listener just does not try to send the event.
|
||||||
if module_sender.is_closed() {
|
if module_sender.is_closed() {
|
||||||
self.modules.insert(btn.index, (btn.clone(), None));
|
self.modules.insert(btn.index, (btn.clone(), None));
|
||||||
} else {
|
} else {
|
||||||
self.modules.insert(btn.index, (btn.clone(), Some(module_sender)));
|
self.modules
|
||||||
|
.insert(btn.index, (btn.clone(), Some(module_sender)));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,11 +140,7 @@ impl Device {
|
||||||
if let Some(handle) = self.modules_runtime.take() {
|
if let Some(handle) = self.modules_runtime.take() {
|
||||||
handle.shutdown_background();
|
handle.shutdown_background();
|
||||||
}
|
}
|
||||||
}
|
self.modules = HashMap::new();
|
||||||
|
|
||||||
/// if there is no runtime left or the runtime never got initialized
|
|
||||||
pub fn is_dropped(&self) -> bool {
|
|
||||||
self.modules_runtime.is_none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// if this device holds any modules
|
/// if this device holds any modules
|
||||||
|
@ -172,22 +171,36 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Switch to a space. This will tear down the whole runtime of the current space.
|
||||||
|
#[tracing::instrument(skip_all, fields(serial = self.serial))]
|
||||||
|
async fn switch_to_space(&mut self, name: &String) {
|
||||||
|
debug!("Switching to space {}", name);
|
||||||
|
self.drop();
|
||||||
|
if let Some(space) = self.spaces.get(name) {
|
||||||
|
self.config.buttons = space.clone();
|
||||||
|
self.device.reset().await.unwrap();
|
||||||
|
self.init_modules().await;
|
||||||
|
} else {
|
||||||
|
error!("Space {} was not found", name);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle all incoming button state updates from the listener (shell actions, module sender)
|
/// Handle all incoming button state updates from the listener (shell actions, module sender)
|
||||||
async fn button_state_update(&mut self, event: ButtonStateUpdate) {
|
async fn button_state_update(&mut self, event: ButtonStateUpdate) {
|
||||||
// get the index out of the enum...
|
// get the index out of the enum...
|
||||||
let index = match event {
|
let index = match event {
|
||||||
ButtonStateUpdate::ButtonUp(i) => i,
|
ButtonStateUpdate::ButtonUp(i) => i,
|
||||||
ButtonStateUpdate::ButtonDown(i) => i
|
ButtonStateUpdate::ButtonDown(i) => i,
|
||||||
};
|
};
|
||||||
// try to get config for the module
|
// try to get config for the module
|
||||||
let options = match self.modules.get_mut(&index) {
|
let options = match self.modules.get_mut(&index) {
|
||||||
Some(options) => options,
|
Some(options) => options,
|
||||||
None => return
|
None => return,
|
||||||
};
|
};
|
||||||
// action will only be some if on_click/on_release is specified in config
|
// action will only be some if on_click/on_release is specified in config
|
||||||
let (action, event) = match event {
|
let (action, event) = match event {
|
||||||
ButtonStateUpdate::ButtonDown(_) => (&options.0.on_click, HostEvent::ButtonPressed),
|
ButtonStateUpdate::ButtonDown(_) => (&options.0.on_click, HostEvent::ButtonPressed),
|
||||||
ButtonStateUpdate::ButtonUp(_) => (&options.0.on_release, HostEvent::ButtonReleased)
|
ButtonStateUpdate::ButtonUp(_) => (&options.0.on_release, HostEvent::ButtonReleased),
|
||||||
};
|
};
|
||||||
// try to send to module and drop the sender if the receiver was droppped
|
// try to send to module and drop the sender if the receiver was droppped
|
||||||
if let Some(sender) = options.to_owned().1 {
|
if let Some(sender) = options.to_owned().1 {
|
||||||
|
@ -200,10 +213,17 @@ impl Device {
|
||||||
if let Some(action) = action {
|
if let Some(action) = action {
|
||||||
execute_sh(&action).await
|
execute_sh(&action).await
|
||||||
}
|
}
|
||||||
|
// switch space if needed
|
||||||
|
if options.0.module == "space" {
|
||||||
|
let name = match options.0.options.get("NAME") {
|
||||||
|
Some(n) => n.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
self.switch_to_space(&name).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn execute_sh(command: &str) {
|
pub async fn execute_sh(command: &str) {
|
||||||
match Command::new("sh").arg(command).output().await {
|
match Command::new("sh").arg(command).output().await {
|
||||||
Ok(o) => debug!("Command \'{}\' returned: {}", command, o.status),
|
Ok(o) => debug!("Command \'{}\' returned: {}", command, o.status),
|
||||||
|
|
76
src/main.rs
76
src/main.rs
|
@ -1,19 +1,28 @@
|
||||||
|
use crate::config::{Config, DeviceConfig, GlobalConfig};
|
||||||
use deck_driver as streamdeck;
|
use deck_driver as streamdeck;
|
||||||
use crate::config::{Config, DeviceConfig};
|
|
||||||
use device::Device;
|
use device::Device;
|
||||||
|
use font_loader::system_fonts::{FontProperty, FontPropertyBuilder};
|
||||||
use hidapi::HidApi;
|
use hidapi::HidApi;
|
||||||
use std::{collections::HashMap, process::exit, time::Duration};
|
use rusttype::Font;
|
||||||
use tracing::{debug, error, info, warn};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
process::exit,
|
||||||
|
sync::{Arc, Mutex, OnceLock},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tracing::{debug, error, info, trace, warn};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
|
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use config::load_config;
|
use config::{load_config, Space};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod device;
|
mod device;
|
||||||
mod modules;
|
mod modules;
|
||||||
|
|
||||||
|
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! skip_if_none {
|
macro_rules! skip_if_none {
|
||||||
($res:expr) => {
|
($res:expr) => {
|
||||||
|
@ -53,6 +62,29 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// load font
|
||||||
|
// TODO: make this prettier
|
||||||
|
let font = match config.global {
|
||||||
|
Some(ref g) => {
|
||||||
|
if let Some(family) = &g.font_family {
|
||||||
|
font_loader::system_fonts::get(
|
||||||
|
&mut FontPropertyBuilder::new().family(family.as_str()).build(),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
warn!("Unable to load custom font");
|
||||||
|
load_system_font()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
load_system_font()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => load_system_font(),
|
||||||
|
};
|
||||||
|
|
||||||
|
GLOBAL_FONT
|
||||||
|
.set(Font::try_from_vec(font.0).expect("Unable to parse font. Maybe try another font?"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
debug!("{:#?}", config);
|
debug!("{:#?}", config);
|
||||||
|
|
||||||
let hid = streamdeck::new_hidapi().expect("Could not create HidApi");
|
let hid = streamdeck::new_hidapi().expect("Could not create HidApi");
|
||||||
|
@ -64,6 +96,12 @@ fn main() {
|
||||||
.block_on(start(config, hid))
|
.block_on(start(config, hid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_system_font() -> (Vec<u8>, i32) {
|
||||||
|
debug!("Retrieving system font");
|
||||||
|
font_loader::system_fonts::get(&mut FontPropertyBuilder::new().monospace().build())
|
||||||
|
.expect("Unable to load system monospace font. Please specify a custom font in the config.")
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(config: Config, mut hid: HidApi) {
|
pub async fn start(config: Config, mut hid: HidApi) {
|
||||||
let mut devices: HashMap<String, Device> = HashMap::new();
|
let mut devices: HashMap<String, Device> = HashMap::new();
|
||||||
|
|
||||||
|
@ -72,7 +110,7 @@ pub async fn start(config: Config, mut hid: HidApi) {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// check for devices that can be removed
|
// check for devices that can be removed
|
||||||
let mut removable_devices = Vec::new();
|
/* let mut removable_devices = Vec::new();
|
||||||
for (key, device) in &devices {
|
for (key, device) in &devices {
|
||||||
if device.is_dropped() {
|
if device.is_dropped() {
|
||||||
removable_devices.push(key.to_owned());
|
removable_devices.push(key.to_owned());
|
||||||
|
@ -80,7 +118,7 @@ pub async fn start(config: Config, mut hid: HidApi) {
|
||||||
}
|
}
|
||||||
for d in removable_devices {
|
for d in removable_devices {
|
||||||
devices.remove(&d);
|
devices.remove(&d);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// refresh device list
|
// refresh device list
|
||||||
if let Err(e) = streamdeck::refresh_device_list(&mut hid) {
|
if let Err(e) = streamdeck::refresh_device_list(&mut hid) {
|
||||||
|
@ -90,12 +128,20 @@ pub async fn start(config: Config, mut hid: HidApi) {
|
||||||
// if the device is not ignored and device is not already started
|
// if the device is not ignored and device is not already started
|
||||||
if !ignore_devices.contains(&hw_device.1) && devices.get(&hw_device.1).is_none() {
|
if !ignore_devices.contains(&hw_device.1) && devices.get(&hw_device.1).is_none() {
|
||||||
debug!("New device detected: {}", &hw_device.1);
|
debug!("New device detected: {}", &hw_device.1);
|
||||||
if let Some(device_config) =
|
// match regex for device serial
|
||||||
config.devices.iter().find(|d| d.serial == hw_device.1)
|
if let Some(device_config) = config
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.serial == hw_device.1 || d.serial == "*")
|
||||||
{
|
{
|
||||||
// start the device and its listener
|
// start the device and its listener
|
||||||
if let Some(device) =
|
if let Some(device) = start_device(
|
||||||
start_device(hw_device, &hid, device_config.clone()).await
|
hw_device,
|
||||||
|
&hid,
|
||||||
|
device_config.clone(),
|
||||||
|
config.spaces.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
devices.insert(device.serial(), device);
|
devices.insert(device.serial(), device);
|
||||||
}
|
}
|
||||||
|
@ -117,16 +163,11 @@ pub async fn start_device(
|
||||||
device: (streamdeck::info::Kind, String),
|
device: (streamdeck::info::Kind, String),
|
||||||
hid: &HidApi,
|
hid: &HidApi,
|
||||||
device_config: DeviceConfig,
|
device_config: DeviceConfig,
|
||||||
|
spaces: Arc<HashMap<String, Space>>,
|
||||||
) -> Option<Device> {
|
) -> Option<Device> {
|
||||||
match Device::new(device.1, device.0, device_config, &hid).await {
|
match Device::new(device.1, device.0, device_config, spaces, &hid).await {
|
||||||
Ok(mut device) => {
|
Ok(mut device) => {
|
||||||
info!("Connected");
|
info!("Connected");
|
||||||
// start all modules or print out the error
|
|
||||||
/* device_config.buttons.iter().for_each(|button| {
|
|
||||||
device
|
|
||||||
.create_module(&button)
|
|
||||||
.unwrap_or_else(|e| error!("{}", e))
|
|
||||||
});*/
|
|
||||||
device.init_modules().await;
|
device.init_modules().await;
|
||||||
device.key_listener().await;
|
device.key_listener().await;
|
||||||
if !device.has_modules() {
|
if !device.has_modules() {
|
||||||
|
@ -140,4 +181,3 @@ pub async fn start_device(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
mod blank;
|
mod blank;
|
||||||
mod counter;
|
mod counter;
|
||||||
|
mod space;
|
||||||
|
|
||||||
|
// modules
|
||||||
use self::counter::Counter;
|
use self::counter::Counter;
|
||||||
|
use self::space::Space;
|
||||||
|
|
||||||
|
use crate::GLOBAL_FONT;
|
||||||
|
// other things
|
||||||
use crate::config::Button;
|
use crate::config::Button;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
pub use deck_driver as streamdeck;
|
pub use deck_driver as streamdeck;
|
||||||
use futures_util::Future;
|
use futures_util::Future;
|
||||||
use image::DynamicImage;
|
use image::{DynamicImage, Rgb, RgbImage};
|
||||||
|
use imageproc::drawing::draw_text_mut;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use rusttype::Scale;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::{error::Error, sync::Arc};
|
use std::{error::Error, sync::Arc};
|
||||||
pub use streamdeck::info::ImageFormat;
|
pub use streamdeck::info::ImageFormat;
|
||||||
use streamdeck::info::Kind;
|
use streamdeck::info::Kind;
|
||||||
|
@ -22,6 +31,7 @@ lazy_static! {
|
||||||
static ref MODULE_MAP: HashMap<&'static str, ModuleFunction> = {
|
static ref MODULE_MAP: HashMap<&'static str, ModuleFunction> = {
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
m.insert("counter", Counter::run as ModuleFunction);
|
m.insert("counter", Counter::run as ModuleFunction);
|
||||||
|
m.insert("space", Space::run as ModuleFunction);
|
||||||
m
|
m
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -103,9 +113,52 @@ impl DeviceAccess {
|
||||||
self.format().size
|
self.format().size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kind(&self) -> Kind {
|
/// Draw an image with text
|
||||||
self.kind
|
///
|
||||||
|
/// 60% image
|
||||||
|
/// 40% text
|
||||||
|
pub fn image_with_text(&self, image: Vec<u8>, text: String, config: &Button) -> DynamicImage {
|
||||||
|
let res = self.resolution();
|
||||||
|
let mut image = RgbImage::new(res.0 as u32, res.1 as u32);
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
-10,
|
||||||
|
10,
|
||||||
|
Scale::uniform(parse_config(config, &"FONT_SIZE".into(), 20.0)),
|
||||||
|
&GLOBAL_FONT.get().unwrap(),
|
||||||
|
&text,
|
||||||
|
);
|
||||||
|
image::DynamicImage::ImageRgb8(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw text
|
||||||
|
pub fn text(&self, text: String, config: &Button) -> DynamicImage {
|
||||||
|
let res = self.resolution();
|
||||||
|
let mut image = RgbImage::new(res.0 as u32, res.1 as u32);
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
Scale::uniform(parse_config(config, &"FONT_SIZE".into(), 20.0)),
|
||||||
|
&GLOBAL_FONT.get().unwrap(),
|
||||||
|
&text,
|
||||||
|
);
|
||||||
|
image::DynamicImage::ImageRgb8(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// reads a key from the config and parses the config in the given type
|
||||||
|
fn parse_config<T>(config: &Button, key: &String, if_wrong_type: T) -> T
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
{
|
||||||
|
let out = match config.options.get(key) {
|
||||||
|
Some(value) => value.parse::<T>().unwrap_or(if_wrong_type),
|
||||||
|
None => if_wrong_type,
|
||||||
|
};
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ReturnError = Box<dyn Error + Send + Sync>;
|
pub type ReturnError = Box<dyn Error + Send + Sync>;
|
||||||
|
|
|
@ -6,9 +6,6 @@ use super::Module;
|
||||||
use super::{ChannelReceiver, DeviceAccess, HostEvent, ReturnError};
|
use super::{ChannelReceiver, DeviceAccess, HostEvent, ReturnError};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use image::{Rgb, RgbImage};
|
|
||||||
use imageproc::drawing::draw_text_mut;
|
|
||||||
use rusttype::{Font, Scale};
|
|
||||||
|
|
||||||
/// A module which displays a counter
|
/// A module which displays a counter
|
||||||
pub struct Counter;
|
pub struct Counter;
|
||||||
|
@ -18,35 +15,25 @@ impl Module for Counter {
|
||||||
async fn run(
|
async fn run(
|
||||||
streamdeck: DeviceAccess,
|
streamdeck: DeviceAccess,
|
||||||
button_receiver: ChannelReceiver,
|
button_receiver: ChannelReceiver,
|
||||||
_config: Arc<Button>,
|
config: Arc<Button>,
|
||||||
) -> Result<(), ReturnError> {
|
) -> Result<(), ReturnError> {
|
||||||
let mut button_receiver = button_receiver;
|
let mut button_receiver = button_receiver;
|
||||||
|
|
||||||
let font_data: &[u8] = include_bytes!("../../fonts/SpaceGrotesk.ttf");
|
streamdeck.clear_img().await.unwrap();
|
||||||
let font: Font<'static> = Font::try_from_bytes(font_data).unwrap();
|
|
||||||
|
|
||||||
let (h, w) = streamdeck.resolution();
|
|
||||||
|
|
||||||
let mut counter: u32 = 0;
|
let mut counter: u32 = 0;
|
||||||
|
|
||||||
|
// render the 0 at the beginning
|
||||||
|
let image = streamdeck.text(counter.to_string(), &config);
|
||||||
|
streamdeck.write_img(image).await.unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(event) = button_receiver.recv().await {
|
if let Some(event) = button_receiver.recv().await {
|
||||||
match event {
|
match event {
|
||||||
HostEvent::ButtonPressed => {
|
HostEvent::ButtonPressed => {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
let mut image = RgbImage::new(h as u32, w as u32);
|
let image = streamdeck.text(counter.to_string(), &config);
|
||||||
draw_text_mut(
|
streamdeck.write_img(image).await.unwrap();
|
||||||
&mut image,
|
|
||||||
Rgb([255, 255, 255]),
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
Scale::uniform(20.0),
|
|
||||||
&font,
|
|
||||||
format!("{}", counter).as_str(),
|
|
||||||
);
|
|
||||||
streamdeck
|
|
||||||
.write_img(image::DynamicImage::ImageRgb8(image))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
21
src/modules/space.rs
Normal file
21
src/modules/space.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use super::Button;
|
||||||
|
use super::ChannelReceiver;
|
||||||
|
use super::DeviceAccess;
|
||||||
|
use super::Module;
|
||||||
|
use super::ReturnError;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// module to represent the switching of a space (just visual)
|
||||||
|
pub struct Space;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Module for Space {
|
||||||
|
async fn run(
|
||||||
|
_streamdeck: DeviceAccess,
|
||||||
|
_button_receiver: ChannelReceiver,
|
||||||
|
_config: Arc<Button>,
|
||||||
|
) -> Result<(), ReturnError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue