Coverage Report

Created: 2026-07-21 00:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-stt-daemon/src/daemon/settings_handlers.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
3
use crate::config::DaemonConfig;
4
use crate::daemon::types::SuperSTTDaemon;
5
use log::{info, warn};
6
use super_stt_shared::models::protocol::DaemonResponse;
7
use super_stt_shared::models::recording_stop_mode::RecordingStopMode;
8
use super_stt_shared::models::write_method::WriteMethod;
9
10
impl SuperSTTDaemon {
11
    /// Mutate the config under the write lock, then persist it. Centralizes the
12
    /// lock → mutate → persist sequence so a settings handler can't hand-roll it
13
    /// and forget the persist (see Tier 1 #3). Returns the persist outcome so the
14
    /// caller can fold a save failure into its response.
15
0
    async fn set_config_field<F>(&self, mutate: F) -> anyhow::Result<()>
16
0
    where
17
0
        F: FnOnce(&mut DaemonConfig),
18
0
    {
19
        {
20
0
            let mut config = self.config.write().await;
21
0
            mutate(&mut config);
22
        }
23
0
        self.persist_config().await
24
0
    }
25
26
    /// Fold a persist outcome into a settings response. `base` already carries
27
    /// the setting-specific `.with_*` field and `message` is the success text.
28
    /// A save failure keeps the (already-applied) in-memory change — the daemon
29
    /// stays authoritative for the process lifetime — and appends the error to
30
    /// the message, logging a warning.
31
0
    fn settings_saved(
32
0
        base: DaemonResponse,
33
0
        message: String,
34
0
        persist: anyhow::Result<()>,
35
0
    ) -> DaemonResponse {
36
0
        match persist {
37
0
            Ok(()) => base.with_message(message),
38
0
            Err(e) => {
39
0
                warn!("Setting changed but config save failed: {e}");
40
0
                base.with_message(format!("{message} (save failed: {e})"))
41
            }
42
        }
43
0
    }
44
    /// Handle set preview typing command - enable or disable preview typing
45
    #[must_use]
46
0
    pub async fn handle_set_preview_typing(&self, enabled: bool) -> DaemonResponse {
47
        // Update the in-memory setting.
48
0
        self.preview_typing_enabled
49
0
            .store(enabled, std::sync::atomic::Ordering::Relaxed);
50
51
0
        let persist = self
52
0
            .set_config_field(|c| c.transcription.preview_typing_enabled = enabled)
53
0
            .await;
54
55
0
        let state = if enabled { "enabled" } else { "disabled" };
56
0
        info!("Preview typing {state}");
57
0
        Self::settings_saved(
58
0
            DaemonResponse::success().with_preview_typing_enabled(enabled),
59
0
            format!("Preview typing {state}"),
60
0
            persist,
61
        )
62
0
    }
63
64
    /// Handle get preview typing command - return current preview typing setting
65
    #[must_use]
66
0
    pub fn handle_get_preview_typing(&self) -> DaemonResponse {
67
0
        let enabled = self
68
0
            .preview_typing_enabled
69
0
            .load(std::sync::atomic::Ordering::Relaxed);
70
71
0
        DaemonResponse::success()
72
0
            .with_preview_typing_enabled(enabled)
73
0
            .with_message("Preview typing setting retrieved successfully".to_string())
74
0
    }
75
76
    /// Handle set recording stop mode command
77
0
    pub async fn handle_set_recording_stop_mode(&self, mode: RecordingStopMode) -> DaemonResponse {
78
0
        let persist = self
79
0
            .set_config_field(|c| c.transcription.recording_stop_mode = mode)
80
0
            .await;
81
82
0
        info!("Recording stop mode set to {mode}");
83
0
        Self::settings_saved(
84
0
            DaemonResponse::success().with_recording_stop_mode(mode.to_string()),
85
0
            format!("Recording stop mode set to {mode}"),
86
0
            persist,
87
        )
88
0
    }
89
90
    /// Handle get recording stop mode command
91
0
    pub async fn handle_get_recording_stop_mode(&self) -> DaemonResponse {
92
0
        let config = self.config.read().await;
93
0
        let mode = config.transcription.recording_stop_mode;
94
0
        DaemonResponse::success().with_recording_stop_mode(mode.to_string())
95
0
    }
96
97
    /// Handle set write method command
98
0
    pub async fn handle_set_write_method(&self, method: WriteMethod) -> DaemonResponse {
99
0
        let persist = self
100
0
            .set_config_field(|c| c.transcription.write_method = method)
101
0
            .await;
102
        // Invalidate the cached simulator so the next recording creates a fresh one.
103
0
        *self.simulator.write().await = None;
104
105
0
        info!("Write method set to {method}");
106
0
        Self::settings_saved(
107
0
            DaemonResponse::success().with_write_method(method.to_string()),
108
0
            format!("Write method set to {method}"),
109
0
            persist,
110
        )
111
0
    }
112
113
    /// Handle get write method command
114
0
    pub async fn handle_get_write_method(&self) -> DaemonResponse {
115
0
        let config = self.config.read().await;
116
0
        let method = config.transcription.write_method;
117
0
        DaemonResponse::success().with_write_method(method.to_string())
118
0
    }
119
120
    /// Handle set allow online models command
121
8
    pub async fn handle_set_allow_online_models(&self, enabled: bool) -> DaemonResponse {
122
        {
123
8
            let mut config_guard = self.config.write().await;
124
8
            config_guard.online.allow_online_models = enabled;
125
        }
126
127
        // If disabling online models and the current model is online, revert to a
128
        // local one. Track why the revert didn't leave a usable local model so the
129
        // response doesn't falsely claim "all transcription is local".
130
8
        let mut revert_warning: Option<String> = None;
131
8
        if !enabled {
132
4
            let current_is_online = {
133
4
                let guard = self.model.read().await;
134
4
                guard
135
4
                    .as_ref()
136
4
                    .is_some_and(|loaded| 
loaded.definition2
.
is_online2
())
137
            };
138
4
            if current_is_online {
139
2
                info!("Online models disabled; reverting to a local model");
140
2
                if let Some((
name0
,
provider0
,
source0
)) = self.first_local_model().await {
141
0
                    let resp = self.handle_set_model(name, provider, source).await;
142
0
                    if resp.status != "success" {
143
0
                        warn!("Reverting to a local model failed; unloading the online model");
144
0
                        *self.model.write().await = None;
145
0
                        revert_warning = Some(format!(
146
                            "could not load a local model: {}",
147
0
                            resp.message.unwrap_or_else(|| "unknown error".to_string())
148
                        ));
149
0
                    }
150
                } else {
151
2
                    warn!("No local backend installed to revert to; unloading current model");
152
2
                    *self.model.write().await = None;
153
2
                    revert_warning = Some("no local backend installed to fall back to".to_string());
154
                }
155
2
            }
156
4
        }
157
158
8
        let persist_result = self.persist_config().await;
159
160
8
        match persist_result {
161
            Ok(()) => {
162
8
                info!(
163
                    "Online models {}",
164
0
                    if enabled { "enabled" } else { "disabled" }
165
                );
166
8
                let message = if enabled {
167
4
                    "Online models enabled — audio may be sent to external APIs".to_string()
168
                } else {
169
4
                    match &revert_warning {
170
2
                        Some(w) => format!("Online models disabled, but {w}"),
171
2
                        None => "Online models disabled — all transcription is local".to_string(),
172
                    }
173
                };
174
8
                DaemonResponse::success()
175
8
                    .with_allow_online_models(enabled)
176
8
                    .with_message(message)
177
            }
178
0
            Err(e) => {
179
0
                warn!("Online models setting changed but config save failed: {e}");
180
0
                let mut message = format!(
181
                    "Online models {} (config save failed: {e})",
182
0
                    if enabled { "enabled" } else { "disabled" }
183
                );
184
0
                if let Some(w) = &revert_warning {
185
0
                    message = format!("{message}; {w}");
186
0
                }
187
0
                DaemonResponse::success()
188
0
                    .with_allow_online_models(enabled)
189
0
                    .with_message(message)
190
            }
191
        }
192
8
    }
193
194
    /// Handle get allow online models command
195
2
    pub async fn handle_get_allow_online_models(&self) -> DaemonResponse {
196
2
        let config = self.config.read().await;
197
2
        let enabled = config.online.allow_online_models;
198
2
        DaemonResponse::success()
199
2
            .with_allow_online_models(enabled)
200
2
            .with_message("Allow online models setting retrieved".to_string())
201
2
    }
202
203
    /// Handle get custom models directory command
204
0
    pub async fn handle_get_custom_models_dir(&self) -> DaemonResponse {
205
0
        let path = self
206
0
            .config
207
0
            .read()
208
0
            .await
209
            .transcription
210
            .custom_models_dir
211
0
            .clone();
212
0
        DaemonResponse::success().with_custom_models_dir(path)
213
0
    }
214
215
    /// Handle set custom models directory command
216
0
    pub async fn handle_set_custom_models_dir(&self, path: Option<String>) -> DaemonResponse {
217
0
        let path_display = path.as_deref().unwrap_or("none").to_string();
218
219
0
        let persist = self
220
0
            .set_config_field(|c| c.transcription.custom_models_dir = path)
221
0
            .await;
222
223
0
        info!("Custom models directory set to {path_display}");
224
0
        Self::settings_saved(
225
0
            DaemonResponse::success(),
226
0
            format!("Custom models directory set to {path_display}"),
227
0
            persist,
228
        )
229
0
    }
230
}