mirror of
https://codeberg.org/Fl1tzi/microdeck.git
synced 2024-05-16 09:51:59 +00:00
275 lines
9.4 KiB
Rust
275 lines
9.4 KiB
Rust
use crate::type_definition::PrettyPrint;
|
|
use dirs::config_dir;
|
|
use serde::Deserialize;
|
|
use serde_json;
|
|
use std::{
|
|
collections::HashMap,
|
|
env,
|
|
fmt::{self, Display},
|
|
fs,
|
|
io::ErrorKind,
|
|
path::PathBuf,
|
|
str::FromStr,
|
|
sync::Arc,
|
|
};
|
|
use tracing::debug;
|
|
|
|
/// The name of the folder which holds the config
|
|
pub const CONFIG_ENVIRONMENT_VARIABLE: &'static str = "MICRODECK_CONFIG";
|
|
pub const CONFIG_FOLDER_NAME: &'static str = "microdeck";
|
|
pub const CONFIG_FILE: &'static str = "config.json";
|
|
pub const DEFAULT_DEVICE_REFRESH_CYCLE: u64 = 3;
|
|
|
|
/// Combination of buttons acting as a folder which a device can switch to
|
|
pub type Space = Vec<Arc<Button>>;
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// CONFIGURATION DEFINITION
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CONFIGURATION
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct Config {
|
|
pub global: GlobalConfig,
|
|
pub devices: Vec<DeviceConfig>,
|
|
pub spaces: Arc<HashMap<String, Space>>,
|
|
}
|
|
|
|
/// settings that effect all devices
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct GlobalConfig {
|
|
pub font_family: Option<String>,
|
|
/// Seconds when new devices get detected
|
|
#[serde(default = "default_device_refresh_cycle")]
|
|
pub device_list_refresh_cycle: u64,
|
|
}
|
|
|
|
fn default_device_refresh_cycle() -> u64 {
|
|
DEFAULT_DEVICE_REFRESH_CYCLE
|
|
}
|
|
|
|
/// configuration of a single device with its default page
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct DeviceConfig {
|
|
pub serial: String,
|
|
#[serde(default = "default_brightness")]
|
|
pub brightness: u8,
|
|
pub buttons: Vec<Arc<Button>>,
|
|
}
|
|
|
|
fn default_brightness() -> u8 {
|
|
100
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct Button {
|
|
pub index: u8,
|
|
pub module: String,
|
|
/// options which get passed to the module
|
|
#[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()
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// BUTTON CONFIGURATION ERRORS
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#[derive(Debug)]
|
|
pub enum ButtonConfigError {
|
|
/// (Key, Expected)
|
|
WrongType(String, &'static str),
|
|
/// a required value
|
|
Required(&'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::Required(key) => {
|
|
write!(formatter, "A value for the option \"{key}\" is required.")
|
|
}
|
|
ButtonConfigError::General(message) => {
|
|
write!(formatter, "{message}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// See [parse_button_config()]
|
|
pub enum ParseButtonConfigResult<T: FromStr> {
|
|
/// (user-defined value, key)
|
|
Found(T, &'static str),
|
|
/// (alternative value, key)
|
|
NotFound(T, &'static str),
|
|
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),
|
|
}
|
|
}
|
|
|
|
/// instead of the enum return Result<Value, ButtonConfigError>
|
|
///
|
|
/// This makes a user-defined value required.
|
|
///
|
|
/// [ParseButtonConfigResult::Found] => Ok(value)
|
|
/// [ParseButtonConfigResult::NotFound] | [ParseButtonConfigResult::ParseError] => Err(e)
|
|
pub fn required(self) -> Result<T, ButtonConfigError> {
|
|
match self {
|
|
ParseButtonConfigResult::Found(v, _) => Ok(v),
|
|
ParseButtonConfigResult::NotFound(_, k) => Err(ButtonConfigError::Required(k)),
|
|
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, key),
|
|
};
|
|
// check if value could be parsed
|
|
if let Ok(out) = parse_result {
|
|
return ParseButtonConfigResult::Found(out, key);
|
|
}
|
|
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
|
|
#[allow(dead_code)]
|
|
pub fn raw_module(&self, key: &String) -> Option<&String> {
|
|
self.options.get(key)
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PARSING CONFIG
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#[tracing::instrument]
|
|
pub fn load_config() -> Result<Config, ConfigError> {
|
|
let config_file: PathBuf = match env::var_os(CONFIG_ENVIRONMENT_VARIABLE) {
|
|
Some(path) => {
|
|
debug!("Using env variable: {:?}", path);
|
|
PathBuf::from(path)
|
|
}
|
|
None => {
|
|
// try to get the system config dir; env var required if not available
|
|
if let Some(mut path) = config_dir() {
|
|
path.push(CONFIG_FOLDER_NAME);
|
|
path.push(CONFIG_FILE);
|
|
debug!("Using system path: {:?}", path);
|
|
path
|
|
} else {
|
|
return Err(ConfigError::PathNotAvailable());
|
|
}
|
|
}
|
|
};
|
|
|
|
let path = config_file.display().to_string().clone();
|
|
|
|
match fs::read_to_string(config_file) {
|
|
Ok(content) => {
|
|
serde_json::from_str(&content).map_err(|e| ConfigError::SyntaxError(e.to_string()))
|
|
}
|
|
Err(file_error) => {
|
|
if file_error.kind() == ErrorKind::NotFound {
|
|
return Err(ConfigError::FilePathDoesNotExist(path));
|
|
} else {
|
|
return Err(ConfigError::ReadError(file_error.to_string()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// OVERALL CONFIGURATION ERRORS
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// General error for parsing the configuration
|
|
#[derive(Debug)]
|
|
pub enum ConfigError {
|
|
ButtonDoesNotExist(u8),
|
|
ModuleDoesNotExist(u8, String),
|
|
PathNotAvailable(),
|
|
SyntaxError(String),
|
|
FilePathDoesNotExist(String),
|
|
ReadError(String),
|
|
}
|
|
|
|
impl Display for ConfigError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
ConfigError::ButtonDoesNotExist(index) => {
|
|
write!(formatter, "Button {}: does not exist.", index)
|
|
}
|
|
ConfigError::ModuleDoesNotExist(index, module) => write!(
|
|
formatter,
|
|
"Button {}: The module \"{}\" does not exist.",
|
|
index, module
|
|
),
|
|
ConfigError::PathNotAvailable() => write!(
|
|
formatter,
|
|
"Config directory not available. Please use the environment variable \"DACH_DECKER_CONFIG\" to specify the location of the config."
|
|
),
|
|
ConfigError::SyntaxError(text) => write!(
|
|
formatter,
|
|
"Syntax error in configuration:\n{}",
|
|
text
|
|
),
|
|
ConfigError::FilePathDoesNotExist(path) => write!(
|
|
formatter,
|
|
"The configuration file does not exist in {}",
|
|
path
|
|
),
|
|
ConfigError::ReadError(error) => write!(
|
|
formatter,
|
|
"Could not read the configuration file: {}",
|
|
error
|
|
)
|
|
}
|
|
}
|
|
}
|