system fonts, spaces

This commit is contained in:
Fl1tzi 2023-07-24 20:43:52 +02:00
parent 69da819aa5
commit 5ddff29c87
No known key found for this signature in database
GPG key ID: 06B333727810C686
7 changed files with 201 additions and 64 deletions

View file

@ -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"

View file

@ -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") {

View file

@ -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),

View file

@ -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(
} }
} }
} }

View file

@ -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>;

View file

@ -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
View 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(())
}
}