Skip to main content

gitkraft_core/features/persistence/
ops.rs

1//! Persistence operations backed by a plain JSON file.
2//!
3//! Settings are stored at `~/.config/gitkraft/settings.json` (or the
4//! platform-appropriate config directory).  Writes are **atomic**: content is
5//! first written to a `NamedTempFile` in the same directory and then
6//! `persist()`-ed into place, so a crash mid-write can never produce a
7//! corrupted file and no stale `.tmp` file is ever left behind.
8//!
9//! GUI settings are stored in `settings.json`; TUI settings are stored in
10//! `tui-settings.json`.  The two files are independent so each UI can evolve
11//! its own preferences (theme index, session, etc.) without stomping the other.
12
13use super::types::{AppSettings, RepoHistoryEntry};
14use anyhow::{Context, Result};
15use std::path::{Path, PathBuf};
16
17// ── Path helpers ──────────────────────────────────────────────────────────────
18
19/// Returns the settings directory (`~/.config/gitkraft/` or equivalent).
20pub fn settings_dir() -> Result<PathBuf> {
21    let base = dirs::config_dir().context("could not determine config directory")?;
22    Ok(base.join("gitkraft"))
23}
24
25/// Full path to the GUI JSON settings file (public so frontends can open it in an editor).
26pub fn settings_json_path() -> Result<PathBuf> {
27    Ok(settings_dir()?.join("settings.json"))
28}
29
30/// Full path to the TUI-specific JSON settings file (public so the TUI can open it in an editor).
31pub fn tui_settings_json_path() -> Result<PathBuf> {
32    Ok(settings_dir()?.join("tui-settings.json"))
33}
34
35/// Full path to the GUI JSON settings file.
36fn json_path() -> Result<PathBuf> {
37    settings_json_path()
38}
39
40/// Full path to the TUI-specific JSON settings file.
41fn tui_json_path() -> Result<PathBuf> {
42    tui_settings_json_path()
43}
44
45// ── Internal I/O helpers ──────────────────────────────────────────────────────
46
47/// Load settings from any JSON path (internal).
48fn load_from(path: &std::path::Path) -> Result<AppSettings> {
49    if path.exists() {
50        let content = std::fs::read_to_string(path)
51            .with_context(|| format!("failed to read {}", path.display()))?;
52        return match serde_json::from_str::<AppSettings>(&content) {
53            Ok(s) => Ok(s),
54            Err(e) => {
55                tracing::warn!(
56                    "settings file {:?} is malformed ({e}); using defaults",
57                    path
58                );
59                Ok(AppSettings::default())
60            }
61        };
62    }
63    Ok(AppSettings::default())
64}
65
66/// Save settings to any JSON path (internal, atomic write via NamedTempFile).
67fn save_to(path: &std::path::Path, settings: &AppSettings) -> Result<()> {
68    use std::io::Write as _;
69
70    let parent = path.parent().unwrap_or(std::path::Path::new("."));
71    std::fs::create_dir_all(parent)
72        .with_context(|| format!("failed to create directory {}", parent.display()))?;
73
74    let content = serde_json::to_string_pretty(settings).context("failed to serialise settings")?;
75
76    // Write to a NamedTempFile in the same directory so that persist() is an
77    // atomic rename on all platforms.  The temp file is auto-deleted on drop
78    // if persist() never succeeds, eliminating stale .tmp files after crashes.
79    let mut tmp = tempfile::NamedTempFile::new_in(parent)
80        .context("failed to create temporary settings file")?;
81    tmp.write_all(content.as_bytes())
82        .context("failed to write settings to temporary file")?;
83    tmp.persist(path)
84        .map_err(|e| anyhow::anyhow!("failed to persist settings file: {}", e))?;
85
86    Ok(())
87}
88
89// ── GUI settings (settings.json) ─────────────────────────────────────────────
90
91/// Load GUI application settings.
92///
93/// Returns `AppSettings::default()` when the file does not exist yet (first run)
94/// or when the file is malformed (file is preserved for manual recovery).
95pub fn load_settings() -> Result<AppSettings> {
96    load_from(&json_path()?)
97}
98
99/// Persist GUI application settings to `settings.json` using an atomic write.
100pub fn save_settings(settings: &AppSettings) -> Result<()> {
101    save_to(&json_path()?, settings)
102}
103
104/// Apply a closure to GUI settings and persist the result in a single
105/// read-modify-write cycle.  Avoids duplicating the load → mutate → save
106/// pattern across every individual setting helper.
107pub fn update_settings<F: FnOnce(&mut AppSettings)>(f: F) -> Result<()> {
108    let mut settings = load_settings()?;
109    f(&mut settings);
110    save_settings(&settings)
111}
112
113/// Record that a repository was opened (updates history + `last_repo`).
114pub fn record_repo_opened(path: &Path) -> Result<()> {
115    update_settings(|s| s.add_recent_repo(path.to_path_buf()))
116}
117
118/// Return the last opened repository path, if any.
119pub fn get_last_repo() -> Result<Option<PathBuf>> {
120    Ok(load_settings()?.last_repo)
121}
122
123/// Persist the selected theme name.
124pub fn save_theme(theme_name: &str) -> Result<()> {
125    update_settings(|s| s.theme_name = Some(theme_name.to_string()))
126}
127
128/// Return the saved theme name, if any.
129pub fn get_saved_theme() -> Result<Option<String>> {
130    Ok(load_settings()?.theme_name)
131}
132
133/// Persist the selected editor name.
134pub fn save_editor(editor_name: &str) -> Result<()> {
135    update_settings(|s| s.editor_name = Some(editor_name.to_string()))
136}
137
138/// Return the saved editor name, if any.
139pub fn get_saved_editor() -> Result<Option<String>> {
140    Ok(load_settings()?.editor_name)
141}
142
143/// Persist layout preferences.
144pub fn save_layout(layout: &super::types::LayoutSettings) -> Result<()> {
145    let layout = layout.clone();
146    update_settings(|s| s.layout = Some(layout))
147}
148
149/// Return saved layout preferences, if any.
150pub fn get_saved_layout() -> Result<Option<super::types::LayoutSettings>> {
151    Ok(load_settings()?.layout)
152}
153
154/// Record that a repo was opened AND update the open-tab session in a single
155/// write (saves one round-trip to disk).
156pub fn record_repo_and_save_session(
157    path: &Path,
158    open_tabs: &[PathBuf],
159    active_tab_index: usize,
160) -> Result<Vec<RepoHistoryEntry>> {
161    let mut settings = load_settings()?;
162    settings.add_recent_repo(path.to_path_buf());
163    settings.open_tabs = open_tabs.to_vec();
164    settings.active_tab_index = active_tab_index;
165    save_settings(&settings)?;
166    Ok(settings.recent_repos)
167}
168
169/// Persist the open-tab session without modifying the recent-repos list.
170pub fn save_session(open_tabs: &[PathBuf], active_tab_index: usize) -> Result<()> {
171    update_settings(|s| {
172        s.open_tabs = open_tabs.to_vec();
173        s.active_tab_index = active_tab_index;
174    })
175}
176
177// ── TUI settings (tui-settings.json) ─────────────────────────────────────────
178
179/// Load TUI application settings from `tui-settings.json`.
180pub fn load_tui_settings() -> Result<AppSettings> {
181    let mut settings = load_from(&tui_json_path()?)?;
182    // If the TUI has no editor configured, fall back to the GUI's editor
183    // setting so the user only has to configure their editor once.
184    if settings.editor_name.is_none() {
185        if let Ok(gui) = load_from(&json_path()?) {
186            if gui.editor_name.is_some() {
187                settings.editor_name = gui.editor_name;
188            }
189        }
190    }
191    Ok(settings)
192}
193
194/// Persist TUI application settings to `tui-settings.json` using an atomic write.
195pub fn save_tui_settings(settings: &AppSettings) -> Result<()> {
196    save_to(&tui_json_path()?, settings)
197}
198
199/// Apply a closure to TUI settings and persist the result in a single
200/// read-modify-write cycle.
201///
202/// Loads the raw stored settings (without the editor-fallback inheritance used
203/// by [`load_tui_settings`]) to avoid accidentally overwriting an explicit
204/// editor choice made via the GUI.
205pub fn update_tui_settings<F: FnOnce(&mut AppSettings)>(f: F) -> Result<()> {
206    let mut settings = load_from(&tui_json_path()?)?;
207    f(&mut settings);
208    save_tui_settings(&settings)
209}
210
211/// Record that a repository was opened in the TUI.
212pub fn record_repo_opened_tui(path: &std::path::Path) -> Result<()> {
213    update_tui_settings(|s| s.add_recent_repo(path.to_path_buf()))
214}
215
216/// Return the last TUI-opened repository path, if any.
217pub fn get_last_tui_repo() -> Result<Option<PathBuf>> {
218    Ok(load_tui_settings()?.last_repo)
219}
220
221/// Persist the TUI theme selection.
222pub fn save_theme_tui(theme_name: &str) -> Result<()> {
223    update_tui_settings(|s| s.theme_name = Some(theme_name.to_string()))
224}
225
226/// Persist the TUI editor selection.
227pub fn save_editor_tui(editor_name: &str) -> Result<()> {
228    update_tui_settings(|s| s.editor_name = Some(editor_name.to_string()))
229}
230
231/// Persist the TUI open-tab session.
232pub fn save_session_tui(open_tabs: &[PathBuf], active_tab_index: usize) -> Result<()> {
233    update_tui_settings(|s| {
234        s.open_tabs = open_tabs.to_vec();
235        s.active_tab_index = active_tab_index;
236    })
237}
238
239// ── Tests ─────────────────────────────────────────────────────────────────────
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use tempfile::TempDir;
245
246    // ── In-process helpers (bypass dirs::config_dir) ──────────────────────────
247
248    /// Write settings via the real `save_to` implementation so helpers
249    /// automatically exercise the `NamedTempFile` atomic-write path.
250    fn write_json(dir: &TempDir, settings: &AppSettings) {
251        let path = dir.path().join("settings.json");
252        save_to(&path, settings).unwrap();
253    }
254
255    fn read_json(dir: &TempDir) -> AppSettings {
256        let path = dir.path().join("settings.json");
257        let content = std::fs::read_to_string(&path).unwrap();
258        serde_json::from_str(&content).unwrap()
259    }
260
261    // ── AppSettings serde round-trip ──────────────────────────────────────────
262
263    #[test]
264    fn settings_json_round_trip() {
265        let dir = TempDir::new().unwrap();
266        let mut s = AppSettings {
267            theme_name: Some("Dracula".to_string()),
268            editor_name: Some("code".to_string()),
269            ..Default::default()
270        };
271        s.add_recent_repo(PathBuf::from("/tmp/repo-a"));
272        s.add_recent_repo(PathBuf::from("/tmp/repo-b"));
273
274        write_json(&dir, &s);
275        let loaded = read_json(&dir);
276
277        assert_eq!(loaded.theme_name, Some("Dracula".to_string()));
278        assert_eq!(loaded.editor_name, Some("code".to_string()));
279        assert_eq!(loaded.recent_repos.len(), 2);
280        assert_eq!(loaded.recent_repos[0].path, PathBuf::from("/tmp/repo-b"));
281        assert_eq!(loaded.recent_repos[1].path, PathBuf::from("/tmp/repo-a"));
282    }
283
284    #[test]
285    fn settings_json_preserves_open_tabs_and_active_index() {
286        let dir = TempDir::new().unwrap();
287        let s = AppSettings {
288            open_tabs: vec![PathBuf::from("/tmp/repo-1"), PathBuf::from("/tmp/repo-2")],
289            active_tab_index: 1,
290            ..Default::default()
291        };
292
293        write_json(&dir, &s);
294        let loaded = read_json(&dir);
295
296        assert_eq!(loaded.open_tabs.len(), 2);
297        assert_eq!(loaded.active_tab_index, 1);
298    }
299
300    #[test]
301    fn settings_json_preserves_layout() {
302        let dir = TempDir::new().unwrap();
303        let s = AppSettings {
304            layout: Some(super::super::types::LayoutSettings {
305                sidebar_width: Some(220.0),
306                commit_log_width: Some(400.0),
307                staging_height: Some(150.0),
308                diff_file_list_width: Some(180.0),
309                sidebar_expanded: Some(true),
310                ui_scale: Some(1.25),
311                ..Default::default()
312            }),
313            ..Default::default()
314        };
315
316        write_json(&dir, &s);
317        let loaded = read_json(&dir);
318        let layout = loaded.layout.unwrap();
319
320        assert!((layout.sidebar_width.unwrap() - 220.0).abs() < f32::EPSILON);
321        assert_eq!(layout.sidebar_expanded, Some(true));
322        assert!((layout.ui_scale.unwrap() - 1.25).abs() < f32::EPSILON);
323    }
324
325    #[test]
326    fn malformed_json_deserialises_to_defaults() {
327        let dir = TempDir::new().unwrap();
328        let path = dir.path().join("settings.json");
329        // Write garbage — simulates a half-written file from a crash.
330        std::fs::write(&path, b"{ this is not valid json !!!").unwrap();
331
332        // serde_json::from_str should fail; caller should get AppSettings::default().
333        let result = serde_json::from_str::<AppSettings>(&std::fs::read_to_string(&path).unwrap());
334        assert!(
335            result.is_err(),
336            "malformed JSON must not parse successfully"
337        );
338        // The file must still exist (we must not delete it).
339        assert!(path.exists(), "malformed file must be preserved");
340    }
341
342    #[test]
343    fn atomic_write_produces_no_tmp_file_on_success() {
344        let dir = TempDir::new().unwrap();
345        let s = AppSettings::default();
346        write_json(&dir, &s);
347
348        let tmp = dir.path().join("settings.json.tmp");
349        assert!(
350            !tmp.exists(),
351            "tmp file must be removed after a successful atomic write"
352        );
353        assert!(dir.path().join("settings.json").exists());
354    }
355
356    #[test]
357    fn serde_default_missing_fields_load_cleanly() {
358        // A JSON object with only known fields — new fields added in future
359        // versions should not break loading older settings files.
360        let dir = TempDir::new().unwrap();
361        let minimal = r#"{"last_repo": null, "recent_repos": [], "theme_name": "Nord"}"#;
362        std::fs::write(dir.path().join("settings.json"), minimal).unwrap();
363
364        let loaded = read_json(&dir);
365        assert_eq!(loaded.theme_name, Some("Nord".to_string()));
366        assert_eq!(loaded.max_recent, 20); // default
367        assert_eq!(loaded.active_tab_index, 0); // default
368        assert!(loaded.open_tabs.is_empty()); // default
369    }
370
371    // ── AppSettings helper logic ──────────────────────────────────────────────
372
373    #[test]
374    fn add_recent_deduplicates() {
375        let mut settings = AppSettings::default();
376        settings.add_recent_repo(PathBuf::from("/tmp/repo1"));
377        settings.add_recent_repo(PathBuf::from("/tmp/repo2"));
378        settings.add_recent_repo(PathBuf::from("/tmp/repo1"));
379        assert_eq!(settings.recent_repos.len(), 2);
380        assert_eq!(settings.recent_repos[0].path, PathBuf::from("/tmp/repo1"));
381    }
382
383    #[test]
384    fn add_recent_respects_max() {
385        let mut settings = AppSettings {
386            max_recent: 3,
387            ..Default::default()
388        };
389        for i in 0..5 {
390            settings.add_recent_repo(PathBuf::from(format!("/tmp/repo{i}")));
391        }
392        assert_eq!(settings.recent_repos.len(), 3);
393    }
394
395    #[test]
396    fn settings_round_trip_via_json_bytes() {
397        let mut settings = AppSettings::default();
398        settings.add_recent_repo(PathBuf::from("/tmp/repo1"));
399        settings.add_recent_repo(PathBuf::from("/tmp/repo2"));
400        settings.theme_name = Some("Dark".to_string());
401
402        let json = serde_json::to_string(&settings).unwrap();
403        let decoded: AppSettings = serde_json::from_str(&json).unwrap();
404        assert_eq!(decoded.recent_repos.len(), 2);
405        assert_eq!(decoded.theme_name, Some("Dark".to_string()));
406    }
407
408    // ── TUI path tests ────────────────────────────────────────────────────────
409
410    #[test]
411    fn tui_and_gui_settings_are_independent() {
412        // Verify the path names differ so they won't overwrite each other.
413        let gui = json_path().unwrap();
414        let tui = tui_json_path().unwrap();
415        assert_ne!(gui, tui);
416        assert!(gui.to_str().unwrap().ends_with("settings.json"));
417        assert!(tui.to_str().unwrap().ends_with("tui-settings.json"));
418    }
419
420    #[test]
421    fn load_tui_inherits_editor_from_gui_when_tui_has_none() {
422        let dir = TempDir::new().unwrap();
423
424        // Write GUI settings with an editor configured.
425        let gui = AppSettings {
426            editor_name: Some("Helix".to_string()),
427            ..Default::default()
428        };
429        write_json(&dir, &gui);
430
431        // TUI settings exist but have no editor_name.
432        let tui_path = dir.path().join("tui-settings.json");
433        let tui_content = r#"{"last_repo":null,"recent_repos":[],"theme_name":null}"#;
434        std::fs::write(&tui_path, tui_content).unwrap();
435
436        // load_from on just the TUI file gives no editor.
437        let tui_raw = load_from(&tui_path).unwrap();
438        assert!(tui_raw.editor_name.is_none());
439
440        // The fallback logic (mirroring load_tui_settings) should pick up the
441        // GUI editor when TUI has none.
442        let gui_loaded = load_from(&dir.path().join("settings.json")).unwrap();
443        let mut merged = tui_raw;
444        if merged.editor_name.is_none() {
445            merged.editor_name = gui_loaded.editor_name;
446        }
447        assert_eq!(merged.editor_name.as_deref(), Some("Helix"));
448    }
449
450    #[test]
451    fn load_tui_keeps_own_editor_when_configured() {
452        let dir = TempDir::new().unwrap();
453
454        // GUI has one editor, TUI has a different one.
455        let gui = AppSettings {
456            editor_name: Some("VS Code".to_string()),
457            ..Default::default()
458        };
459        write_json(&dir, &gui);
460
461        let tui_path = dir.path().join("tui-settings.json");
462        let tui_content = r#"{"last_repo":null,"recent_repos":[],"editor_name":"Neovim"}"#;
463        std::fs::write(&tui_path, tui_content).unwrap();
464
465        let tui_raw = load_from(&tui_path).unwrap();
466        assert_eq!(tui_raw.editor_name.as_deref(), Some("Neovim"));
467
468        // When TUI already has an editor, the GUI value must not override it.
469        let gui_loaded = load_from(&dir.path().join("settings.json")).unwrap();
470        let mut merged = tui_raw;
471        if merged.editor_name.is_none() {
472            merged.editor_name = gui_loaded.editor_name;
473        }
474        assert_eq!(merged.editor_name.as_deref(), Some("Neovim"));
475    }
476
477    #[test]
478    fn load_tui_settings_returns_default_when_no_file() {
479        // Can't easily control the real config dir in unit tests, but we can
480        // verify load_from works correctly with a nonexistent path.
481        let tmp = std::path::Path::new("/nonexistent/path/that/does/not/exist.json");
482        let result = load_from(tmp).unwrap();
483        assert_eq!(result.theme_name, None);
484        assert!(result.recent_repos.is_empty());
485    }
486
487    // ── save_to: NamedTempFile atomic-write guarantees ──────────────────────────
488
489    #[test]
490    fn save_to_leaves_no_temp_files() {
491        // After save_to completes there must be NO file in the directory
492        // whose extension ends with `tmp`, proving NamedTempFile cleaned up.
493        let dir = TempDir::new().unwrap();
494        let path = dir.path().join("settings.json");
495        save_to(&path, &AppSettings::default()).unwrap();
496
497        let leftovers: Vec<_> = std::fs::read_dir(dir.path())
498            .unwrap()
499            .filter_map(|e| e.ok())
500            .filter(|e| {
501                e.path()
502                    .extension()
503                    .and_then(|x| x.to_str())
504                    .map(|ext| ext.contains("tmp"))
505                    .unwrap_or(false)
506            })
507            .collect();
508
509        assert!(
510            leftovers.is_empty(),
511            "save_to must not leave any temp files: {leftovers:?}"
512        );
513        assert!(path.exists(), "settings file must exist after save_to");
514    }
515
516    #[test]
517    fn save_to_creates_parent_directories_automatically() {
518        let dir = TempDir::new().unwrap();
519        let nested = dir.path().join("a").join("b").join("settings.json");
520        // Parent dirs do not exist yet—save_to must create them.
521        save_to(&nested, &AppSettings::default()).unwrap();
522        assert!(nested.exists());
523    }
524
525    #[test]
526    fn save_to_and_load_from_round_trip() {
527        let dir = TempDir::new().unwrap();
528        let path = dir.path().join("settings.json");
529
530        let mut s = AppSettings {
531            theme_name: Some("Nord".to_string()),
532            editor_name: Some("Neovim".to_string()),
533            ..Default::default()
534        };
535        s.add_recent_repo(PathBuf::from("/tmp/my-repo"));
536
537        save_to(&path, &s).unwrap();
538        let loaded = load_from(&path).unwrap();
539
540        assert_eq!(loaded.theme_name, Some("Nord".to_string()));
541        assert_eq!(loaded.editor_name, Some("Neovim".to_string()));
542        assert_eq!(loaded.recent_repos.len(), 1);
543        assert_eq!(loaded.recent_repos[0].path, PathBuf::from("/tmp/my-repo"));
544    }
545
546    // ── update_settings combinator ──────────────────────────────────────────
547
548    /// Test the combinator via the underlying `save_to` / `load_from`
549    /// primitives (bypasses `dirs::config_dir` which is not controllable
550    /// in unit tests).
551    #[test]
552    fn update_via_save_to_load_from_preserves_other_fields() {
553        let dir = TempDir::new().unwrap();
554        let path = dir.path().join("settings.json");
555
556        // Write initial state with an editor and a recent repo.
557        let mut initial = AppSettings {
558            editor_name: Some("Vim".to_string()),
559            ..Default::default()
560        };
561        initial.add_recent_repo(PathBuf::from("/tmp/repo1"));
562        save_to(&path, &initial).unwrap();
563
564        // Simulate what update_settings does: load → mutate → save.
565        let mut s = load_from(&path).unwrap();
566        s.theme_name = Some("Dracula".to_string());
567        save_to(&path, &s).unwrap();
568
569        let loaded = load_from(&path).unwrap();
570
571        // New field was written.
572        assert_eq!(loaded.theme_name, Some("Dracula".to_string()));
573        // Pre-existing fields were NOT overwritten.
574        assert_eq!(loaded.editor_name, Some("Vim".to_string()));
575        assert_eq!(loaded.recent_repos.len(), 1);
576    }
577
578    #[test]
579    fn update_via_save_to_multiple_mutations_in_one_write() {
580        let dir = TempDir::new().unwrap();
581        let path = dir.path().join("settings.json");
582        save_to(&path, &AppSettings::default()).unwrap();
583
584        // A single load → closure → save call touches multiple fields.
585        let mut s = load_from(&path).unwrap();
586        s.theme_name = Some("Monokai".to_string());
587        s.editor_name = Some("Helix".to_string());
588        s.active_tab_index = 2;
589        save_to(&path, &s).unwrap();
590
591        let loaded = load_from(&path).unwrap();
592        assert_eq!(loaded.theme_name, Some("Monokai".to_string()));
593        assert_eq!(loaded.editor_name, Some("Helix".to_string()));
594        assert_eq!(loaded.active_tab_index, 2);
595    }
596
597    // ── update_tui_settings: raw-load avoids editor-fallback overwrite ────────
598
599    /// `update_tui_settings` must load from the raw TUI file (not via
600    /// `load_tui_settings`), so the GUI’s editor is never silently written
601    /// back into the TUI file.
602    #[test]
603    fn update_tui_settings_does_not_inherit_gui_editor() {
604        let dir = TempDir::new().unwrap();
605        let gui_path = dir.path().join("settings.json");
606        let tui_path = dir.path().join("tui-settings.json");
607
608        // GUI has an editor; TUI file has none.
609        let gui = AppSettings {
610            editor_name: Some("VS Code".to_string()),
611            ..Default::default()
612        };
613        save_to(&gui_path, &gui).unwrap();
614        save_to(&tui_path, &AppSettings::default()).unwrap();
615
616        // Simulate update_tui_settings: load raw (no fallback) → mutate → save.
617        let mut s = load_from(&tui_path).unwrap();
618        s.theme_name = Some("Nord".to_string());
619        save_to(&tui_path, &s).unwrap();
620
621        // The TUI file must have the new theme but editor_name must remain None.
622        let tui_loaded = load_from(&tui_path).unwrap();
623        assert_eq!(tui_loaded.theme_name, Some("Nord".to_string()));
624        assert!(
625            tui_loaded.editor_name.is_none(),
626            "update_tui_settings must not inherit the GUI editor into the TUI file"
627        );
628    }
629}