mirror of
https://codeberg.org/Fl1tzi/microdeck.git
synced 2024-05-08 14:40:44 +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-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
lazy_static = "1.4.0"
|
||||
font-loader = "0.11.0"
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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),
|
||||
|
|
76
src/main.rs
76
src/main.rs
|
@ -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(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
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