From eff665a59aa5ce1efc59e567dadc91fef1ec0366 Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 16 Jun 2025 00:27:57 +0200 Subject: feat: automatically save config updates --- src/config.rs | 56 +++++++++++++++++++++++++--------------------- src/error.rs | 12 +++++----- src/main.rs | 44 +++++++++++++++++++++++++++++++----- src/panes/code_view.rs | 4 ++-- src/panes/designer_view.rs | 5 ++--- src/types.rs | 22 +++++++++++++++++- 6 files changed, 101 insertions(+), 42 deletions(-) diff --git a/src/config.rs b/src/config.rs index 8e2a63b..537cf66 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,8 +14,15 @@ use crate::{Error, environment}; #[derive(Debug, Clone, Default)] pub struct Config { - appearance: Appearance, - last_project: Option, + pub appearance: Appearance, + pub last_project: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ConfigRepr { + #[serde(default)] + pub theme: String, + pub last_project: Option, } impl Config { @@ -57,40 +64,20 @@ impl Config { pub async fn load() -> Result { use tokio::fs; - use tokio::io::AsyncWriteExt; - - #[derive(Deserialize, Serialize)] - pub struct Configuration { - #[serde(default)] - pub theme: String, - pub last_project: Option, - } let path = Self::config_file_path(); if !path.try_exists()? { - let mut config = fs::File::create(path) + Self::default() + .save() .await - .expect("expected permissions to create config file"); - let default_config = Configuration { - theme: Theme::default().to_string(), - last_project: None, - }; - - config - .write_all( - toml::to_string_pretty(&default_config) - .expect("stringify default configuration") - .as_bytes(), - ) - .await - .expect("expected config write operation to succeed"); + .expect("expected permission to write config file"); return Err(Error::ConfigMissing); } let content = fs::read_to_string(path).await?; - let Configuration { + let ConfigRepr { theme, last_project, } = toml::from_str(content.as_ref())?; @@ -146,4 +133,21 @@ impl Config { all: all.into(), }) } + + pub async fn save(self) -> Result<(), Error> { + use tokio::fs; + use tokio::io::AsyncWriteExt; + + let mut file = fs::File::create(Self::config_file_path()).await?; + + let config = ConfigRepr { + theme: self.appearance.selected.to_string(), + last_project: self.last_project, + }; + + file.write_all(toml::to_string_pretty(&config)?.as_bytes()) + .await?; + + Ok(()) + } } diff --git a/src/error.rs b/src/error.rs index 8c405b3..b98d678 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,13 +7,15 @@ use thiserror::Error; #[derive(Debug, Clone, Error)] #[error(transparent)] pub enum Error { - IO(Arc), + Io(Arc), #[error("Config file does not exist, so a default one was created")] ConfigMissing, #[error("JSON parsing error: {0}")] - SerdeJSON(Arc), + Json(Arc), #[error("TOML parsing error: {0}")] - SerdeTOML(#[from] toml::de::Error), + TomlDe(#[from] toml::de::Error), + #[error("TOML serialization error: {0}")] + TomlSer(#[from] toml::ser::Error), RustFmt(Arc), #[error("The element tree contains no matching element")] NonExistentElement, @@ -25,13 +27,13 @@ pub enum Error { impl From for Error { fn from(value: io::Error) -> Self { - Self::IO(Arc::new(value)) + Self::Io(Arc::new(value)) } } impl From for Error { fn from(value: serde_json::Error) -> Self { - Self::SerdeJSON(Arc::new(value)) + Self::Json(Arc::new(value)) } } diff --git a/src/main.rs b/src/main.rs index d0dbd9e..f9352fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,9 @@ use iced_anim::transition::Easing; use iced_anim::{Animated, Animation}; use material_theme::Theme; use panes::{code_view, designer_view, element_list}; -use types::{Action, DesignerPane, Element, Message, Panes, Project}; +use types::{ + Action, ConfigChangeType, DesignerPane, Element, Message, Panes, Project, +}; fn main() -> iced::Result { let version = std::env::args() @@ -143,13 +145,38 @@ impl IcedBuilder { } Err(error) => self.dialog = Dialog::error(error), }, - Message::SwitchTheme(event) => self.theme.update(event), + Message::ConfigWrite(result) => { + if let Err(error) = result { + self.dialog = Dialog::error(error); + } + } + Message::SaveConfigChanges(change) => { + match change { + ConfigChangeType::LastProject => { + self.config.last_project = self.project_path.clone(); + } + ConfigChangeType::SelectedTheme => { + self.config.appearance.selected = + self.theme.target().clone(); + } + } + + return Task::perform( + self.config.clone().save(), + Message::ConfigWrite, + ); + } + Message::SwitchTheme(event) => { + self.theme.update(event); + + return self.update(ConfigChangeType::SelectedTheme.into()); + } Message::CopyCode => { return clipboard::write(self.editor_content.text()); } - Message::SwitchPage(page) => self.designer_page = page, + Message::SwitchPane(pane) => self.designer_page = pane, Message::EditorAction(action) => { - if let text_editor::Action::Scroll { lines: _ } = action { + if matches!(action, text_editor::Action::Scroll { .. }) { self.editor_content.perform(action); } } @@ -318,7 +345,11 @@ impl IcedBuilder { Ok((path, project)) => { self.project = project; self.project_path = Some(path); - return self.update(Message::RefreshEditorContent); + + return Task::done( + ConfigChangeType::LastProject.into(), + ) + .chain(Task::done(Message::RefreshEditorContent)); } Err(error) => self.dialog = Dialog::error(error), }; @@ -352,6 +383,9 @@ impl IcedBuilder { Ok(path) => { self.project_path = Some(path); self.is_dirty = false; + + return self + .update(ConfigChangeType::LastProject.into()); } Err(error) => self.dialog = Dialog::error(error), } diff --git a/src/panes/code_view.rs b/src/panes/code_view.rs index 5999b8f..b7aa760 100644 --- a/src/panes/code_view.rs +++ b/src/panes/code_view.rs @@ -43,7 +43,7 @@ pub fn view( tip::Position::FollowCursor ), button("Switch to Designer view") - .on_press(Message::SwitchPage(DesignerPane::DesignerView)) + .on_press(DesignerPane::DesignerView.into()) ] .spacing(20) .align_y(Alignment::Center), @@ -57,7 +57,7 @@ pub fn view( tip::Position::FollowCursor ), button(icon::switch()) - .on_press(Message::SwitchPage(DesignerPane::DesignerView)) + .on_press(DesignerPane::DesignerView.into()) ] .spacing(20) .align_y(Alignment::Center), diff --git a/src/panes/designer_view.rs b/src/panes/designer_view.rs index 0255b40..1525993 100644 --- a/src/panes/designer_view.rs +++ b/src/panes/designer_view.rs @@ -38,12 +38,11 @@ pub fn view<'a>( .controls(pane_grid::Controls::dynamic( row![ button("Switch to Code view") - .on_press(Message::SwitchPage(DesignerPane::CodeView),) + .on_press(DesignerPane::CodeView.into(),) ] .align_y(Alignment::Center), row![ - button(icon::switch()) - .on_press(Message::SwitchPage(DesignerPane::CodeView),) + button(icon::switch()).on_press(DesignerPane::CodeView.into(),) ] .align_y(Alignment::Center), )) diff --git a/src/types.rs b/src/types.rs index dfb85d8..64f10d0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -22,9 +22,11 @@ pub type Element<'a, Message> = iced::Element<'a, Message, Theme>; #[derive(Debug, Clone)] pub enum Message { ConfigLoad(Result), + ConfigWrite(Result<(), Error>), + SaveConfigChanges(ConfigChangeType), SwitchTheme(Event), CopyCode, - SwitchPage(DesignerPane), + SwitchPane(DesignerPane), EditorAction(text_editor::Action), RefreshEditorContent, DropNewElement(ElementName, iced::Point, iced::Rectangle), @@ -59,3 +61,21 @@ pub enum DesignerPane { DesignerView, CodeView, } + +impl From for Message { + fn from(pane: DesignerPane) -> Self { + Self::SwitchPane(pane) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ConfigChangeType { + LastProject, + SelectedTheme, +} + +impl From for Message { + fn from(change: ConfigChangeType) -> Self { + Self::SaveConfigChanges(change) + } +} -- cgit v1.2.3