Skip to main content

qcli/app/
form.rs

1//! The provider form: the four settings a provider needs, checked before they are written.
2
3use qframe::prelude::*;
4use qframe::widgets::{Field as FormFieldWidget, Form, Modal, TextInput};
5
6use super::Msg;
7use crate::config::Provider;
8use crate::provider::{Key, KeyError};
9
10/// The id of the form's first field, which has focus when the form opens.
11pub const FIRST: &str = "provider-address";
12
13/// The fields of the form.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Field {
16    /// The address.
17    Address,
18    /// The model.
19    Model,
20    /// The key file's path.
21    KeyFile,
22    /// The header the key goes in.
23    KeyHeader,
24}
25
26impl Field {
27    const ALL: [Self; 4] = [Self::Address, Self::Model, Self::KeyFile, Self::KeyHeader];
28
29    fn id(self) -> &'static str {
30        match self {
31            Self::Address => FIRST,
32            Self::Model => "provider-model",
33            Self::KeyFile => "provider-key-file",
34            Self::KeyHeader => "provider-key-header",
35        }
36    }
37
38    fn label(self) -> String {
39        match self {
40            Self::Address => t!("provider.address"),
41            Self::Model => t!("provider.model"),
42            Self::KeyFile => t!("provider.key-file"),
43            Self::KeyHeader => t!("provider.key-header"),
44        }
45    }
46
47    fn hint(self) -> String {
48        match self {
49            Self::Address => t!("provider.address-hint"),
50            Self::Model => t!("provider.model-hint"),
51            Self::KeyFile => t!("provider.key-file-hint"),
52            Self::KeyHeader => t!("provider.key-header-hint"),
53        }
54    }
55}
56
57/// What is wrong with a field after Save was pressed.
58#[derive(Debug, Clone, PartialEq, Eq)]
59enum Problem {
60    /// It is empty and must not be.
61    Empty,
62    /// The key file gave no key.
63    Key(KeyError),
64    /// The settings could not be written.
65    Unsaved(String),
66}
67
68/// The form's values while it is open.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ProviderForm {
71    provider: Provider,
72    problems: Vec<(Field, Problem)>,
73}
74
75impl ProviderForm {
76    /// The form filled with `provider`.
77    #[must_use]
78    pub fn from_provider(provider: &Provider) -> Self {
79        Self { provider: provider.clone(), problems: Vec::new() }
80    }
81
82    /// Changes one field.
83    pub fn set(&mut self, field: Field, value: String) {
84        match field {
85            Field::Address => self.provider.address = value,
86            Field::Model => self.provider.model = value,
87            Field::KeyFile => self.provider.key_file = value,
88            Field::KeyHeader => self.provider.key_header = value,
89        }
90    }
91
92    fn value(&self, field: Field) -> &str {
93        match field {
94            Field::Address => &self.provider.address,
95            Field::Model => &self.provider.model,
96            Field::KeyFile => &self.provider.key_file,
97            Field::KeyHeader => &self.provider.key_header,
98        }
99    }
100
101    /// The provider when every field holds what it must, the key file included; otherwise the
102    /// problems are kept for the form to show and there is nothing to save.
103    pub fn check(&mut self) -> Option<Provider> {
104        let provider = Provider {
105            address: self.provider.address.trim().to_owned(),
106            model: self.provider.model.trim().to_owned(),
107            key_file: self.provider.key_file.trim().to_owned(),
108            key_header: self.provider.key_header.trim().to_owned(),
109        };
110        self.problems = [Field::Address, Field::Model, Field::KeyFile]
111            .into_iter()
112            .filter(|field| {
113                let value = match field {
114                    Field::Address => &provider.address,
115                    Field::Model => &provider.model,
116                    _ => &provider.key_file,
117                };
118                value.is_empty()
119            })
120            .map(|field| (field, Problem::Empty))
121            .collect();
122        if !provider.key_file.is_empty()
123            && let Err(error) = Key::from_file(&provider.key_path())
124        {
125            self.problems.push((Field::KeyFile, Problem::Key(error)));
126        }
127        self.problems.is_empty().then_some(provider)
128    }
129
130    /// The id of the first field with a problem, for focus to go to after a failed save.
131    #[must_use]
132    pub fn first_problem(&self) -> Option<&'static str> {
133        Field::ALL.into_iter().find(|field| self.problems.iter().any(|(with, _)| with == field)).map(Field::id)
134    }
135
136    /// Says that the settings could not be written.
137    pub fn failed(&mut self, error: &std::io::Error) {
138        self.problems = vec![(Field::Address, Problem::Unsaved(error.to_string()))];
139    }
140
141    fn problem(&self, field: Field) -> Option<String> {
142        self.problems.iter().find(|(with, _)| *with == field).map(|(_, problem)| match problem {
143            Problem::Empty => t!("provider.empty"),
144            Problem::Key(KeyError::Unreadable { path, reason }) => {
145                t!("provider.key-unreadable", path = path.display().to_string(), reason = reason.clone())
146            }
147            Problem::Key(KeyError::NotAKey { path }) => t!("provider.not-a-key", path = path.display().to_string()),
148            Problem::Unsaved(reason) => t!("provider.unsaved", reason = reason.clone()),
149        })
150    }
151
152    /// Draws the form as a dialog over the chat.
153    pub fn view(&self, ui: &mut View<'_, Msg>) {
154        let dialog = Modal::new()
155            .title(t!("provider.title"))
156            .width(72)
157            .on_close(Msg::CloseProvider)
158            .action(Button::new(t!("provider.cancel")).on_press(Msg::CloseProvider))
159            .action(Button::new(t!("provider.save")).variant("primary").on_press(Msg::SaveProvider));
160        ui.add_with(dialog, |ui| {
161            ui.add(Text::new(t!("provider.intro")).role("secondary"));
162            Form::new().show(ui, |fields| {
163                for field in Field::ALL {
164                    let widget = FormFieldWidget::new(field.label())
165                        .required(field != Field::KeyHeader)
166                        .hint(field.hint())
167                        .error(self.problem(field));
168                    fields.field(widget, |ui| {
169                        ui.add(
170                            TextInput::new(self.value(field))
171                                .invalid(self.problem(field).is_some())
172                                .on_change(move |value| Msg::Form(field, value))
173                                .on_submit(|_| Msg::SaveProvider),
174                        )
175                        .id(field.id())
176                        .fill_width();
177                    });
178                }
179            });
180        });
181    }
182}