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-subscriber = { version = "0.3", features = ["env-filter"] }
lazy_static = "1.4.0"
font-loader = "0.11.0"

View file

@ -1,30 +1,40 @@
use serde::Deserialize;
use dirs::config_dir;
use serde::Deserialize;
use serde_json;
use std::{
collections::HashMap,
env,
fmt::{self, Display},
fs,
hash::Hash,
io::ErrorKind,
path::PathBuf,
collections::HashMap,
sync::Arc
sync::Arc,
};
use tracing::debug;
use serde_json;
/// The name of the folder which holds the config
pub const CONFIG_FOLDER_NAME: &'static str = "microdeck";
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)]
pub struct Config {
pub global: Option<GlobalConfig>,
pub devices: Vec<DeviceConfig>,
pub spaces: Arc<HashMap<String, Space>>,
}
/// settings that effect all devices
#[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)]
pub struct DeviceConfig {
pub serial: String,
@ -42,13 +52,18 @@ pub struct Button {
pub index: u8,
pub module: String,
/// options which get passed to the module
pub options: Option<HashMap<String, String>>,
/// allows to overwrite what it will do on a click (executes in /bin/sh)
#[serde(default = "new_hashmap")]
pub options: HashMap<String, String>,
/// allows to overwrite what it will do on a click
pub on_click: Option<String>,
/// allows to overwrite what it will do on a release
pub on_release: Option<String>,
}
fn new_hashmap() -> HashMap<String, String> {
HashMap::new()
}
#[tracing::instrument]
pub fn load_config() -> Result<Config, ConfigError> {
let config_file: PathBuf = match env::var_os("DACH_DECKER_CONFIG") {

View file

@ -1,5 +1,5 @@
use crate::{
config::{Button, ConfigError, DeviceConfig},
config::{Button, ConfigError, DeviceConfig, Space},
modules::{retrieve_module_from_name, start_module, HostEvent},
unwrap_or_error,
};
@ -41,6 +41,7 @@ pub struct Device {
device: Arc<AsyncStreamDeck>,
modules_runtime: Option<Runtime>,
config: DeviceConfig,
spaces: Arc<HashMap<String, Space>>,
serial: String,
}
@ -49,6 +50,7 @@ impl Device {
serial: String,
kind: Kind,
device_conf: DeviceConfig,
spaces: Arc<HashMap<String, Space>>,
hid: &HidApi,
) -> Result<Device, DeviceError> {
// connect to deck or continue to next
@ -78,6 +80,7 @@ impl Device {
device: deck,
modules_runtime: None,
config: device_conf,
spaces,
serial,
})
}
@ -108,16 +111,16 @@ impl Device {
let dev = self.device.clone();
let b = btn.clone();
runtime.spawn(async move {
start_module(ser, b, module, dev, module_receiver).await
});
runtime
.spawn(async move { start_module(ser, b, module, dev, module_receiver).await });
}
// 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.
if module_sender.is_closed() {
self.modules.insert(btn.index, (btn.clone(), None));
} else {
self.modules.insert(btn.index, (btn.clone(), Some(module_sender)));
self.modules
.insert(btn.index, (btn.clone(), Some(module_sender)));
}
return Ok(());
} else {
@ -137,11 +140,7 @@ impl Device {
if let Some(handle) = self.modules_runtime.take() {
handle.shutdown_background();
}
}
/// if there is no runtime left or the runtime never got initialized
pub fn is_dropped(&self) -> bool {
self.modules_runtime.is_none()
self.modules = HashMap::new();
}
/// 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)
async fn button_state_update(&mut self, event: ButtonStateUpdate) {
// get the index out of the enum...
let index = match event {
ButtonStateUpdate::ButtonUp(i) => i,
ButtonStateUpdate::ButtonDown(i) => i
ButtonStateUpdate::ButtonDown(i) => i,
};
// try to get config for the module
let options = match self.modules.get_mut(&index) {
Some(options) => options,
None => return
None => return,
};
// action will only be some if on_click/on_release is specified in config
let (action, event) = match event {
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
if let Some(sender) = options.to_owned().1 {
@ -200,10 +213,17 @@ impl Device {
if let Some(action) = action {
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) {
match Command::new("sh").arg(command).output().await {
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 crate::config::{Config, DeviceConfig};
use device::Device;
use font_loader::system_fonts::{FontProperty, FontPropertyBuilder};
use hidapi::HidApi;
use std::{collections::HashMap, process::exit, time::Duration};
use tracing::{debug, error, info, warn};
use rusttype::Font;
use std::{
collections::HashMap,
process::exit,
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use tracing::{debug, error, info, trace, warn};
use tracing_subscriber::{
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
};
use config::load_config;
use config::{load_config, Space};
mod config;
mod device;
mod modules;
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
#[macro_export]
macro_rules! skip_if_none {
($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);
let hid = streamdeck::new_hidapi().expect("Could not create HidApi");
@ -64,6 +96,12 @@ fn main() {
.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) {
let mut devices: HashMap<String, Device> = HashMap::new();
@ -72,7 +110,7 @@ pub async fn start(config: Config, mut hid: HidApi) {
loop {
// check for devices that can be removed
let mut removable_devices = Vec::new();
/* let mut removable_devices = Vec::new();
for (key, device) in &devices {
if device.is_dropped() {
removable_devices.push(key.to_owned());
@ -80,7 +118,7 @@ pub async fn start(config: Config, mut hid: HidApi) {
}
for d in removable_devices {
devices.remove(&d);
}
}*/
// refresh device list
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 !ignore_devices.contains(&hw_device.1) && devices.get(&hw_device.1).is_none() {
debug!("New device detected: {}", &hw_device.1);
if let Some(device_config) =
config.devices.iter().find(|d| d.serial == hw_device.1)
// match regex for device serial
if let Some(device_config) = config
.devices
.iter()
.find(|d| d.serial == hw_device.1 || d.serial == "*")
{
// start the device and its listener
if let Some(device) =
start_device(hw_device, &hid, device_config.clone()).await
if let Some(device) = start_device(
hw_device,
&hid,
device_config.clone(),
config.spaces.clone(),
)
.await
{
devices.insert(device.serial(), device);
}
@ -117,16 +163,11 @@ pub async fn start_device(
device: (streamdeck::info::Kind, String),
hid: &HidApi,
device_config: DeviceConfig,
spaces: Arc<HashMap<String, Space>>,
) -> 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) => {
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.key_listener().await;
if !device.has_modules() {
@ -140,4 +181,3 @@ pub async fn start_device(
}
}
}

View file

@ -1,15 +1,24 @@
mod blank;
mod counter;
mod space;
// modules
use self::counter::Counter;
use self::space::Space;
use crate::GLOBAL_FONT;
// other things
use crate::config::Button;
use async_trait::async_trait;
pub use deck_driver as streamdeck;
use futures_util::Future;
use image::DynamicImage;
use image::{DynamicImage, Rgb, RgbImage};
use imageproc::drawing::draw_text_mut;
use lazy_static::lazy_static;
use rusttype::Scale;
use std::collections::HashMap;
use std::pin::Pin;
use std::str::FromStr;
use std::{error::Error, sync::Arc};
pub use streamdeck::info::ImageFormat;
use streamdeck::info::Kind;
@ -22,6 +31,7 @@ lazy_static! {
static ref MODULE_MAP: HashMap<&'static str, ModuleFunction> = {
let mut m = HashMap::new();
m.insert("counter", Counter::run as ModuleFunction);
m.insert("space", Space::run as ModuleFunction);
m
};
}
@ -103,9 +113,52 @@ impl DeviceAccess {
self.format().size
}
pub fn kind(&self) -> Kind {
self.kind
/// Draw an image with text
///
/// 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>;

View file

@ -6,9 +6,6 @@ use super::Module;
use super::{ChannelReceiver, DeviceAccess, HostEvent, ReturnError};
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
pub struct Counter;
@ -18,35 +15,25 @@ impl Module for Counter {
async fn run(
streamdeck: DeviceAccess,
button_receiver: ChannelReceiver,
_config: Arc<Button>,
config: Arc<Button>,
) -> Result<(), ReturnError> {
let mut button_receiver = button_receiver;
let font_data: &[u8] = include_bytes!("../../fonts/SpaceGrotesk.ttf");
let font: Font<'static> = Font::try_from_bytes(font_data).unwrap();
let (h, w) = streamdeck.resolution();
streamdeck.clear_img().await.unwrap();
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 {
if let Some(event) = button_receiver.recv().await {
match event {
HostEvent::ButtonPressed => {
counter += 1;
let mut image = RgbImage::new(h as u32, w as u32);
draw_text_mut(
&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();
let image = streamdeck.text(counter.to_string(), &config);
streamdeck.write_img(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(())
}
}