Skip to main content

cmdpal/
settings.rs

1//! Types for building extension settings page.
2
3use crate::bindings::*;
4use crate::page::content::ContentPage;
5
6use crate::utils::ComBuilder;
7use serde_json::{Map, Value as JsonValue, json};
8use std::sync::{Arc, Mutex};
9use windows::Win32::Foundation::{E_FAIL, ERROR_FILE_INVALID};
10use windows_core::{ComObject, Error, implement};
11
12/// A raw implementation of the [`ICommandSettings`] interface.
13#[implement(ICommandSettings)]
14pub struct CommandSettings(pub ComObject<ContentPage>);
15
16impl ICommandSettings_Impl for CommandSettings_Impl {
17    fn SettingsPage(&self) -> windows_core::Result<IContentPage> {
18        Ok(self.0.to_interface())
19    }
20}
21
22#[derive(Debug, Clone)]
23struct BasePropSetting {
24    id: String,
25    is_required: bool,
26    error_message: String,
27    title: String,
28    label: String,
29}
30
31impl BasePropSetting {
32    fn new(id: impl ToString) -> Self {
33        Self {
34            id: id.to_string(),
35            is_required: false,
36            error_message: String::new(),
37            title: String::new(),
38            label: String::new(),
39        }
40    }
41
42    fn serialize(&self) -> Map<String, JsonValue> {
43        let mut map = Map::new();
44        map.insert("id".into(), json!(self.id));
45        map.insert("isRequired".into(), json!(self.is_required));
46        map.insert("errorMessage".into(), json!(self.error_message));
47        map.insert("title".into(), json!(self.title));
48        map.insert("label".into(), json!(self.label));
49        map
50    }
51}
52
53trait SettingItem {
54    fn id(&self) -> &str;
55    fn base_prop_mut(&mut self) -> &mut BasePropSetting;
56    fn serialize_adaptive_card(&self) -> serde_json::Value;
57    fn serialize_value(&self) -> Option<serde_json::Value>;
58    fn update(&self, data: &Map<String, JsonValue>);
59}
60
61/// Helper trait to modify base properties of a setting item.
62pub trait SettingBasePropModifier {
63    /// Specify whether this setting is required.
64    fn is_required(self, is_required: bool) -> Self;
65    /// Specify an error message to show when the setting is invalid.
66    fn error_message(self, error_message: impl ToString) -> Self;
67    /// Caption which will be shown next to the input.
68    fn caption(self, caption: impl ToString) -> Self;
69    /// Descriptive text shown for the setting item.
70    fn description(self, description: impl ToString) -> Self;
71}
72
73impl<T> SettingBasePropModifier for T
74where
75    T: SettingItem,
76{
77    fn caption(mut self, caption: impl ToString) -> Self {
78        self.base_prop_mut().label = caption.to_string();
79        self
80    }
81
82    fn error_message(mut self, error_message: impl ToString) -> Self {
83        self.base_prop_mut().error_message = error_message.to_string();
84        self
85    }
86
87    fn description(mut self, description: impl ToString) -> Self {
88        self.base_prop_mut().title = description.to_string();
89        self
90    }
91
92    fn is_required(mut self, is_required: bool) -> Self {
93        self.base_prop_mut().is_required = is_required;
94        self
95    }
96}
97
98/// A setting item that allows users to input text.
99#[derive(Debug, Clone)]
100pub struct TextSetting {
101    placeholder: String,
102    pattern: String,
103    is_multiline: bool,
104    base_prop: BasePropSetting,
105    value: Arc<Mutex<Option<String>>>,
106}
107
108impl TextSetting {
109    /// Creates a new `TextSetting` with the given ID.
110    ///
111    /// The ID should be unique across all settings in this `CommandSettings`.
112    pub fn new(id: impl ToString) -> Self {
113        Self {
114            placeholder: String::new(),
115            pattern: String::new(),
116            is_multiline: false,
117            base_prop: BasePropSetting::new(id),
118            value: Arc::new(Mutex::new(None)),
119        }
120    }
121
122    /// Sets the placeholder text for the input field.
123    pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
124        self.placeholder = placeholder.to_string();
125        self
126    }
127
128    /// Sets the regex pattern that the input must match.
129    pub fn pattern(mut self, pattern: impl ToString) -> Self {
130        self.pattern = pattern.to_string();
131        self
132    }
133
134    /// Specifies whether the input field should allow multiple lines.
135    pub fn is_multiline(mut self, is_multiline: bool) -> Self {
136        self.is_multiline = is_multiline;
137        self
138    }
139
140    /// Specifies a default value for the input field.
141    /// This will be used when there are no prior settings saved.
142    pub fn default(self, value: impl ToString) -> Self {
143        self.value
144            .lock()
145            .ok()
146            .map(|mut v| v.replace(value.to_string()));
147        self
148    }
149}
150
151/// A setting item that allows users to input a number.
152///
153/// The number input will always be a floating point value.
154#[derive(Debug, Clone)]
155pub struct NumberSetting {
156    placeholder: String,
157    min: Option<f64>,
158    max: Option<f64>,
159    base_prop: BasePropSetting,
160    value: Arc<Mutex<Option<f64>>>,
161}
162
163impl NumberSetting {
164    /// Creates a new `NumberSetting` with the given ID.
165    ///
166    /// The ID should be unique across all settings in this `CommandSettings`.
167    pub fn new(id: impl ToString) -> Self {
168        Self {
169            placeholder: String::new(),
170            min: None,
171            max: None,
172            base_prop: BasePropSetting::new(id),
173            value: Arc::new(Mutex::new(None)),
174        }
175    }
176
177    /// Sets the placeholder text for the input field.
178    /// Doesn't have to be a valid number.
179    pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
180        self.placeholder = placeholder.to_string();
181        self
182    }
183
184    /// Sets the minimum value for the input field.
185    pub fn min(mut self, min: f64) -> Self {
186        self.min = Some(min);
187        self
188    }
189
190    /// Sets the maximum value for the input field.
191    pub fn max(mut self, max: f64) -> Self {
192        self.max = Some(max);
193        self
194    }
195
196    /// Specifies a default value for the input field.
197    ///
198    /// This will be used when there are no prior settings saved.
199    pub fn default(self, value: f64) -> Self {
200        self.value.lock().ok().map(|mut v| v.replace(value));
201        self
202    }
203}
204
205/// A setting item that allows users to toggle a boolean value.
206///
207/// This is typically used for settings that can be enabled or disabled, like a switch.
208#[derive(Debug, Clone)]
209pub struct ToggleSetting {
210    base_prop: BasePropSetting,
211    value: Arc<Mutex<Option<bool>>>,
212}
213
214impl ToggleSetting {
215    /// Creates a new `ToggleSetting` with the given ID.
216    ///
217    /// The ID should be unique across all settings in this `CommandSettings`.
218    pub fn new(id: impl ToString) -> Self {
219        Self {
220            base_prop: BasePropSetting::new(id),
221            value: Arc::new(Mutex::new(None)),
222        }
223    }
224
225    /// Specifies a default value for the toggle setting.
226    ///
227    /// This will be used when there are no prior settings saved.
228    pub fn default(self, value: bool) -> Self {
229        self.value.lock().ok().map(|mut v| v.replace(value));
230        self
231    }
232}
233
234/// A trait for defining choices in a choice set setting.
235///
236/// The trait has already been implemented for `String`, `&str`, and tuples of `(&str, &str)`.
237///
238/// You can implement this trait for your own types to use them in a [`ChoiceSetSetting`]
239/// (as long as their `value` don't collide with each other):
240///
241/// ```rust
242/// # use cmdpal::settings::Choice;
243///
244/// #[derive(Debug, Clone)]
245/// enum TextSize {
246///     Small,
247///     Medium,
248///     Large
249/// }
250///
251/// impl Choice for TextSize {
252///     fn value(&self) -> &str {
253///         match self {
254///             TextSize::Small => "small",
255///             TextSize::Medium => "medium",
256///             TextSize::Large => "large",
257///         }
258///     }
259///     fn title(&self) -> &str {
260///         match self {
261///            TextSize::Small => "Small",
262///            TextSize::Medium => "Medium",
263///           TextSize::Large => "Large",
264///        }
265///     }   
266/// }
267/// ```
268pub trait Choice: Sized + Clone + 'static {
269    /// Returns the value of the choice.
270    ///
271    /// This value should be unique across all choices in the set.
272    fn value(&self) -> &str;
273
274    /// Returns the title of the choice.
275    fn title(&self) -> &str;
276}
277
278impl Choice for String {
279    fn value(&self) -> &str {
280        self
281    }
282
283    fn title(&self) -> &str {
284        self
285    }
286}
287
288impl Choice for (&'static str, &'static str) {
289    fn value(&self) -> &str {
290        self.0
291    }
292
293    fn title(&self) -> &str {
294        self.1
295    }
296}
297
298impl Choice for &'static str {
299    fn value(&self) -> &str {
300        self
301    }
302
303    fn title(&self) -> &str {
304        self
305    }
306}
307
308/// A setting item that allows users to select one from a set of choices.
309#[derive(Debug, Clone)]
310pub struct ChoiceSetSetting<T> {
311    choices: Vec<T>,
312    base_prop: BasePropSetting,
313    value: Arc<Mutex<Option<T>>>,
314}
315
316impl<T: Choice> ChoiceSetSetting<T> {
317    /// Creates a new `ChoiceSetSetting` with the given ID.
318    ///
319    /// The ID should be unique across all settings in this `CommandSettings`.
320    pub fn new(id: impl ToString) -> Self {
321        Self {
322            choices: Vec::new(),
323            base_prop: BasePropSetting::new(id),
324            value: Arc::new(Mutex::new(None)),
325        }
326    }
327
328    /// Adds a choice to the set.
329    ///
330    /// The choice must implement the [`Choice`] trait.
331    pub fn add_choice(mut self, choice: T) -> Self {
332        self.choices.push(choice);
333        self
334    }
335
336    /// Sets the choices for the set.
337    ///
338    /// This will replace any existing choices.
339    pub fn choices(mut self, choices: Vec<T>) -> Self {
340        self.choices = choices;
341        self
342    }
343
344    /// Specifies a default choice for the set.
345    ///
346    /// This will be used when there are no prior settings saved.
347    pub fn default(self, value: T) -> Self {
348        self.value.lock().ok().map(|mut v| v.replace(value));
349        self
350    }
351}
352
353impl SettingItem for TextSetting {
354    fn base_prop_mut(&mut self) -> &mut BasePropSetting {
355        &mut self.base_prop
356    }
357
358    fn id(&self) -> &str {
359        &self.base_prop.id
360    }
361
362    fn serialize_adaptive_card(&self) -> serde_json::Value {
363        let mut map = self.base_prop.serialize();
364        map.insert("type".into(), json!("Input.Text"));
365        map.insert("placeholder".into(), json!(self.placeholder));
366        map.insert("isMultiline".into(), json!(self.is_multiline));
367        map.insert("pattern".into(), json!(self.pattern));
368        if let Some(value) = self.value.lock().ok().and_then(|v| (*v).clone()) {
369            map.insert("value".into(), json!(value));
370        }
371        json!(map)
372    }
373
374    fn update(&self, data: &Map<String, JsonValue>) {
375        if let Some(value) = data
376            .get(self.id())
377            .and_then(|v| v.as_str().map(|s| s.to_string()))
378        {
379            self.value.lock().ok().map(|mut v| v.replace(value));
380        }
381    }
382
383    fn serialize_value(&self) -> Option<serde_json::Value> {
384        self.value
385            .lock()
386            .ok()
387            .and_then(|v| v.clone())
388            .map(|v| json!(v))
389    }
390}
391
392// NOTE: Special case for NumberSetting since it uses number as default value field and string for value returns.
393
394impl SettingItem for NumberSetting {
395    fn base_prop_mut(&mut self) -> &mut BasePropSetting {
396        &mut self.base_prop
397    }
398
399    fn id(&self) -> &str {
400        &self.base_prop.id
401    }
402
403    fn serialize_adaptive_card(&self) -> serde_json::Value {
404        let mut map = self.base_prop.serialize();
405        map.insert("type".into(), json!("Input.Number"));
406        map.insert("placeholder".into(), json!(self.placeholder));
407        if let Some(min) = self.min {
408            map.insert("min".into(), json!(min));
409        }
410        if let Some(max) = self.max {
411            map.insert("max".into(), json!(max));
412        }
413        if let Some(value) = self.value.lock().ok().and_then(|v| *v) {
414            map.insert("value".into(), json!(value));
415        }
416
417        json!(map)
418    }
419
420    fn update(&self, data: &Map<String, JsonValue>) {
421        if let Some(value) = data
422            .get(self.id())
423            .and_then(|v| v.as_str())
424            .and_then(|s| s.parse().ok())
425        {
426            self.value.lock().ok().map(|mut v| v.replace(value));
427        }
428    }
429
430    fn serialize_value(&self) -> Option<serde_json::Value> {
431        self.value
432            .lock()
433            .ok()
434            .and_then(|v| v.clone())
435            .map(|v| json!(v.to_string()))
436    }
437}
438
439// NOTE: Special case for ToggleSetting since it uses string as default value field and returns.
440
441impl SettingItem for ToggleSetting {
442    fn base_prop_mut(&mut self) -> &mut BasePropSetting {
443        &mut self.base_prop
444    }
445
446    fn id(&self) -> &str {
447        &self.base_prop.id
448    }
449
450    fn serialize_adaptive_card(&self) -> serde_json::Value {
451        let mut map = self.base_prop.serialize();
452        map.insert("type".into(), json!("Input.Toggle"));
453        if let Some(value) = self.value.lock().ok().and_then(|v| *v) {
454            map.insert("value".into(), json!(value.to_string()));
455        }
456        json!(map)
457    }
458
459    fn update(&self, data: &Map<String, JsonValue>) {
460        if let Some(value) = data.get(self.id()).and_then(|v| match v.as_str() {
461            Some("true") => Some(true),
462            Some("false") => Some(false),
463            _ => None,
464        }) {
465            self.value.lock().ok().map(|mut v| v.replace(value));
466        }
467    }
468
469    fn serialize_value(&self) -> Option<serde_json::Value> {
470        self.value
471            .lock()
472            .ok()
473            .and_then(|v| v.clone())
474            .map(|v| json!(v))
475    }
476}
477
478impl<T: Choice> SettingItem for ChoiceSetSetting<T> {
479    fn base_prop_mut(&mut self) -> &mut BasePropSetting {
480        &mut self.base_prop
481    }
482
483    fn id(&self) -> &str {
484        &self.base_prop.id
485    }
486
487    fn serialize_adaptive_card(&self) -> serde_json::Value {
488        let mut map = self.base_prop.serialize();
489        map.insert("type".into(), json!("Input.ChoiceSet"));
490        map.insert(
491            "choices".into(),
492            json!(
493                self.choices
494                    .iter()
495                    .map(|c| {
496                        json!({
497                            "value": c.value(),
498                            "title": c.title(),
499                        })
500                    })
501                    .collect::<Vec<_>>()
502            ),
503        );
504        if let Some(value) = self.value.lock().ok().and_then(|v| (*v).clone()) {
505            map.insert("value".into(), json!(value.value()));
506        }
507        json!(map)
508    }
509
510    fn update(&self, data: &Map<String, JsonValue>) {
511        if let Some(value) = data
512            .get(self.id())
513            .and_then(|v| v.as_str().map(|s| s.to_string()))
514        {
515            if let Some(choice) = self.choices.iter().find(|c| c.value() == value) {
516                self.value
517                    .lock()
518                    .ok()
519                    .map(|mut v| v.replace(choice.clone()));
520            }
521        }
522    }
523
524    fn serialize_value(&self) -> Option<serde_json::Value> {
525        self.value
526            .lock()
527            .ok()
528            .and_then(|v| v.clone())
529            .map(|v| json!(v.value()))
530    }
531}
532
533trait ValueLock {
534    type Value: Clone + 'static;
535    fn value_lock(&self) -> Arc<Mutex<Option<Self::Value>>>;
536}
537
538impl ValueLock for TextSetting {
539    type Value = String;
540    fn value_lock(&self) -> Arc<Mutex<Option<Self::Value>>> {
541        self.value.clone()
542    }
543}
544
545impl ValueLock for NumberSetting {
546    type Value = f64;
547    fn value_lock(&self) -> Arc<Mutex<Option<Self::Value>>> {
548        self.value.clone()
549    }
550}
551
552impl ValueLock for ToggleSetting {
553    type Value = bool;
554    fn value_lock(&self) -> Arc<Mutex<Option<Self::Value>>> {
555        self.value.clone()
556    }
557}
558
559impl<T: Choice> ValueLock for ChoiceSetSetting<T> {
560    type Value = T;
561    fn value_lock(&self) -> Arc<Mutex<Option<Self::Value>>> {
562        self.value.clone()
563    }
564}
565
566/// A detailed implementation of the [`ICommandSettings`] interface which preserves config in a JSON file.
567///
568/// This struct automatically handles read and write of JSON settings file,
569/// and exposes `Arc<Mutex<Option<T>>>` for each setting item for developer to access the value.
570#[implement(ICommandSettings)]
571#[derive(Clone)]
572pub struct JsonCommandSettings {
573    path: std::path::PathBuf,
574    settings: Vec<Arc<dyn SettingItem + Send + Sync>>,
575    page: ComObject<ContentPage>,
576}
577
578impl JsonCommandSettings {
579    /// Creates a new `JsonCommandSettings` with the given path.
580    ///
581    /// The path should point to a JSON file where the settings will be stored.
582    /// If the file does not exist, it will be created.
583    ///
584    /// It will also attempt to create parent directories when writing the config file,
585    /// if they do not exist.
586    pub fn new(path: std::path::PathBuf) -> Self {
587        use crate::cmd::BaseCommandBuilder;
588        use crate::icon::{IconData, IconInfo};
589        use crate::page::BasePageBuilder;
590        use crate::page::content::ContentPageBuilder;
591        let page = ContentPageBuilder::new(
592            BasePageBuilder::new(
593                BaseCommandBuilder::new()
594                    .name("Settings")
595                    .icon(IconInfo::new(IconData::from("\u{E713}")))
596                    .build(),
597            )
598            .build(),
599        )
600        .build();
601        Self {
602            path,
603            settings: Vec::new(),
604            page,
605        }
606    }
607
608    /// Adds a setting item to the settings page.
609    ///
610    /// Valid setting items are:
611    /// - [`TextSetting`]
612    /// - [`NumberSetting`]
613    /// - [`ToggleSetting`]
614    /// - [`ChoiceSetSetting`]
615    ///
616    /// Returns an `Arc<Mutex<Option<V>>>` that can be used to access the real-time value of the setting.
617    #[allow(private_bounds, reason = "Trait bounds are only for internal use")]
618    pub fn add_setting<
619        T: SettingItem + ValueLock<Value = V> + Send + Sync + 'static,
620        V: Clone + 'static,
621    >(
622        &mut self,
623        setting: T,
624    ) -> Arc<Mutex<Option<V>>> {
625        let lock = setting.value_lock();
626        self.settings.push(Arc::new(setting));
627        lock
628    }
629
630    fn template_json(&self) -> JsonValue {
631        let body: Vec<_> = self
632            .settings
633            .iter()
634            .map(|s| s.serialize_adaptive_card())
635            .collect();
636        let mut keys: Map<String, JsonValue> = Map::new();
637        for id in self.settings.iter().map(|s| s.id()) {
638            keys.insert(id.to_string(), json!(id));
639        }
640
641        let v = json!({
642          "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
643          "type": "AdaptiveCard",
644          "version": "1.5",
645          "body": body,
646          "actions": [
647            {
648              "type": "Action.Submit",
649        "title": "Save",
650              "data": keys
651            }
652          ]
653        });
654        v
655    }
656
657    fn read_settings(&self) -> Option<()> {
658        let data = std::fs::read_to_string(&self.path).ok()?;
659        let data: Map<String, JsonValue> = serde_json::from_str(&data).ok()?;
660        for setting in self.settings.iter() {
661            setting.update(&data);
662        }
663        Some(())
664    }
665
666    fn write_settings(&self) -> Option<()> {
667        let mut data = Map::new();
668        for setting in self.settings.iter() {
669            if let Some(value) = setting.serialize_value() {
670                data.insert(setting.id().to_string(), value);
671            }
672        }
673        let json = serde_json::to_string(&data).ok()?;
674        let path = self.path.canonicalize().ok()?;
675        std::fs::create_dir_all(path.parent()?).ok()?;
676        std::fs::write(&path, json).ok()?;
677        Some(())
678    }
679}
680
681impl ICommandSettings_Impl for JsonCommandSettings_Impl {
682    fn SettingsPage(&self) -> windows_core::Result<IContentPage> {
683        use crate::cmd_result::CommandResult;
684        use crate::content::FormContentBuilder;
685        let slf = self.this.clone();
686        slf.read_settings();
687        let form = FormContentBuilder::new()
688            .template_json(
689                serde_json::to_string(&slf.template_json())
690                    .map_err(|e| Error::new(E_FAIL, e.to_string()))?,
691            )
692            .submit(move |form, input, _| {
693                let data: Map<String, JsonValue> =
694                    serde_json::from_str(&input.to_string_lossy())
695                        .map_err(|e| Error::new(E_FAIL, e.to_string()))?;
696                for setting in slf.settings.iter() {
697                    setting.update(&data);
698                }
699
700                slf.write_settings().ok_or(Error::new(
701                    ERROR_FILE_INVALID.to_hresult(),
702                    format!("Failed to write settings to {}", slf.path.display()),
703                ))?;
704
705                let new_template = serde_json::to_string(&slf.template_json())
706                    .map_err(|e| Error::new(E_FAIL, e.to_string()))?;
707                let mut guard = form.template_json_mut()?;
708                *guard = new_template.into();
709                drop(guard);
710                slf.page.emit_items_changed(slf.page.to_interface(), -1);
711                Ok(CommandResult::GoHome)
712            })
713            .build();
714
715        let mut guard = self.page.contents_mut()?;
716        *guard = vec![form.into()];
717        drop(guard);
718
719        Ok(self.page.to_interface())
720    }
721}