1
0
mirror of synced 2024-12-22 17:13:08 +01:00

Implemented reading/writing of json settings

This commit is contained in:
Mark van Renswoude 2024-06-27 13:21:23 +02:00
parent ec70ed2c51
commit 260ecdc531
14 changed files with 383 additions and 1243 deletions

1119
Linux/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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

View File

@ -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
View 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
View 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\.\-_]+$" }
}

View File

@ -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: &gtk::Notebook, title_key: &str) -> gtk::Box
impl MainWindow
{
let tab = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
fn init_ui(window: &gtk::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: &gtk::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: &gtk::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
}
}

View File

@ -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}");
}
}
}

View 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
}
}
}

View File

@ -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);
}

View File

@ -1 +1,3 @@
pub mod unique_id;
pub mod unique_id;
pub mod option_result;
pub mod validated_string;

View 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}"),
}
}
}

View File

@ -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\.\-_]+$" }
}

View 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;
}