mirror of
https://codeberg.org/Fl1tzi/microdeck.git
synced 2024-05-19 19:20:20 +00:00
changed way on how to program modules and refactored other things
This commit is contained in:
parent
b26a8834c6
commit
26b4ad0344
100
src/config.rs
100
src/config.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::type_definition::PrettyPrint;
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -6,9 +7,9 @@ use std::{
|
||||||
env,
|
env,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
fs,
|
fs,
|
||||||
hash::Hash,
|
|
||||||
io::ErrorKind,
|
io::ErrorKind,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
@ -20,6 +21,10 @@ pub const CONFIG_FILE: &'static str = "config.json";
|
||||||
/// Combination of buttons acting as a folder which a device can switch to
|
/// Combination of buttons acting as a folder which a device can switch to
|
||||||
pub type Space = Vec<Arc<Button>>;
|
pub type Space = Vec<Arc<Button>>;
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONFIGURATION DEFINITION
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/// CONFIGURATION
|
/// CONFIGURATION
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -64,6 +69,94 @@ fn new_hashmap() -> HashMap<String, String> {
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// BUTTON CONFIGURATION ERRORS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ButtonConfigError {
|
||||||
|
/// (Key, Expected)
|
||||||
|
WrongType(String, &'static str),
|
||||||
|
/// A general error which gets directly outputed to the user
|
||||||
|
General(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ButtonConfigError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ButtonConfigError::WrongType(key, expected) => {
|
||||||
|
write!(
|
||||||
|
formatter,
|
||||||
|
"Expected value of type {expected} in option \"{key}\"."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ButtonConfigError::General(message) => {
|
||||||
|
write!(formatter, "{message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [parse_button_config()]
|
||||||
|
pub enum ParseButtonConfigResult<T: FromStr> {
|
||||||
|
Found(T),
|
||||||
|
NotFound(T),
|
||||||
|
ParseError(ButtonConfigError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FromStr> ParseButtonConfigResult<T> {
|
||||||
|
/// instead of the enum return Result<Value, ButtonConfigError>.
|
||||||
|
///
|
||||||
|
/// [ParseButtonConfigResult::Found] || [ParseButtonConfigResult::NotFound] => Ok(value)
|
||||||
|
/// [ParseButtonConfigResult::ParseError] => Err(e)
|
||||||
|
pub fn res(self) -> Result<T, ButtonConfigError> {
|
||||||
|
match self {
|
||||||
|
ParseButtonConfigResult::Found(v) | ParseButtonConfigResult::NotFound(v) => Ok(v),
|
||||||
|
ParseButtonConfigResult::ParseError(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Button {
|
||||||
|
/// reads a key from the config and parses the config in the given type
|
||||||
|
///
|
||||||
|
/// # Return
|
||||||
|
///
|
||||||
|
/// - ParseButtonConfigResult::Found(value) -> returns the value found in the configuration; value is
|
||||||
|
/// parsing result
|
||||||
|
/// - ParseButtonConfigResult::NotFound(value) -> the key was not found in the configuration; value
|
||||||
|
/// is 'if_wrong_type'
|
||||||
|
/// - ParseButtonConfigResult::ParseError(error) -> the value could not be parsed; error is
|
||||||
|
/// [ButtonConfigError::WrongType]
|
||||||
|
pub fn parse_module<T>(&self, key: &'static str, if_wrong_type: T) -> ParseButtonConfigResult<T>
|
||||||
|
where
|
||||||
|
T: PrettyPrint + FromStr,
|
||||||
|
{
|
||||||
|
// try to find value or return None
|
||||||
|
let parse_result = match self.options.get(key) {
|
||||||
|
Some(value) => value.parse::<T>(),
|
||||||
|
_ => return ParseButtonConfigResult::NotFound(if_wrong_type),
|
||||||
|
};
|
||||||
|
// check if value could be parsed
|
||||||
|
if let Ok(out) = parse_result {
|
||||||
|
return ParseButtonConfigResult::Found(out);
|
||||||
|
}
|
||||||
|
ParseButtonConfigResult::ParseError(ButtonConfigError::WrongType(
|
||||||
|
key.to_string(),
|
||||||
|
if_wrong_type.pprint(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Just retrieve the raw string from a key without trying to parse the value
|
||||||
|
pub fn raw_module(&self, key: &String) -> Option<&String> {
|
||||||
|
self.options.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PARSING CONFIG
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#[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") {
|
||||||
|
@ -100,6 +193,11 @@ pub fn load_config() -> Result<Config, ConfigError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// OVERALL CONFIGURATION ERRORS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// General error for parsing the configuration
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
ButtonDoesNotExist(u8),
|
ButtonDoesNotExist(u8),
|
||||||
|
|
|
@ -101,12 +101,15 @@ impl Device {
|
||||||
}
|
}
|
||||||
// TODO: DO THIS WITHOUT CLONING! Currently takes up a big amount of memory.
|
// TODO: DO THIS WITHOUT CLONING! Currently takes up a big amount of memory.
|
||||||
let button_config = match &self.selected_space {
|
let button_config = match &self.selected_space {
|
||||||
Some(s) => self.spaces.get(s).unwrap_or_else(|| {
|
Some(s) => self
|
||||||
warn!("The space \"{}\" was not found", s);
|
.spaces
|
||||||
&self.config.buttons
|
.get(s)
|
||||||
}
|
.unwrap_or_else(|| {
|
||||||
).to_owned(),
|
warn!("The space \"{}\" was not found", s);
|
||||||
None => self.config.buttons.to_owned()
|
&self.config.buttons
|
||||||
|
})
|
||||||
|
.to_owned(),
|
||||||
|
None => self.config.buttons.to_owned(),
|
||||||
};
|
};
|
||||||
for i in 0..button_config.len() {
|
for i in 0..button_config.len() {
|
||||||
let button = button_config.get(i).unwrap().to_owned();
|
let button = button_config.get(i).unwrap().to_owned();
|
||||||
|
|
252
src/image_rendering.rs
Normal file
252
src/image_rendering.rs
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
// multiple functions for rendering various images/buttons
|
||||||
|
//
|
||||||
|
use crate::GLOBAL_FONT;
|
||||||
|
use image::imageops;
|
||||||
|
use image::{io::Reader, DynamicImage, ImageBuffer, Rgb, RgbImage};
|
||||||
|
use imageproc::drawing::draw_text_mut;
|
||||||
|
use rusttype::Scale;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
/// Loads an image from a path
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn load_image(path: String) -> io::Result<DynamicImage> {
|
||||||
|
Ok(Reader::open(path)?
|
||||||
|
.decode()
|
||||||
|
.expect("Unable to decode image"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A red image which should represent an missing image or error
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn create_error_image() -> DynamicImage {
|
||||||
|
let mut error_img: RgbImage = ImageBuffer::new(1, 1);
|
||||||
|
|
||||||
|
for pixel in error_img.enumerate_pixels_mut() {
|
||||||
|
*pixel.2 = image::Rgb([240, 128, 128]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicImage::ImageRgb8(error_img)
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Component {
|
||||||
|
fn render(&self) -> DynamicImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ImageBuilder is an easy way to build images.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// ImageBuilder::new(20, 20)
|
||||||
|
/// .set_text("This is a test")
|
||||||
|
/// .build();
|
||||||
|
/// ```
|
||||||
|
pub struct ImageBuilder {
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
scale: f32,
|
||||||
|
font_size: f32,
|
||||||
|
text: Option<String>,
|
||||||
|
image: Option<DynamicImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImageBuilder {
|
||||||
|
fn default() -> ImageBuilder {
|
||||||
|
ImageBuilder {
|
||||||
|
// will get changed
|
||||||
|
height: 0,
|
||||||
|
// will get changed
|
||||||
|
width: 0,
|
||||||
|
scale: 60.0,
|
||||||
|
font_size: 15.0,
|
||||||
|
text: None,
|
||||||
|
image: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageBuilder {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new(height: usize, width: usize) -> Self {
|
||||||
|
ImageBuilder {
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_image_scale(mut self, scale: f32) -> Self {
|
||||||
|
self.scale = scale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_text(mut self, text: String) -> Self {
|
||||||
|
self.text = Some(text);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_font_size(mut self, font_size: f32) -> Self {
|
||||||
|
self.font_size = font_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_image(mut self, image: DynamicImage) -> Self {
|
||||||
|
self.image = Some(image);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn build(self) -> DynamicImage {
|
||||||
|
// cannot use "if let" here, because variables would be moved
|
||||||
|
if self.text.is_some() && self.image.is_some() {
|
||||||
|
let c = ImageTextComponent {
|
||||||
|
height: self.height,
|
||||||
|
width: self.width,
|
||||||
|
image: self.image.unwrap(),
|
||||||
|
scale: self.scale,
|
||||||
|
font_size: self.font_size,
|
||||||
|
text: self.text.unwrap(),
|
||||||
|
};
|
||||||
|
return c.render();
|
||||||
|
} else if let Some(text) = self.text {
|
||||||
|
let c = TextComponent {
|
||||||
|
height: self.height,
|
||||||
|
width: self.width,
|
||||||
|
font_size: self.font_size,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
return c.render();
|
||||||
|
} else if let Some(image) = self.image {
|
||||||
|
let c = ImageComponent {
|
||||||
|
height: self.height,
|
||||||
|
width: self.width,
|
||||||
|
scale: self.scale,
|
||||||
|
image,
|
||||||
|
};
|
||||||
|
return c.render();
|
||||||
|
} else {
|
||||||
|
return create_error_image();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component that just displays an image
|
||||||
|
struct ImageComponent {
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
scale: f32,
|
||||||
|
image: DynamicImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ImageComponent {
|
||||||
|
fn render(&self) -> DynamicImage {
|
||||||
|
let new_h = (self.height as f32 * (self.scale * 0.01)) as u32;
|
||||||
|
let new_w = (self.width as f32 * (self.scale * 0.01)) as u32;
|
||||||
|
|
||||||
|
let image = self
|
||||||
|
.image
|
||||||
|
.resize_to_fill(new_w, new_h, image::imageops::FilterType::Nearest);
|
||||||
|
|
||||||
|
let mut base_image = RgbImage::new(self.height as u32, self.width as u32);
|
||||||
|
|
||||||
|
let free_x = self.width - image.width() as usize;
|
||||||
|
let free_y = self.height - image.height() as usize;
|
||||||
|
imageops::overlay(
|
||||||
|
&mut base_image,
|
||||||
|
&image.to_rgb8(),
|
||||||
|
(free_x / 2) as i64,
|
||||||
|
(free_y / 2) as i64,
|
||||||
|
);
|
||||||
|
|
||||||
|
image::DynamicImage::ImageRgb8(base_image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component that just displays text
|
||||||
|
struct TextComponent {
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
font_size: f32,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TextComponent {
|
||||||
|
fn render(&self) -> DynamicImage {
|
||||||
|
let mut image = RgbImage::new(self.width as u32, self.height as u32);
|
||||||
|
|
||||||
|
let scale = Scale::uniform(self.font_size);
|
||||||
|
let font = &GLOBAL_FONT.get().unwrap();
|
||||||
|
|
||||||
|
let v_metrics = font.v_metrics(scale);
|
||||||
|
let height = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap).round() as i32;
|
||||||
|
|
||||||
|
// start at y = 10
|
||||||
|
let mut y_pos = 10;
|
||||||
|
|
||||||
|
for line in self.text.split("\n") {
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
10,
|
||||||
|
y_pos,
|
||||||
|
scale,
|
||||||
|
&GLOBAL_FONT.get().unwrap(),
|
||||||
|
&line,
|
||||||
|
);
|
||||||
|
y_pos += height;
|
||||||
|
}
|
||||||
|
|
||||||
|
image::DynamicImage::ImageRgb8(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component that displays image and text
|
||||||
|
struct ImageTextComponent {
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
image: DynamicImage,
|
||||||
|
scale: f32,
|
||||||
|
font_size: f32,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ImageTextComponent {
|
||||||
|
fn render(&self) -> DynamicImage {
|
||||||
|
let new_h = (self.height as f32 * (self.scale * 0.01)) as u32;
|
||||||
|
let new_w = (self.width as f32 * (self.scale * 0.01)) as u32;
|
||||||
|
|
||||||
|
let image = self
|
||||||
|
.image
|
||||||
|
.resize_to_fill(new_w, new_h, image::imageops::FilterType::Nearest);
|
||||||
|
|
||||||
|
let mut base_image = RgbImage::new(self.height as u32, self.width as u32);
|
||||||
|
|
||||||
|
let font = &GLOBAL_FONT.get().unwrap();
|
||||||
|
let font_scale = Scale::uniform(self.font_size);
|
||||||
|
|
||||||
|
// TODO: allow new line
|
||||||
|
draw_text_mut(
|
||||||
|
&mut base_image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
font_scale,
|
||||||
|
font,
|
||||||
|
&self.text,
|
||||||
|
);
|
||||||
|
// position at the middle
|
||||||
|
let free_space = self.width - image.width() as usize;
|
||||||
|
// TODO: allow padding to be manually set
|
||||||
|
imageops::overlay(
|
||||||
|
&mut base_image,
|
||||||
|
&image.to_rgb8(),
|
||||||
|
(free_space / 2) as i64,
|
||||||
|
self.height as i64 - image.height() as i64 - 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
image::DynamicImage::ImageRgb8(base_image)
|
||||||
|
}
|
||||||
|
}
|
21
src/main.rs
21
src/main.rs
|
@ -1,16 +1,16 @@
|
||||||
use crate::config::{Config, DeviceConfig, GlobalConfig};
|
use crate::config::{Config, DeviceConfig};
|
||||||
use deck_driver as streamdeck;
|
use deck_driver as streamdeck;
|
||||||
use device::Device;
|
use device::Device;
|
||||||
use font_loader::system_fonts::{FontProperty, FontPropertyBuilder};
|
use font_loader::system_fonts::FontPropertyBuilder;
|
||||||
use hidapi::HidApi;
|
use hidapi::HidApi;
|
||||||
use rusttype::Font;
|
use rusttype::Font;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
process::exit,
|
process::exit,
|
||||||
sync::{Arc, Mutex, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
|
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter,
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,9 @@ use config::{load_config, Space};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod device;
|
mod device;
|
||||||
|
mod image_rendering;
|
||||||
mod modules;
|
mod modules;
|
||||||
|
mod type_definition;
|
||||||
|
|
||||||
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
|
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
|
||||||
|
|
||||||
|
@ -109,17 +111,6 @@ pub async fn start(config: Config, mut hid: HidApi) {
|
||||||
let mut ignore_devices: Vec<String> = Vec::new();
|
let mut ignore_devices: Vec<String> = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// check for devices that can be removed
|
|
||||||
/* let mut removable_devices = Vec::new();
|
|
||||||
for (key, device) in &devices {
|
|
||||||
if device.is_dropped() {
|
|
||||||
removable_devices.push(key.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for d in removable_devices {
|
|
||||||
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) {
|
||||||
warn!("Cannot fetch new devices: {}", e);
|
warn!("Cannot fetch new devices: {}", e);
|
||||||
|
|
171
src/modules.rs
171
src/modules.rs
|
@ -7,38 +7,18 @@ use self::counter::Counter;
|
||||||
use self::space::Space;
|
use self::space::Space;
|
||||||
|
|
||||||
// other things
|
// other things
|
||||||
use crate::GLOBAL_FONT;
|
use crate::config::{Button, ButtonConfigError};
|
||||||
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::imageops::{resize, self};
|
use image::DynamicImage;
|
||||||
use image::io::Reader;
|
use std::{error::Error, pin::Pin, sync::Arc};
|
||||||
use image::{DynamicImage, Rgb, RgbImage, ImageBuffer};
|
use streamdeck::info::ImageFormat;
|
||||||
use imageproc::drawing::draw_text_mut;
|
|
||||||
use imageproc::filter;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use rusttype::Scale;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::{BufReader, self};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{error::Error, sync::Arc};
|
|
||||||
pub use streamdeck::info::ImageFormat;
|
|
||||||
use streamdeck::info::Kind;
|
use streamdeck::info::Kind;
|
||||||
use streamdeck::AsyncStreamDeck;
|
use streamdeck::AsyncStreamDeck;
|
||||||
pub use streamdeck::StreamDeckError;
|
use streamdeck::StreamDeckError;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, error, info, trace};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Events that are coming from the host
|
/// Events that are coming from the host
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
@ -49,11 +29,16 @@ pub enum HostEvent {
|
||||||
ButtonReleased,
|
ButtonReleased,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ModuleFuture = Pin<Box<dyn Future<Output = Result<(), ReturnError>> + Send>>;
|
pub type ModuleFuture =
|
||||||
pub type ModuleFunction = fn(DeviceAccess, ChannelReceiver, Arc<Button>) -> ModuleFuture;
|
Pin<Box<dyn Future<Output = Result<Box<dyn Module + Sync + Send>, ButtonConfigError>> + Send>>;
|
||||||
|
pub type ModuleInitFunction = fn(Arc<Button>) -> ModuleFuture;
|
||||||
|
|
||||||
pub fn retrieve_module_from_name(name: &str) -> Option<ModuleFunction> {
|
pub fn retrieve_module_from_name(name: &str) -> Option<ModuleInitFunction> {
|
||||||
MODULE_MAP.get(name).copied()
|
match name {
|
||||||
|
"space" => Some(Space::init as ModuleInitFunction),
|
||||||
|
"counter" => Some(Counter::init as ModuleInitFunction),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// starts a module
|
/// starts a module
|
||||||
|
@ -62,17 +47,25 @@ pub async fn start_module(
|
||||||
// Just for logging purpose
|
// Just for logging purpose
|
||||||
serial: String,
|
serial: String,
|
||||||
button: Arc<Button>,
|
button: Arc<Button>,
|
||||||
module_function: ModuleFunction,
|
module_init_function: ModuleInitFunction,
|
||||||
device: Arc<AsyncStreamDeck>,
|
device: Arc<AsyncStreamDeck>,
|
||||||
br: ChannelReceiver,
|
br: ChannelReceiver,
|
||||||
) {
|
) {
|
||||||
debug!("STARTED");
|
debug!("STARTED");
|
||||||
let da = DeviceAccess::new(device, button.index).await;
|
let da = DeviceAccess::new(device, button.index).await;
|
||||||
|
|
||||||
// actually run the module
|
// run init first
|
||||||
match module_function(da, br, button).await {
|
//
|
||||||
Ok(_) => info!("CLOSED"),
|
// panic should be prevented by the config being checked before running
|
||||||
Err(e) => error!("ERR: {:?}", e),
|
let mut module = match module_init_function(button).await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => panic!("{}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// then run module
|
||||||
|
match module.run(da, br).await {
|
||||||
|
Ok(_) => debug!("RETURNED"),
|
||||||
|
Err(e) => error!("RETURNED_ERROR: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +77,7 @@ pub struct DeviceAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceAccess {
|
impl DeviceAccess {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn new(streamdeck: Arc<AsyncStreamDeck>, index: u8) -> DeviceAccess {
|
pub async fn new(streamdeck: Arc<AsyncStreamDeck>, index: u8) -> DeviceAccess {
|
||||||
let kind = streamdeck.kind();
|
let kind = streamdeck.kind();
|
||||||
DeviceAccess {
|
DeviceAccess {
|
||||||
|
@ -94,128 +88,51 @@ impl DeviceAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// write a raw image to the Deck.
|
/// write a raw image to the Deck.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn write_raw_img(&self, img: &[u8]) -> Result<(), StreamDeckError> {
|
pub async fn write_raw_img(&self, img: &[u8]) -> Result<(), StreamDeckError> {
|
||||||
self.streamdeck.write_image(self.index, img).await
|
self.streamdeck.write_image(self.index, img).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write an image to the Deck.
|
/// Write an image to the Deck.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn write_img(&self, img: DynamicImage) -> Result<(), StreamDeckError> {
|
pub async fn write_img(&self, img: DynamicImage) -> Result<(), StreamDeckError> {
|
||||||
self.streamdeck.set_button_image(self.index, img).await
|
self.streamdeck.set_button_image(self.index, img).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reset the image.
|
/// reset the image.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn clear_img(&self) -> Result<(), StreamDeckError> {
|
pub async fn clear_img(&self) -> Result<(), StreamDeckError> {
|
||||||
self.streamdeck.clear_button_image(self.index).await
|
self.streamdeck.clear_button_image(self.index).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn format(&self) -> ImageFormat {
|
pub fn format(&self) -> ImageFormat {
|
||||||
self.kind.key_image_format()
|
self.kind.key_image_format()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The resolution of the image on the Deck.
|
/// The resolution of the image on the Deck.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn resolution(&self) -> (usize, usize) {
|
pub fn resolution(&self) -> (usize, usize) {
|
||||||
self.format().size
|
self.format().size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw an image with text
|
|
||||||
#[tracing::instrument(skip_all, fields(index = config.index))]
|
|
||||||
pub fn image_with_text(&self, image: DynamicImage, text: String, config: &Button) -> DynamicImage {
|
|
||||||
trace!("Render start");
|
|
||||||
let (w, h) = self.resolution();
|
|
||||||
|
|
||||||
let image_scaling = parse_config(&config, &"IMAGE_SCALE".into(), 65.0);
|
|
||||||
|
|
||||||
// TODO: lots of parsing. This can probbably be improved.
|
|
||||||
let new_h = (h as f32 * (image_scaling * 0.01)) as u32;
|
|
||||||
let new_w = (w as f32 * (image_scaling * 0.01)) as u32;
|
|
||||||
|
|
||||||
// Calculate percentage of which we can scale down to the button resolution.
|
|
||||||
// By taking the smallest it keeps the aspect ratio.
|
|
||||||
// let percentage = f32::min(deck_w / image.width() as f32, deck_h / image.height() as f32);
|
|
||||||
|
|
||||||
let image = image.resize_to_fill(new_w, new_h, image::imageops::FilterType::Nearest);
|
|
||||||
|
|
||||||
let mut base_image = RgbImage::new(h as u32, w as u32);
|
|
||||||
draw_text_mut(
|
|
||||||
&mut base_image,
|
|
||||||
Rgb([255, 255, 255]),
|
|
||||||
0,
|
|
||||||
h as i32 - 20,
|
|
||||||
Scale::uniform(parse_config(config, &"FONT_SIZE".into(), 15.0)),
|
|
||||||
&GLOBAL_FONT.get().unwrap(),
|
|
||||||
&text,
|
|
||||||
);
|
|
||||||
// position at the middle
|
|
||||||
let free_space = w - image.width() as usize;
|
|
||||||
imageops::overlay(&mut base_image, &image.to_rgb8(), (free_space/2) as i64, 0);
|
|
||||||
trace!("Render end");
|
|
||||||
image::DynamicImage::ImageRgb8(base_image)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw text
|
|
||||||
#[tracing::instrument(skip_all, fields(index = config.index))]
|
|
||||||
pub fn text(&self, text: String, config: &Button) -> DynamicImage {
|
|
||||||
trace!("Render start");
|
|
||||||
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(), 15.0)),
|
|
||||||
&GLOBAL_FONT.get().unwrap(),
|
|
||||||
&text,
|
|
||||||
);
|
|
||||||
trace!("Render end");
|
|
||||||
image::DynamicImage::ImageRgb8(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the image from the `IMAGE` option.
|
|
||||||
/// Displays [create_error_image] if it does not exist or cannot be loeded.
|
|
||||||
pub fn load_image(config: &Button) -> io::Result<DynamicImage> {
|
|
||||||
// TODO: maybe us an Option (faster?)
|
|
||||||
let file_path = parse_config(config, &"IMAGE".into(), "None".to_string());
|
|
||||||
|
|
||||||
if file_path == "None" {
|
|
||||||
return Ok(create_error_image());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Reader::open(file_path)?.decode().expect("Unable to decode image"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A smooth red image which should represent an empty space
|
|
||||||
pub fn create_error_image() -> DynamicImage {
|
|
||||||
let mut error_img: RgbImage = ImageBuffer::new(1, 1);
|
|
||||||
|
|
||||||
for pixel in error_img.enumerate_pixels_mut() {
|
|
||||||
*pixel.2 = image::Rgb([240, 128, 128]);
|
|
||||||
}
|
|
||||||
|
|
||||||
DynamicImage::ImageRgb8(error_img)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reads a key from the config and parses the config in the given type
|
|
||||||
pub 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>;
|
||||||
pub type ChannelReceiver = mpsc::Receiver<HostEvent>;
|
pub type ChannelReceiver = mpsc::Receiver<HostEvent>;
|
||||||
|
pub type ModuleObject = Box<dyn Module + Send + Sync>;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Module {
|
/// An object safe module trait.
|
||||||
|
///
|
||||||
|
/// - init() -> function for checking config and creating module
|
||||||
|
/// - run() -> function that happens when the device actually runs
|
||||||
|
pub trait Module: Sync + Send {
|
||||||
|
async fn init(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
async fn run(
|
async fn run(
|
||||||
|
&mut self,
|
||||||
device: DeviceAccess,
|
device: DeviceAccess,
|
||||||
receiver: ChannelReceiver,
|
receiver: ChannelReceiver,
|
||||||
config: Arc<Button>,
|
|
||||||
) -> Result<(), ReturnError>;
|
) -> Result<(), ReturnError>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use super::Button;
|
use super::Button;
|
||||||
|
use super::ButtonConfigError;
|
||||||
use super::ChannelReceiver;
|
use super::ChannelReceiver;
|
||||||
use super::DeviceAccess;
|
use super::DeviceAccess;
|
||||||
use super::Module;
|
use super::Module;
|
||||||
|
use super::ModuleObject;
|
||||||
use super::ReturnError;
|
use super::ReturnError;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -10,10 +12,14 @@ pub struct Blank;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Module for Blank {
|
impl Module for Blank {
|
||||||
|
async fn init(_config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||||
|
Ok(Box::new(Blank {}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn run(
|
async fn run(
|
||||||
|
&mut self,
|
||||||
_streamdeck: DeviceAccess,
|
_streamdeck: DeviceAccess,
|
||||||
_button_receiver: ChannelReceiver,
|
_button_receiver: ChannelReceiver,
|
||||||
_config: Arc<Button>,
|
|
||||||
) -> Result<(), ReturnError> {
|
) -> Result<(), ReturnError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,42 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::config::Button;
|
use crate::config::Button;
|
||||||
|
|
||||||
use super::Module;
|
use super::{
|
||||||
use super::{ChannelReceiver, DeviceAccess, HostEvent, ReturnError};
|
ButtonConfigError, ChannelReceiver, DeviceAccess, HostEvent, Module, ModuleObject, ReturnError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::GLOBAL_FONT;
|
||||||
|
use image::{DynamicImage, Rgb, RgbImage};
|
||||||
|
use imageproc::drawing::draw_text_mut;
|
||||||
|
use rusttype::Scale;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
/// A module which displays a counter
|
/// A module which displays a counter
|
||||||
pub struct Counter;
|
pub struct Counter {
|
||||||
|
title: String,
|
||||||
|
title_size: f32,
|
||||||
|
number_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Module for Counter {
|
impl Module for Counter {
|
||||||
|
async fn init(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||||
|
let title = config.parse_module("TITLE", " ".to_string()).res()?;
|
||||||
|
let title_size = config.parse_module("TITLE_SIZE", 15.0).res()?;
|
||||||
|
let number_size = config.parse_module("NUMBER_SIZE", 25.0).res()?;
|
||||||
|
|
||||||
|
Ok(Box::new(Counter {
|
||||||
|
title,
|
||||||
|
title_size,
|
||||||
|
number_size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn run(
|
async fn run(
|
||||||
|
&mut self,
|
||||||
streamdeck: DeviceAccess,
|
streamdeck: DeviceAccess,
|
||||||
button_receiver: ChannelReceiver,
|
button_receiver: ChannelReceiver,
|
||||||
config: Arc<Button>,
|
|
||||||
) -> Result<(), ReturnError> {
|
) -> Result<(), ReturnError> {
|
||||||
let mut button_receiver = button_receiver;
|
let mut button_receiver = button_receiver;
|
||||||
|
|
||||||
|
@ -24,15 +46,28 @@ impl Module for Counter {
|
||||||
let mut counter: u32 = 0;
|
let mut counter: u32 = 0;
|
||||||
|
|
||||||
// render the 0 at the beginning
|
// render the 0 at the beginning
|
||||||
let image = streamdeck.text(counter.to_string(), &config);
|
let image = render_text(
|
||||||
|
&streamdeck,
|
||||||
|
&self.title,
|
||||||
|
&counter.to_string(),
|
||||||
|
self.title_size,
|
||||||
|
self.number_size,
|
||||||
|
);
|
||||||
streamdeck.write_img(image).await.unwrap();
|
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;
|
// just return to zero if u32 MAX is reached
|
||||||
let image = streamdeck.text(counter.to_string(), &config);
|
counter = counter.checked_add(1).unwrap_or(0);
|
||||||
|
let image = render_text(
|
||||||
|
&streamdeck,
|
||||||
|
&self.title,
|
||||||
|
&counter.to_string(),
|
||||||
|
self.title_size,
|
||||||
|
self.number_size,
|
||||||
|
);
|
||||||
streamdeck.write_img(image).await.unwrap();
|
streamdeck.write_img(image).await.unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -41,3 +76,48 @@ impl Module for Counter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_text(
|
||||||
|
streamdeck: &DeviceAccess,
|
||||||
|
title: &String,
|
||||||
|
counter: &String,
|
||||||
|
title_size: f32,
|
||||||
|
number_size: f32,
|
||||||
|
) -> DynamicImage {
|
||||||
|
let res = streamdeck.resolution();
|
||||||
|
let mut image = RgbImage::new(res.0 as u32, res.1 as u32);
|
||||||
|
|
||||||
|
let scale = Scale::uniform(title_size);
|
||||||
|
let font = &GLOBAL_FONT.get().unwrap();
|
||||||
|
|
||||||
|
let v_metrics = font.v_metrics(scale);
|
||||||
|
let height = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap).round() as i32;
|
||||||
|
|
||||||
|
// start at y = 10
|
||||||
|
let mut y_pos = 10;
|
||||||
|
|
||||||
|
for line in title.split("\n") {
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
10,
|
||||||
|
y_pos,
|
||||||
|
Scale::uniform(title_size),
|
||||||
|
&GLOBAL_FONT.get().unwrap(),
|
||||||
|
&line,
|
||||||
|
);
|
||||||
|
y_pos += height;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
Rgb([255, 255, 255]),
|
||||||
|
10,
|
||||||
|
y_pos,
|
||||||
|
Scale::uniform(number_size),
|
||||||
|
&GLOBAL_FONT.get().unwrap(),
|
||||||
|
&counter,
|
||||||
|
);
|
||||||
|
|
||||||
|
image::DynamicImage::ImageRgb8(image)
|
||||||
|
}
|
||||||
|
|
|
@ -1,28 +1,39 @@
|
||||||
use super::Button;
|
use super::Button;
|
||||||
|
use super::ButtonConfigError;
|
||||||
use super::ChannelReceiver;
|
use super::ChannelReceiver;
|
||||||
use super::DeviceAccess;
|
use super::DeviceAccess;
|
||||||
use super::Module;
|
use super::Module;
|
||||||
|
use super::ModuleObject;
|
||||||
use super::ReturnError;
|
use super::ReturnError;
|
||||||
use super::create_error_image;
|
use crate::image_rendering::{create_error_image, ImageBuilder};
|
||||||
use super::load_image;
|
|
||||||
use super::parse_config;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// module to represent the switching of a space (just visual)
|
/// module to represent the switching of a space (just visual)
|
||||||
pub struct Space;
|
pub struct Space {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Module for Space {
|
impl Module for Space {
|
||||||
|
async fn init(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||||
|
let name = config.parse_module("NAME", "Unknown".to_string()).res()?;
|
||||||
|
Ok(Box::new(Space { name }))
|
||||||
|
}
|
||||||
|
|
||||||
async fn run(
|
async fn run(
|
||||||
|
&mut self,
|
||||||
streamdeck: DeviceAccess,
|
streamdeck: DeviceAccess,
|
||||||
_button_receiver: ChannelReceiver,
|
_button_receiver: ChannelReceiver,
|
||||||
config: Arc<Button>,
|
|
||||||
) -> Result<(), ReturnError> {
|
) -> Result<(), ReturnError> {
|
||||||
// let icon = load_image(&config).unwrap();
|
// let icon = load_image(&config).unwrap();
|
||||||
let icon = create_error_image();
|
let icon = create_error_image();
|
||||||
|
|
||||||
let image = streamdeck.image_with_text(icon, parse_config(&config, &"NAME".into(), "Unknown".to_string()), &config);
|
let res = streamdeck.resolution();
|
||||||
|
let image = ImageBuilder::new(res.0, res.1)
|
||||||
|
.set_image(icon)
|
||||||
|
.set_text(self.name.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
streamdeck.write_img(image).await.unwrap();
|
streamdeck.write_img(image).await.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
78
src/type_definition.rs
Normal file
78
src/type_definition.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
macro_rules! impl_string {
|
||||||
|
($T:ident) => {
|
||||||
|
impl PrettyPrint for $T {
|
||||||
|
fn pprint(&self) -> &'static str {
|
||||||
|
"string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_positive_number {
|
||||||
|
($T:ident) => {
|
||||||
|
impl PrettyPrint for $T {
|
||||||
|
fn pprint(&self) -> &'static str {
|
||||||
|
"positive number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_decimal_number {
|
||||||
|
($T:ident) => {
|
||||||
|
impl PrettyPrint for $T {
|
||||||
|
fn pprint(&self) -> &'static str {
|
||||||
|
"decimal number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_number {
|
||||||
|
($T:ident) => {
|
||||||
|
impl PrettyPrint for $T {
|
||||||
|
fn pprint(&self) -> &'static str {
|
||||||
|
"number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_bool {
|
||||||
|
($T:ident) => {
|
||||||
|
impl PrettyPrint for $T {
|
||||||
|
fn pprint(&self) -> &'static str {
|
||||||
|
"1 or 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait implements a pretty print function to make a variable user readable. Useful for
|
||||||
|
/// messages towards the user.
|
||||||
|
///
|
||||||
|
/// | type | output |
|
||||||
|
/// | --------- | --------- |
|
||||||
|
/// | usize, u32, u64 | positive number |
|
||||||
|
/// | f32, f64 | decimal |
|
||||||
|
/// | isize, i32, i64 | number |
|
||||||
|
/// | bool | 1 or 0 |
|
||||||
|
/// | String, str | string |
|
||||||
|
pub trait PrettyPrint {
|
||||||
|
fn pprint(&self) -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_string!(String);
|
||||||
|
|
||||||
|
impl_positive_number!(usize);
|
||||||
|
impl_positive_number!(u32);
|
||||||
|
impl_positive_number!(u64);
|
||||||
|
|
||||||
|
impl_decimal_number!(f32);
|
||||||
|
impl_decimal_number!(f64);
|
||||||
|
|
||||||
|
impl_number!(isize);
|
||||||
|
impl_number!(i32);
|
||||||
|
impl_number!(i64);
|
||||||
|
|
||||||
|
impl_bool!(bool);
|
Loading…
Reference in a new issue