Implemented reading/writing of json settings
This commit is contained in:
parent
ec70ed2c51
commit
260ecdc531
1119
Linux/Cargo.lock
generated
1119
Linux/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
env_logger = "0.11.3"
|
||||
log = "0.4.21"
|
||||
platform-dirs = "0.3.0"
|
||||
@ -12,11 +13,8 @@ regex = "1.10.5"
|
||||
relm4 = "0.8.1"
|
||||
relm4-icons = "0.8.3"
|
||||
rust-i18n = "3.0.1"
|
||||
serde = "1.0.203"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
|
||||
[dependencies.min-rs]
|
||||
git = "https://github.com/MvRens/min-rs.git"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.6"
|
||||
walkdir = "2.5.0"
|
||||
git = "https://github.com/MvRens/min-rs.git"
|
@ -3,6 +3,7 @@ _version: 2
|
||||
mainwindow:
|
||||
title:
|
||||
en: MassiveKnob
|
||||
|
||||
tab:
|
||||
device:
|
||||
en: Device
|
||||
@ -13,4 +14,8 @@ mainwindow:
|
||||
analogoutputs:
|
||||
en: Analog outputs
|
||||
digitaloutputs:
|
||||
en: Digital outputs
|
||||
en: Digital outputs
|
||||
|
||||
deviceType:
|
||||
label:
|
||||
en: Type
|
@ -1,57 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::{Error, Read, Write};
|
||||
use platform_dirs::AppDirs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config
|
||||
{
|
||||
root: PathBuf,
|
||||
pub device_id: Option<String>
|
||||
}
|
||||
|
||||
|
||||
impl Config
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
let appdirs = AppDirs::new(Some("massiveknob"), false).unwrap();
|
||||
|
||||
Self
|
||||
{
|
||||
root: appdirs.data_dir,
|
||||
device_id: None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_reader(&self, name: &str) -> Option<impl Read>
|
||||
{
|
||||
let path = Path::join(&self.root, name);
|
||||
if !path.exists()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match std::fs::File::open(path)
|
||||
{
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_writer(&self, name: &str) -> Result<impl Write, Error>
|
||||
{
|
||||
let path = Path::join(&self.root, name);
|
||||
if !path.exists()
|
||||
{
|
||||
match std::fs::create_dir_all(path.clone())
|
||||
{
|
||||
Ok(_v) => (),
|
||||
Err(e) => return Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::File::create(path)
|
||||
}
|
||||
}
|
55
Linux/src/config/json.rs
Normal file
55
Linux/src/config/json.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use crate::util::option_result::OptionResult;
|
||||
use super::{ConfigManager, ConfigName};
|
||||
|
||||
pub trait JsonConfigManager
|
||||
{
|
||||
fn read_json<T>(&self, name: &ConfigName) -> OptionResult<T, Error> where T : serde::de::DeserializeOwned;
|
||||
fn write_json<T>(&self, name: &ConfigName, value: &T) -> Result<(), Error> where T : serde::ser::Serialize;
|
||||
}
|
||||
|
||||
|
||||
impl JsonConfigManager for ConfigManager
|
||||
{
|
||||
fn read_json<T>(&self, name: &ConfigName) -> OptionResult<T, Error> where T : serde::de::DeserializeOwned
|
||||
{
|
||||
match self.get_reader(&json_config_name(name))
|
||||
{
|
||||
OptionResult::None => OptionResult::None,
|
||||
|
||||
OptionResult::Some(reader) =>
|
||||
{
|
||||
match serde_json::from_reader(reader)
|
||||
{
|
||||
Ok(v) => OptionResult::Some(v),
|
||||
Err(e) => OptionResult::Err(e.into())
|
||||
}
|
||||
},
|
||||
|
||||
OptionResult::Err(e) => OptionResult::Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn write_json<T>(&self, name: &ConfigName, value: &T) -> Result<(), Error> where T : serde::ser::Serialize
|
||||
{
|
||||
match self.get_writer(&json_config_name(name))
|
||||
{
|
||||
Ok(writer) => match serde_json::to_writer(writer, value)
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.into())
|
||||
},
|
||||
|
||||
Err(e) => Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[inline]
|
||||
fn json_config_name(name: &ConfigName) -> ConfigName
|
||||
{
|
||||
ConfigName::new(format!("{}.json", name.as_str()).as_str())
|
||||
}
|
75
Linux/src/config/mod.rs
Normal file
75
Linux/src/config/mod.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::{Error, Read, Write};
|
||||
use platform_dirs::AppDirs;
|
||||
|
||||
use crate::util::option_result::OptionResult;
|
||||
use crate::util::validated_string::{ValidatedString, ValidatedStringPattern};
|
||||
|
||||
|
||||
pub mod json;
|
||||
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigManager
|
||||
{
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
impl ConfigManager
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
let appdirs = AppDirs::new(Some("massiveknob"), false).unwrap();
|
||||
|
||||
Self
|
||||
{
|
||||
root: appdirs.data_dir
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_reader(&self, name: &ConfigName) -> OptionResult<impl Read, anyhow::Error>
|
||||
{
|
||||
let path = Path::join(&self.root, name.as_str());
|
||||
if !path.exists()
|
||||
{
|
||||
return OptionResult::None;
|
||||
}
|
||||
|
||||
match std::fs::File::open(path)
|
||||
{
|
||||
Ok(v) => OptionResult::Some(v),
|
||||
Err(e) => OptionResult::Err(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_writer(&self, name: &ConfigName) -> Result<impl Write, Error>
|
||||
{
|
||||
let path = Path::join(&self.root, name.as_str());
|
||||
if !path.exists()
|
||||
{
|
||||
match std::fs::create_dir_all(path.clone())
|
||||
{
|
||||
Ok(_v) => (),
|
||||
Err(e) => return Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::File::create(path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub type ConfigName = ValidatedString<ConfigNamePattern>;
|
||||
|
||||
|
||||
pub struct ConfigNamePattern;
|
||||
|
||||
impl ValidatedStringPattern for ConfigNamePattern
|
||||
{
|
||||
fn pattern() -> &'static str { r"^[a-zA-Z0-9\.\-_]+$" }
|
||||
}
|
@ -34,6 +34,13 @@ pub enum MainWindowMsg
|
||||
|
||||
pub struct MainWindowWidgets
|
||||
{
|
||||
device: MainWindowDeviceWidgets
|
||||
}
|
||||
|
||||
|
||||
pub struct MainWindowDeviceWidgets
|
||||
{
|
||||
devices_combobox: gtk::ComboBoxText
|
||||
}
|
||||
|
||||
|
||||
@ -60,18 +67,8 @@ impl SimpleComponent for MainWindow
|
||||
fn init(_data: Self::Init, window: Self::Root, _sender: ComponentSender<Self>, ) -> ComponentParts<Self>
|
||||
{
|
||||
let model = MainWindow {};
|
||||
let widgets = Self::init_ui(&window);
|
||||
|
||||
let tabs = gtk::Notebook::builder().build();
|
||||
window.set_child(Some(&tabs));
|
||||
|
||||
add_box_tab(&tabs, "mainwindow.tab.device");
|
||||
add_box_tab(&tabs, "mainwindow.tab.analoginputs");
|
||||
add_box_tab(&tabs, "mainwindow.tab.digitalinputs");
|
||||
//add_box_tab(&tabs, "mainwindow.tab.analogoutputs");
|
||||
//add_box_tab(&tabs, "mainwindow.tab.digitaloutputs");
|
||||
|
||||
|
||||
let widgets = MainWindowWidgets {};
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
@ -85,17 +82,73 @@ impl SimpleComponent for MainWindow
|
||||
}
|
||||
|
||||
|
||||
fn add_box_tab(notebook: >k::Notebook, title_key: &str) -> gtk::Box
|
||||
|
||||
impl MainWindow
|
||||
{
|
||||
let tab = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
fn init_ui(window: >k::Window) -> MainWindowWidgets
|
||||
{
|
||||
let tabs = gtk::Notebook::builder().build();
|
||||
window.set_child(Some(&tabs));
|
||||
|
||||
let tab_label = gtk::Label::builder()
|
||||
.label(t!(title_key))
|
||||
.build();
|
||||
MainWindowWidgets
|
||||
{
|
||||
device: Self::init_device_tab(&tabs)
|
||||
//Self::new_box_tab(&tabs, "mainwindow.tab.analoginputs");
|
||||
//Self::new_box_tab(&tabs, "mainwindow.tab.digitalinputs");
|
||||
//Self::add_box_tab(&tabs, "mainwindow.tab.analogoutputs");
|
||||
//Self::add_box_tab(&tabs, "mainwindow.tab.digitaloutputs");
|
||||
}
|
||||
}
|
||||
|
||||
notebook.append_page(&tab, Some(&tab_label));
|
||||
|
||||
tab
|
||||
fn init_device_tab(tabs: >k::Notebook) -> MainWindowDeviceWidgets
|
||||
{
|
||||
let tab = Self::new_box_tab(&tabs, "mainwindow.tab.device");
|
||||
|
||||
let label = gtk::Label::builder()
|
||||
.label(t!("mainwindow.deviceType.label"))
|
||||
.halign(gtk::Align::Start)
|
||||
.build();
|
||||
|
||||
tab.append(&label);
|
||||
|
||||
|
||||
|
||||
let devices_combobox = gtk::ComboBoxText::builder()
|
||||
.build();
|
||||
|
||||
tab.append(&devices_combobox);
|
||||
|
||||
|
||||
// TEMP
|
||||
devices_combobox.append_text("Test");
|
||||
devices_combobox.append_text("Test 2");
|
||||
|
||||
|
||||
MainWindowDeviceWidgets
|
||||
{
|
||||
devices_combobox
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn new_box_tab(notebook: >k::Notebook, title_key: &str) -> gtk::Box
|
||||
{
|
||||
let tab = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.margin_start(8)
|
||||
.margin_end(8)
|
||||
.margin_top(8)
|
||||
.margin_bottom(8)
|
||||
.build();
|
||||
|
||||
let tab_label = gtk::Label::builder()
|
||||
.label(t!(title_key))
|
||||
.build();
|
||||
|
||||
notebook.append_page(&tab, Some(&tab_label));
|
||||
|
||||
tab
|
||||
}
|
||||
}
|
@ -1,18 +1,25 @@
|
||||
use crate::actions;
|
||||
use crate::actions::MkAction;
|
||||
use crate::config::Config;
|
||||
use crate::config::json::JsonConfigManager;
|
||||
use crate::config::{ConfigManager, ConfigName};
|
||||
use crate::devices;
|
||||
use crate::devices::MkDevice;
|
||||
use crate::registry::MkRegistry;
|
||||
use crate::util::unique_id::UniqueId;
|
||||
|
||||
|
||||
mod settings;
|
||||
|
||||
|
||||
pub struct Orchestrator
|
||||
{
|
||||
config: Config,
|
||||
config_manager: ConfigManager,
|
||||
|
||||
device_registry: MkRegistry<MkDevice>,
|
||||
action_registry: MkRegistry<MkAction>
|
||||
action_registry: MkRegistry<MkAction>,
|
||||
|
||||
settings_name: ConfigName,
|
||||
settings: settings::Settings
|
||||
}
|
||||
|
||||
|
||||
@ -20,8 +27,14 @@ impl Orchestrator
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
let config = Config::new();
|
||||
//config.get_reader(name)
|
||||
let config_manager = ConfigManager::new();
|
||||
|
||||
let settings_name = ConfigName::new("settings");
|
||||
let settings = match config_manager.read_json(&settings_name).expect("Error reading settings")
|
||||
{
|
||||
None => settings::Settings::new(),
|
||||
Some(v) => v
|
||||
};
|
||||
|
||||
let mut device_registry = MkRegistry::new();
|
||||
let mut action_registry = MkRegistry::new();
|
||||
@ -31,23 +44,41 @@ impl Orchestrator
|
||||
|
||||
Self
|
||||
{
|
||||
config,
|
||||
config_manager,
|
||||
|
||||
device_registry,
|
||||
action_registry
|
||||
action_registry,
|
||||
|
||||
settings_name,
|
||||
settings
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn current_device(&self) -> Option<&MkDevice>
|
||||
{
|
||||
let Some(device_id) = &self.config.device_id else { return None };
|
||||
let Some(device_id) = &self.settings.device_id else { return None };
|
||||
self.device_registry.by_id(UniqueId::new(device_id.as_str()))
|
||||
}
|
||||
|
||||
|
||||
pub fn set_current_device_id(&self, id: &str)
|
||||
pub fn set_current_device_id(&mut self, id: &str)
|
||||
{
|
||||
// TODO if changed, unload old device, activate new
|
||||
todo!("Store in config");
|
||||
let new_id = Some(String::from(id));
|
||||
if new_id == self.settings.device_id { return; }
|
||||
|
||||
self.settings.device_id = new_id;
|
||||
self.store_settings();
|
||||
|
||||
// TODO unload old device, activate new
|
||||
}
|
||||
|
||||
|
||||
fn store_settings(&self)
|
||||
{
|
||||
if let Err(e) = self.config_manager.write_json(&self.settings_name, &self.settings)
|
||||
{
|
||||
log::error!("Error writing settings: {e}");
|
||||
}
|
||||
}
|
||||
}
|
19
Linux/src/orchestrator/settings.rs
Normal file
19
Linux/src/orchestrator/settings.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Settings
|
||||
{
|
||||
pub device_id: Option<String>
|
||||
}
|
||||
|
||||
|
||||
impl Settings
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self
|
||||
{
|
||||
device_id: None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use log::info;
|
||||
use log;
|
||||
|
||||
use crate::util::unique_id::UniqueId;
|
||||
|
||||
@ -38,7 +38,7 @@ impl<'a, T> MkRegistry<T> where T: RegistryItem
|
||||
{
|
||||
let device_id = device.unique_id();
|
||||
|
||||
info!("Registered device: [{}] {}", device_id.as_str(), device.name());
|
||||
log::debug!("Registered device: [{}] {}", device_id.as_str(), device.name());
|
||||
self.items.insert(String::from(device_id.as_str()), device);
|
||||
}
|
||||
|
||||
|
@ -1 +1,3 @@
|
||||
pub mod unique_id;
|
||||
pub mod unique_id;
|
||||
pub mod option_result;
|
||||
pub mod validated_string;
|
||||
|
33
Linux/src/util/option_result.rs
Normal file
33
Linux/src/util/option_result.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
|
||||
pub enum OptionResult<T, E: Display>
|
||||
{
|
||||
None,
|
||||
Some(T),
|
||||
Err(E),
|
||||
}
|
||||
|
||||
|
||||
impl<T, E: Display> OptionResult<T, E>
|
||||
{
|
||||
pub fn unwrap(self) -> Option<T>
|
||||
{
|
||||
match self
|
||||
{
|
||||
OptionResult::None => None,
|
||||
OptionResult::Some(v) => Some(v),
|
||||
OptionResult::Err(e) => panic!("called `OptionResult::unwrap()` on an `Err` value: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect(self, msg: &str) -> Option<T>
|
||||
{
|
||||
match self
|
||||
{
|
||||
OptionResult::None => None,
|
||||
OptionResult::Some(v) => Some(v),
|
||||
OptionResult::Err(e) => panic!("{msg}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +1,12 @@
|
||||
use regex::Regex;
|
||||
use super::validated_string::{ValidatedString, ValidatedStringPattern};
|
||||
|
||||
|
||||
pub struct UniqueId
|
||||
pub type UniqueId = ValidatedString<UniqueIdPattern>;
|
||||
|
||||
|
||||
pub struct UniqueIdPattern;
|
||||
|
||||
impl ValidatedStringPattern for UniqueIdPattern
|
||||
{
|
||||
inner: String,
|
||||
}
|
||||
|
||||
|
||||
impl UniqueId
|
||||
{
|
||||
pub fn new(id: &str) -> Self
|
||||
{
|
||||
assert!(is_valid_unique_id(id), "Id '{id}' has invalid characters");
|
||||
UniqueId { inner: id.to_string() }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str
|
||||
{
|
||||
self.inner.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn is_valid_unique_id(id: &str) -> bool
|
||||
{
|
||||
let re = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap();
|
||||
re.is_match(id)
|
||||
}
|
||||
|
||||
|
||||
impl Clone for UniqueId
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
Self { inner: self.inner.clone() }
|
||||
}
|
||||
fn pattern() -> &'static str { r"^[a-zA-Z0-9\.\-_]+$" }
|
||||
}
|
55
Linux/src/util/validated_string.rs
Normal file
55
Linux/src/util/validated_string.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::marker::PhantomData;
|
||||
use regex::Regex;
|
||||
|
||||
|
||||
/// A string which must conform to the specified regex pattern,
|
||||
/// otherwise it will panic by design. Intended for code validation,
|
||||
/// not for runtime input validation.
|
||||
pub struct ValidatedString<T: ValidatedStringPattern>
|
||||
{
|
||||
inner: String,
|
||||
|
||||
// Satisfy the compiler's demand to use T
|
||||
_phantom: std::marker::PhantomData<T>
|
||||
}
|
||||
|
||||
|
||||
impl<T: ValidatedStringPattern> ValidatedString<T>
|
||||
{
|
||||
pub fn new(value: &str) -> Self
|
||||
{
|
||||
let pattern = Regex::new(T::pattern()).unwrap();
|
||||
assert!(pattern.is_match(value), "Value '{value}' has invalid characters");
|
||||
|
||||
Self
|
||||
{
|
||||
inner: value.to_string(),
|
||||
_phantom: PhantomData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn as_str(&self) -> &str
|
||||
{
|
||||
self.inner.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<T: ValidatedStringPattern> Clone for ValidatedString<T>
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
Self
|
||||
{
|
||||
inner: self.inner.clone(),
|
||||
_phantom: PhantomData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait ValidatedStringPattern
|
||||
{
|
||||
fn pattern() -> &'static str;
|
||||
}
|
Loading…
Reference in New Issue
Block a user