Skip to main content

atmos/kernel/
config.rs

1//! OS 設定・テーマ(配色)の永続化と読み込み。
2//!
3//! [`ThemeConfig`] が1テーマ分の全配色(デスクトップ・ボタン・ログイン画面・システムモニタ等)
4//! を保持する。プリセットテーマは `colorset/*.json`(`default`/`dracula`/`earth`/`nord`/
5//! `sakura`)として同梱され、[`crate::apps::settings`] からカスタムテーマも JSON で
6//! SylFS に保存できる。キーボードレイアウト([`KeyboardLayout`])もここで管理する。
7// src/config.rs - OS Settings & Theme Configuration for AtmOS
8#![allow(dead_code)]
9#![allow(static_mut_refs)]
10
11extern crate alloc;
12use crate::kernel::fs;
13pub use crate::kernel::keyboard::KeyboardLayout;
14use crate::os_lib::json::{self, JsonValue};
15use alloc::collections::BTreeMap;
16use alloc::string::String;
17
18#[derive(Clone, Copy, Debug)]
19pub struct ThemeConfig {
20    pub name: &'static str,
21    pub desktop_bg: u32,
22    pub terminal_bg: u32,
23    pub terminal_fg: u32,
24    pub active_window_border: u32,
25    pub non_active_window_border: u32,
26    pub window_title_bar: u32,
27    pub window_title_fg: u32,
28    // --- GUI Detailed Colors ---
29    pub app_bg: u32,
30    pub app_fg: u32,
31    pub button_bg: u32,
32    pub button_fg: u32,
33    pub button_active_bg: u32,
34    pub button_active_fg: u32,
35    pub focus_highlight: u32,
36    // --- Mouse Cursor Theme Colors ---
37    pub mouse_cursor: u32,
38    pub mouse_stroke: u32,
39    // --- 追加:ログイン画面用 ---
40    pub login_screen_bg: u32,
41    pub login_dialog_bg: u32,
42    pub login_dialog_shadow: u32,
43    pub login_dialog_border: u32,
44    pub login_text_fg: u32,
45    pub login_field_bg: u32,
46    pub login_field_border: u32,
47    // --- 追加:システムモニター用 ---
48    pub sysmon_bg: u32,
49    pub sysmon_header_bg: u32,
50    pub sysmon_accent: u32,
51    pub sysmon_text: u32,
52    pub sysmon_value: u32,
53    pub sysmon_border: u32,
54    // --- 追加:タブエリア用 ---
55    pub tab_area_bg: u32,
56    pub tab_area_border: u32,
57    pub tab_active_bg: u32,
58    pub tab_active_fg: u32,
59    pub tab_inactive_fg: u32,
60    // --- 追加:ファンクションバー用 ---
61    pub fn_bar_bg: u32,
62    pub fn_bar_cell_bg: u32,
63    pub fn_bar_key_bg: u32,
64    pub fn_bar_key_fg: u32,
65    pub fn_bar_text_fg: u32,
66    pub fn_bar_dim_fg: u32,
67    pub fn_bar_border: u32,
68    // --- 追加:IME用 ---
69    pub ime_bg: u32,
70    pub ime_border: u32,
71    pub ime_text: u32,
72    // --- ウィンドウ制御ボタン ---
73    pub win_btn_close: u32,
74    pub win_btn_fullscreen: u32,
75    pub win_btn_maximize: u32,
76    pub win_btn_restore: u32,
77    pub win_btn_minimize: u32,
78    // --- OS共通 UI カラー ---
79    pub error_fg: u32,          // エラーテキスト(赤)
80    pub success_fg: u32,        // 成功・アクティブ表示(緑)
81    pub warning_fg: u32,        // 警告テキスト(オレンジ)
82    pub ui_overlay_bg: u32,     // オーバーレイ背景(暗め)
83    pub ui_overlay_accent: u32, // オーバーレイアクセントライン・フォーカス
84    pub ui_input_bg: u32,       // フォーム入力フィールド背景
85    pub browser_bg: u32,        // ブラウザ内部UI背景
86    pub browser_fg: u32,        // ブラウザ内部UIテキスト
87    pub browser_secondary: u32, // ブラウザ二次カラー(枠線・スクロールバー等)
88    pub login_caret: u32,       // ログイン入力カーソル
89    pub login_hint_fg: u32,     // ログインヒントテキスト
90    // --- スクロールバー ---
91    pub scrollbar_thumb: u32,   // スクロールバーのサム
92    // --- フォーム入力 (Webエンジン) ---
93    pub form_input_bg: u32,         // input/textarea/select 背景
94    pub form_input_fg: u32,         // input/textarea/select 文字色
95    pub form_input_border: u32,     // input/textarea/select 枠線
96    pub form_input_border_focus: u32, // フォーカス時枠線
97    pub form_input_accent: u32,     // select ▼ アイコン色
98    pub form_checkbox_checked: u32, // チェック時 fill 色
99    pub form_checkbox_unchecked: u32, // 未チェック時 fill 色
100    // --- ボタン デフォルトフォールバック (Webエンジン) ---
101    pub btn_default_bg: u32,         // CSSでbg未指定のbutton背景
102    pub btn_default_border: u32,     // 同 border
103    pub btn_hover_bg: u32,           // hover 時 button 背景
104    pub btn_hover_border: u32,       // hover 時 button 枠線
105    // --- テキストエリア・エディタ (Webエンジン) ---
106    pub editor_line_highlight: u32,  // カーソル行の下線色
107    pub editor_dim_fg: u32,          // 制御文字(スペース記号・改行記号等)の色
108    // --- 動画プレーヤー (Webエンジン) ---
109    pub video_bar_bg: u32,           // コントロールバー背景
110    pub video_bar_sep: u32,          // バー上部境界線
111    pub video_progress_fg: u32,      // プログレスバー色
112    pub video_thumb_fg: u32,         // プログレスのサムネイル色
113    // --- Privacy Shield バッジ (Webエンジン) ---
114    pub privacy_badge_bg: u32,       // バッジ背景
115    pub privacy_badge_fg: u32,       // バッジ文字色
116}
117
118pub static PRESET_THEMES: &[ThemeConfig] = &[
119    ThemeConfig {
120        name: "Earth Cream",
121        desktop_bg: 0xFFE5DFD3,
122        terminal_bg: 0xFFFDFBF7,
123        terminal_fg: 0xFF2F2A25,
124        active_window_border: 0xFF8F9779,
125        non_active_window_border: 0xFFC5BDB2,
126        window_title_bar: 0xFFBCAE9B,
127        window_title_fg: 0xFF2F2A25,
128        app_bg: 0xFFFAF8F5,
129        app_fg: 0xFF2F2A25,
130        button_bg: 0xFFDDD6CB,
131        button_fg: 0xFF4E453C,
132        button_active_bg: 0xFFC48B71,
133        button_active_fg: 0xFFFFFFFF,
134        focus_highlight: 0xFFC48B71,
135        mouse_cursor: 0xFFFAF8F5,
136        mouse_stroke: 0xFF2F2A25,
137        login_screen_bg: 0xFF28231E,
138        login_dialog_bg: 0xFFEAE5DC,
139        login_dialog_shadow: 0xFF161311,
140        login_dialog_border: 0xFF8F9779,
141        login_text_fg: 0xFF2F2A25,
142        login_field_bg: 0xFFFAF8F5,
143        login_field_border: 0xFF8F9779,
144        sysmon_bg: 0xFF2F2A25,
145        sysmon_header_bg: 0xFF3D3730,
146        sysmon_accent: 0xFFC48B71,
147        sysmon_text: 0xFFB8B0A5,
148        sysmon_value: 0xFFF6F2EA,
149        sysmon_border: 0xFF4A423B,
150        tab_area_bg: 0xFF3A342E,
151        tab_area_border: 0xFF4A423B,
152        tab_active_bg: 0xFFC48B71,
153        tab_active_fg: 0xFFFDFBF7,
154        tab_inactive_fg: 0xFFB8B0A5,
155        fn_bar_bg: 0xFF25201C,
156        fn_bar_cell_bg: 0xFF2F2A25,
157        fn_bar_key_bg: 0xFF4A423B,
158        fn_bar_key_fg: 0xFFC48B71,
159        fn_bar_text_fg: 0xFFFAF8F5,
160        fn_bar_dim_fg: 0xFF73695F,
161        fn_bar_border: 0xFF4A423B,
162        ime_bg: 0xFFFAF8F5,
163        ime_border: 0xFFC48B71,
164        ime_text: 0xFF2F2A25,
165        win_btn_close: 0xFFB85450,
166        win_btn_fullscreen: 0xFF8A6CA0,
167        win_btn_maximize: 0xFF5A8C5A,
168        win_btn_restore: 0xFFB87840,
169        win_btn_minimize: 0xFFA09040,
170        error_fg: 0xFFCC4444,
171        success_fg: 0xFF4A8A50,
172        warning_fg: 0xFFCC8040,
173        ui_overlay_bg: 0xFF2A2420,
174        ui_overlay_accent: 0xFFC48B71,
175        ui_input_bg: 0xFF3A342E,
176        browser_bg: 0xFF2A2420,
177        browser_fg: 0xFFFDFBF7,
178        browser_secondary: 0xFF4A423B,
179        login_caret: 0xFFC48B71,
180        login_hint_fg: 0xFF8A7E72,
181        scrollbar_thumb: 0xFF8F9779,
182        form_input_bg: 0xFFFAF8F5,
183        form_input_fg: 0xFF2F2A25,
184        form_input_border: 0xFF9E9286,
185        form_input_border_focus: 0xFFC48B71,
186        form_input_accent: 0xFF8F9779,
187        form_checkbox_checked: 0xFFC48B71,
188        form_checkbox_unchecked: 0xFFEAE5DC,
189        btn_default_bg: 0xFFDDD6CB,
190        btn_default_border: 0xFF9E9286,
191        btn_hover_bg: 0xFF8F9779,
192        btn_hover_border: 0xFFC48B71,
193        editor_line_highlight: 0xFFC48B71,
194        editor_dim_fg: 0xFF9E9286,
195        video_bar_bg: 0xFF3A342E,
196        video_bar_sep: 0xFF4A423B,
197        video_progress_fg: 0xFF4A8A50,
198        video_thumb_fg: 0xFFFAF8F5,
199        privacy_badge_bg: 0xFF2F4A2F,
200        privacy_badge_fg: 0xFFECFDF5,
201    },
202    ThemeConfig {
203        name: "Dracula",
204        desktop_bg: 0xFF181A1F,
205        terminal_bg: 0xFF282A36,
206        terminal_fg: 0xFFF8F8F2,
207        active_window_border: 0xFFFF79C6,
208        non_active_window_border: 0xFF6272A4,
209        window_title_bar: 0xFF44475A,
210        window_title_fg: 0xFFF8F8F2,
211        app_bg: 0xFFF3F4F6,
212        app_fg: 0xFF1F2937,
213        button_bg: 0xFFE5E7EB,
214        button_fg: 0xFF1F2937,
215        button_active_bg: 0xFF3B82F6,
216        button_active_fg: 0xFFFFFFFF,
217        focus_highlight: 0xFFF59E0B,
218        mouse_cursor: 0xFFFFFFFF,
219        mouse_stroke: 0xFF000000,
220        login_screen_bg: 0xFF0B0B0D,
221        login_dialog_bg: 0xFF1E1F29,
222        login_dialog_shadow: 0xFF040405,
223        login_dialog_border: 0xFF6272A4,
224        login_text_fg: 0xFFF8F8F2,
225        login_field_bg: 0xFF282A36,
226        login_field_border: 0xFFFF79C6,
227        sysmon_bg: 0xFF1E1F29,
228        sysmon_header_bg: 0xFF282A36,
229        sysmon_accent: 0xFFBD93F9,
230        sysmon_text: 0xFF6272A4,
231        sysmon_value: 0xFFF8F8F2,
232        sysmon_border: 0xFF44475A,
233        tab_area_bg: 0xFF1E1F29,
234        tab_area_border: 0xFF44475A,
235        tab_active_bg: 0xFF44475A,
236        tab_active_fg: 0xFF50FA7B,
237        tab_inactive_fg: 0xFF8BE9FD,
238        fn_bar_bg: 0xFF1E1F29,
239        fn_bar_cell_bg: 0xFF282A36,
240        fn_bar_key_bg: 0xFF44475A,
241        fn_bar_key_fg: 0xFF8BE9FD,
242        fn_bar_text_fg: 0xFFF8F8F2,
243        fn_bar_dim_fg: 0xFF6272A4,
244        fn_bar_border: 0xFF6272A4,
245        ime_bg: 0xFF1E1F29,
246        ime_border: 0xFFBD93F9,
247        ime_text: 0xFFF8F8F2,
248        win_btn_close: 0xFFFF5555,
249        win_btn_fullscreen: 0xFFBD93F9,
250        win_btn_maximize: 0xFF50FA7B,
251        win_btn_restore: 0xFFFFB86C,
252        win_btn_minimize: 0xFFF1FA8C,
253        error_fg: 0xFFFF5555,
254        success_fg: 0xFF50FA7B,
255        warning_fg: 0xFFFFB86C,
256        ui_overlay_bg: 0xFF1E1F29,
257        ui_overlay_accent: 0xFFFF79C6,
258        ui_input_bg: 0xFF1E1F29,
259        browser_bg: 0xFF282A36,
260        browser_fg: 0xFFF8F8F2,
261        browser_secondary: 0xFF44475A,
262        login_caret: 0xFF756CE0,
263        login_hint_fg: 0xFF6272A4,
264        scrollbar_thumb: 0xFF6272A4,
265        form_input_bg: 0xFF1E1F29,
266        form_input_fg: 0xFFF8F8F2,
267        form_input_border: 0xFF6272A4,
268        form_input_border_focus: 0xFFFF79C6,
269        form_input_accent: 0xFF8BE9FD,
270        form_checkbox_checked: 0xFFFF79C6,
271        form_checkbox_unchecked: 0xFF1E1F29,
272        btn_default_bg: 0xFF44475A,
273        btn_default_border: 0xFF6272A4,
274        btn_hover_bg: 0xFF6272A4,
275        btn_hover_border: 0xFF8BE9FD,
276        editor_line_highlight: 0xFF756CE0,
277        editor_dim_fg: 0xFF6272A4,
278        video_bar_bg: 0xFF282A36,
279        video_bar_sep: 0xFF44475A,
280        video_progress_fg: 0xFF50FA7B,
281        video_thumb_fg: 0xFFF8F8F2,
282        privacy_badge_bg: 0xFF064E3B,
283        privacy_badge_fg: 0xFFECFDF5,
284    },
285    ThemeConfig {
286        name: "Slate Blue",
287        desktop_bg: 0xFF0F172A,
288        terminal_bg: 0xFF1E293B,
289        terminal_fg: 0xFFF1F5F9,
290        active_window_border: 0xFF38BDF8,
291        non_active_window_border: 0xFF475569,
292        window_title_bar: 0xFF334155,
293        window_title_fg: 0xFFF1F5F9,
294        app_bg: 0xFFF8FAFC,
295        app_fg: 0xFF0F172A,
296        button_bg: 0xFFE2E8F0,
297        button_fg: 0xFF334155,
298        button_active_bg: 0xFF2563EB,
299        button_active_fg: 0xFFFFFFFF,
300        focus_highlight: 0xFFF59E0B,
301        mouse_cursor: 0xFFFFFFFF,
302        mouse_stroke: 0xFF000000,
303        login_screen_bg: 0xFF0B132B,
304        login_dialog_bg: 0xFF1C2541,
305        login_dialog_shadow: 0xFF050814,
306        login_dialog_border: 0xFF475569,
307        login_text_fg: 0xFFF1F5F9,
308        login_field_bg: 0xFF1E293B,
309        login_field_border: 0xFF38BDF8,
310        sysmon_bg: 0xFF1E293B,
311        sysmon_header_bg: 0xFF334155,
312        sysmon_accent: 0xFF38BDF8,
313        sysmon_text: 0xFF94A3B8,
314        sysmon_value: 0xFFF1F5F9,
315        sysmon_border: 0xFF475569,
316        tab_area_bg: 0xFF1E293B,
317        tab_area_border: 0xFF475569,
318        tab_active_bg: 0xFF334155,
319        tab_active_fg: 0xFF38BDF8,
320        tab_inactive_fg: 0xFF94A3B8,
321        fn_bar_bg: 0xFF0F172A,
322        fn_bar_cell_bg: 0xFF1E293B,
323        fn_bar_key_bg: 0xFF334155,
324        fn_bar_key_fg: 0xFF38BDF8,
325        fn_bar_text_fg: 0xFFF1F5F9,
326        fn_bar_dim_fg: 0xFF64748B,
327        fn_bar_border: 0xFF475569,
328        ime_bg: 0xFF1E293B,
329        ime_border: 0xFF38BDF8,
330        ime_text: 0xFFF1F5F9,
331        win_btn_close: 0xFFEF4444,
332        win_btn_fullscreen: 0xFF8B5CF6,
333        win_btn_maximize: 0xFF22C55E,
334        win_btn_restore: 0xFFF97316,
335        win_btn_minimize: 0xFFEAB308,
336        error_fg: 0xFFEF4444,
337        success_fg: 0xFF22C55E,
338        warning_fg: 0xFFF97316,
339        ui_overlay_bg: 0xFF1E293B,
340        ui_overlay_accent: 0xFF38BDF8,
341        ui_input_bg: 0xFF1E293B,
342        browser_bg: 0xFF1E293B,
343        browser_fg: 0xFFF1F5F9,
344        browser_secondary: 0xFF334155,
345        login_caret: 0xFF38BDF8,
346        login_hint_fg: 0xFF64748B,
347        scrollbar_thumb: 0xFF475569,
348        form_input_bg: 0xFF1E293B,
349        form_input_fg: 0xFFF1F5F9,
350        form_input_border: 0xFF475569,
351        form_input_border_focus: 0xFF38BDF8,
352        form_input_accent: 0xFF38BDF8,
353        form_checkbox_checked: 0xFF38BDF8,
354        form_checkbox_unchecked: 0xFF1E293B,
355        btn_default_bg: 0xFF334155,
356        btn_default_border: 0xFF475569,
357        btn_hover_bg: 0xFF475569,
358        btn_hover_border: 0xFF38BDF8,
359        editor_line_highlight: 0xFF38BDF8,
360        editor_dim_fg: 0xFF64748B,
361        video_bar_bg: 0xFF1E293B,
362        video_bar_sep: 0xFF334155,
363        video_progress_fg: 0xFF22C55E,
364        video_thumb_fg: 0xFFF1F5F9,
365        privacy_badge_bg: 0xFF064E3B,
366        privacy_badge_fg: 0xFFECFDF5,
367    },
368    ThemeConfig {
369        name: "Emerald Green",
370        desktop_bg: 0xFF022C22,
371        terminal_bg: 0xFF064E3B,
372        terminal_fg: 0xFFECFDF5,
373        active_window_border: 0xFF34D399,
374        non_active_window_border: 0xFF059669,
375        window_title_bar: 0xFF047857,
376        window_title_fg: 0xFFECFDF5,
377        app_bg: 0xFFF0FDF4,
378        app_fg: 0xFF064E3B,
379        button_bg: 0xFFD1FAE5,
380        button_fg: 0xFF065F46,
381        button_active_bg: 0xFF10B981,
382        button_active_fg: 0xFFFFFFFF,
383        focus_highlight: 0xFFF59E0B,
384        mouse_cursor: 0xFFFFFFFF,
385        mouse_stroke: 0xFF000000,
386        login_screen_bg: 0xFF021B15,
387        login_dialog_bg: 0xFF064E3B,
388        login_dialog_shadow: 0xFF010C09,
389        login_dialog_border: 0xFF059669,
390        login_text_fg: 0xFFECFDF5,
391        login_field_bg: 0xFF022C22,
392        login_field_border: 0xFF34D399,
393        sysmon_bg: 0xFF064E3B,
394        sysmon_header_bg: 0xFF047857,
395        sysmon_accent: 0xFF34D399,
396        sysmon_text: 0xFF6EE7B7,
397        sysmon_value: 0xFFECFDF5,
398        sysmon_border: 0xFF059669,
399        tab_area_bg: 0xFF064E3B,
400        tab_area_border: 0xFF059669,
401        tab_active_bg: 0xFF047857,
402        tab_active_fg: 0xFF34D399,
403        tab_inactive_fg: 0xFF6EE7B7,
404        fn_bar_bg: 0xFF022C22,
405        fn_bar_cell_bg: 0xFF064E3B,
406        fn_bar_key_bg: 0xFF047857,
407        fn_bar_key_fg: 0xFF34D399,
408        fn_bar_text_fg: 0xFFECFDF5,
409        fn_bar_dim_fg: 0xFF059669,
410        fn_bar_border: 0xFF059669,
411        ime_bg: 0xFF064E3B,
412        ime_border: 0xFF34D399,
413        ime_text: 0xFFECFDF5,
414        win_btn_close: 0xFFEF4444,
415        win_btn_fullscreen: 0xFFA78BFA,
416        win_btn_maximize: 0xFF34D399,
417        win_btn_restore: 0xFFFB923C,
418        win_btn_minimize: 0xFFFBBF24,
419        error_fg: 0xFFEF4444,
420        success_fg: 0xFF34D399,
421        warning_fg: 0xFFFB923C,
422        ui_overlay_bg: 0xFF064E3B,
423        ui_overlay_accent: 0xFF34D399,
424        ui_input_bg: 0xFF064E3B,
425        browser_bg: 0xFF064E3B,
426        browser_fg: 0xFFECFDF5,
427        browser_secondary: 0xFF047857,
428        login_caret: 0xFF34D399,
429        login_hint_fg: 0xFF6EE7B7,
430        scrollbar_thumb: 0xFF059669,
431        form_input_bg: 0xFF064E3B,
432        form_input_fg: 0xFFECFDF5,
433        form_input_border: 0xFF059669,
434        form_input_border_focus: 0xFF34D399,
435        form_input_accent: 0xFF34D399,
436        form_checkbox_checked: 0xFF34D399,
437        form_checkbox_unchecked: 0xFF064E3B,
438        btn_default_bg: 0xFF047857,
439        btn_default_border: 0xFF059669,
440        btn_hover_bg: 0xFF059669,
441        btn_hover_border: 0xFF34D399,
442        editor_line_highlight: 0xFF34D399,
443        editor_dim_fg: 0xFF059669,
444        video_bar_bg: 0xFF064E3B,
445        video_bar_sep: 0xFF047857,
446        video_progress_fg: 0xFF34D399,
447        video_thumb_fg: 0xFFECFDF5,
448        privacy_badge_bg: 0xFF022C22,
449        privacy_badge_fg: 0xFFECFDF5,
450    },
451    ThemeConfig {
452        name: "Cherry Light",
453        desktop_bg: 0xFFFFF1F2,
454        terminal_bg: 0xFFFFE4E6,
455        terminal_fg: 0xFF9F1239,
456        active_window_border: 0xFFF43F5E,
457        non_active_window_border: 0xFFFDA4AF,
458        window_title_bar: 0xFFFECDD3,
459        window_title_fg: 0xFF9F1239,
460        app_bg: 0xFFFFF1F2,
461        app_fg: 0xFF881337,
462        button_bg: 0xFFFFE4E6,
463        button_fg: 0xFF9F1239,
464        button_active_bg: 0xFFE11D48,
465        button_active_fg: 0xFFFFFFFF,
466        focus_highlight: 0xFFF59E0B,
467        mouse_cursor: 0xFFFFFFFF,
468        mouse_stroke: 0xFF000000,
469        login_screen_bg: 0xFF4C0519,
470        login_dialog_bg: 0xFFFFE4E6,
471        login_dialog_shadow: 0xFF27020B,
472        login_dialog_border: 0xFFFDA4AF,
473        login_text_fg: 0xFF9F1239,
474        login_field_bg: 0xFFFFF1F2,
475        login_field_border: 0xFFF43F5E,
476        sysmon_bg: 0xFFFFE4E6,
477        sysmon_header_bg: 0xFFFECDD3,
478        sysmon_accent: 0xFFF43F5E,
479        sysmon_text: 0xFFFDA4AF,
480        sysmon_value: 0xFF9F1239,
481        sysmon_border: 0xFFFDA4AF,
482        tab_area_bg: 0xFFFFE4E6,
483        tab_area_border: 0xFFFDA4AF,
484        tab_active_bg: 0xFFFECDD3,
485        tab_active_fg: 0xFFF43F5E,
486        tab_inactive_fg: 0xFFFDA4AF,
487        fn_bar_bg: 0xFFFFF1F2,
488        fn_bar_cell_bg: 0xFFFFE4E6,
489        fn_bar_key_bg: 0xFFFECDD3,
490        fn_bar_key_fg: 0xFFF43F5E,
491        fn_bar_text_fg: 0xFF9F1239,
492        fn_bar_dim_fg: 0xFFFDA4AF,
493        fn_bar_border: 0xFFFDA4AF,
494        ime_bg: 0xFFFFE4E6,
495        ime_border: 0xFFF43F5E,
496        ime_text: 0xFF9F1239,
497        win_btn_close: 0xFFE11D48,
498        win_btn_fullscreen: 0xFFA855F7,
499        win_btn_maximize: 0xFF22C55E,
500        win_btn_restore: 0xFFF97316,
501        win_btn_minimize: 0xFFEAB308,
502        error_fg: 0xFFE11D48,
503        success_fg: 0xFF22C55E,
504        warning_fg: 0xFFF97316,
505        ui_overlay_bg: 0xFFFFE4E6,
506        ui_overlay_accent: 0xFFF43F5E,
507        ui_input_bg: 0xFFFFE4E6,
508        browser_bg: 0xFFFFE4E6,
509        browser_fg: 0xFF9F1239,
510        browser_secondary: 0xFFFECDD3,
511        login_caret: 0xFFF43F5E,
512        login_hint_fg: 0xFFFDA4AF,
513        scrollbar_thumb: 0xFFFDA4AF,
514        form_input_bg: 0xFFFFF1F2,
515        form_input_fg: 0xFF9F1239,
516        form_input_border: 0xFFFDA4AF,
517        form_input_border_focus: 0xFFF43F5E,
518        form_input_accent: 0xFFF43F5E,
519        form_checkbox_checked: 0xFFF43F5E,
520        form_checkbox_unchecked: 0xFFFFE4E6,
521        btn_default_bg: 0xFFFFE4E6,
522        btn_default_border: 0xFFFDA4AF,
523        btn_hover_bg: 0xFFFDA4AF,
524        btn_hover_border: 0xFFF43F5E,
525        editor_line_highlight: 0xFFF43F5E,
526        editor_dim_fg: 0xFFFDA4AF,
527        video_bar_bg: 0xFFFFE4E6,
528        video_bar_sep: 0xFFFDA4AF,
529        video_progress_fg: 0xFFF43F5E,
530        video_thumb_fg: 0xFF9F1239,
531        privacy_badge_bg: 0xFF4C0519,
532        privacy_badge_fg: 0xFFFFF1F2,
533    },
534];
535
536#[derive(Clone, Debug)]
537pub struct OsConfig {
538    pub keyboard_layout: KeyboardLayout,
539    pub theme_name: String,
540    pub theme: ThemeConfig,
541    /// フォントのフォールバック順([0]=プライマリ)。主にグリフが無い文字を順に次のフォントで補う。
542    pub font_names: [String; 3],
543    pub enable_sound: bool,
544    pub swap_caps_ctrl: bool,
545    pub auto_dhcp: bool,
546    pub wifi_ssid: String,
547    pub wifi_password: String,
548    pub custom_theme: ThemeConfig,  // カスタムテーマ保持値
549    pub enable_custom_colors: bool, // カスタムカラー有効フラグ
550}
551
552impl Default for OsConfig {
553    fn default() -> Self {
554        Self {
555            keyboard_layout: KeyboardLayout::Jis,
556            theme_name: String::from("Earth Cream"),
557            theme: PRESET_THEMES[0],
558            font_names: [
559                String::from(crate::kernel::vector_font::DEFAULT_FONT_CHAIN[0]),
560                String::from(crate::kernel::vector_font::DEFAULT_FONT_CHAIN[1]),
561                String::from(crate::kernel::vector_font::DEFAULT_FONT_CHAIN[2]),
562            ],
563            enable_sound: true,
564            swap_caps_ctrl: false,
565            auto_dhcp: true,
566            wifi_ssid: String::new(),
567            wifi_password: String::new(),
568            custom_theme: PRESET_THEMES[0],
569            enable_custom_colors: false,
570        }
571    }
572}
573
574// グローバル設定インスタンス
575pub static mut GLOBAL_CONFIG: Option<OsConfig> = None;
576
577/// 設定読み込みの結果。
578///
579/// `config` は**常に利用可能**(ファイルが壊れていても既定値へ復旧済み)だが、
580/// 途中で起きた失敗(書き込み失敗・壊れた JSON・未知の値・型違い)は
581/// `status` として必ず呼び出し側へ返す。以前はこれらを全て握り潰していたため、
582/// 「設定したのに反映されない/再起動で戻る」原因が誰にも分からなかった。
583///
584/// `status` は**最初に検出した失敗**を保持する(2 件目以降はログのみ)。
585#[derive(Clone, Debug)]
586pub struct ConfigLoad {
587    pub config: OsConfig,
588    pub status: Result<(), &'static str>,
589}
590
591/// 最初の失敗だけを保持する(後続はログ済みなので上書きしない)。
592fn record_failure(status: &mut Result<(), &'static str>, err: &'static str) {
593    if status.is_ok() {
594        *status = Err(err);
595    }
596}
597
598/// グローバル設定を取得する。
599///
600/// # エラーの扱い
601/// 戻り値が `&'static OsConfig` である以上ここで `Result` は返せないため、
602/// 未初期化時の読み込みで失敗が起きた場合は `error!` へ出力したうえで
603/// 既定値へ復旧した設定を返す。**失敗の有無を判定したい呼び出し側は
604/// `init_or_load()` / `reload_and_apply()` を使うこと。**
605pub fn get_config() -> &'static OsConfig {
606    // まだ読み込まれていない場合は初期化してから返す (panic フリー)
607    unsafe {
608        GLOBAL_CONFIG.get_or_insert_with(|| {
609            let loaded = load_config();
610            if let Err(e) = loaded.status {
611                crate::error!("[SYS] get_config: configuration load degraded: {}", e);
612            }
613            loaded.config
614        })
615    }
616}
617
618pub fn init_or_load() -> ConfigLoad {
619    let loaded = load_config();
620    unsafe {
621        GLOBAL_CONFIG = Some(loaded.config.clone());
622    }
623    loaded
624}
625
626pub fn reload_and_apply() -> Result<(), &'static str> {
627    crate::info!("[SYS] Reloading OS Configuration...");
628    let loaded = load_config();
629    let conf = loaded.config;
630    unsafe {
631        GLOBAL_CONFIG = Some(conf.clone());
632    }
633
634    // フォントの動的再ロード(フォールバックチェーン)
635    let chain: alloc::vec::Vec<&str> = conf.font_names.iter().map(|s| s.as_str()).collect();
636    crate::kernel::vector_font::init_global_font_chain(&chain);
637
638    // 描画画面全体が再描画されるようにDirtyフラグを立てる
639    let mut wm = crate::kernel::window_mgr::get_instance();
640    wm.dirty = true;
641
642    // 再読み込み自体で起きた失敗(保存できなかった・設定が壊れていた等)は
643    // 呼び出し側へそのまま返す。適用処理は完了しているが、状態は健全ではない。
644    loaded.status
645}
646
647pub fn parse_hex_color(val: &JsonValue) -> Option<u32> {
648    match val {
649        JsonValue::Number(n) => Some(*n as u32),
650        JsonValue::String(s) => {
651            let s_trimmed = s.trim_start_matches("0x").trim_start_matches('#');
652            u32::from_str_radix(s_trimmed, 16).ok()
653        }
654        _ => None,
655    }
656}
657
658/// カラーセット JSON の 1 キーを適用する。
659///
660/// キーが無いのは正常(未指定=既定色のまま)だが、**キーがあるのに値が
661/// 読めない場合はエラー**を返す。ここを黙って無視すると「その色だけ効かない」
662/// 状態が何の手掛かりも無しに残る。
663fn apply_color(
664    obj: &BTreeMap<String, JsonValue>,
665    key: &'static str,
666    dst: &mut u32,
667) -> Result<(), &'static str> {
668    if let Some(v) = obj.get(key) {
669        match parse_hex_color(v) {
670            Some(c) => *dst = c,
671            None => {
672                crate::error!("[SYS] colorset: invalid color value for key '{}'", key);
673                return Err("colorset: invalid color value");
674            }
675        }
676    }
677    Ok(())
678}
679
680pub fn load_colorset_from_json(
681    json_str: &str,
682    theme: &mut ThemeConfig,
683) -> Result<(), &'static str> {
684    let parsed = json::parse(json_str).map_err(|_| "colorset: malformed JSON")?;
685    let JsonValue::Object(obj) = parsed else {
686        return Err("colorset: root is not an object");
687    };
688    {
689        let obj = &obj;
690        apply_color(obj, "desktop_bg", &mut theme.desktop_bg)?;
691        apply_color(obj, "terminal_bg", &mut theme.terminal_bg)?;
692        apply_color(obj, "terminal_fg", &mut theme.terminal_fg)?;
693        apply_color(obj, "active_window_border", &mut theme.active_window_border)?;
694        apply_color(obj, "non_active_window_border", &mut theme.non_active_window_border)?;
695        apply_color(obj, "window_title_bar", &mut theme.window_title_bar)?;
696        apply_color(obj, "window_title_fg", &mut theme.window_title_fg)?;
697        apply_color(obj, "app_bg", &mut theme.app_bg)?;
698        apply_color(obj, "app_fg", &mut theme.app_fg)?;
699        apply_color(obj, "button_bg", &mut theme.button_bg)?;
700        apply_color(obj, "button_fg", &mut theme.button_fg)?;
701        apply_color(obj, "button_active_bg", &mut theme.button_active_bg)?;
702        apply_color(obj, "button_active_fg", &mut theme.button_active_fg)?;
703        apply_color(obj, "focus_highlight", &mut theme.focus_highlight)?;
704        apply_color(obj, "mouse_cursor", &mut theme.mouse_cursor)?;
705        apply_color(obj, "mouse_stroke", &mut theme.mouse_stroke)?;
706        apply_color(obj, "login_screen_bg", &mut theme.login_screen_bg)?;
707        apply_color(obj, "login_dialog_bg", &mut theme.login_dialog_bg)?;
708        apply_color(obj, "login_dialog_shadow", &mut theme.login_dialog_shadow)?;
709        apply_color(obj, "login_dialog_border", &mut theme.login_dialog_border)?;
710        apply_color(obj, "login_text_fg", &mut theme.login_text_fg)?;
711        apply_color(obj, "login_field_bg", &mut theme.login_field_bg)?;
712        apply_color(obj, "login_field_border", &mut theme.login_field_border)?;
713        apply_color(obj, "sysmon_bg", &mut theme.sysmon_bg)?;
714        apply_color(obj, "sysmon_header_bg", &mut theme.sysmon_header_bg)?;
715        apply_color(obj, "sysmon_accent", &mut theme.sysmon_accent)?;
716        apply_color(obj, "sysmon_text", &mut theme.sysmon_text)?;
717        apply_color(obj, "sysmon_value", &mut theme.sysmon_value)?;
718        apply_color(obj, "sysmon_border", &mut theme.sysmon_border)?;
719        apply_color(obj, "tab_area_bg", &mut theme.tab_area_bg)?;
720        apply_color(obj, "tab_area_border", &mut theme.tab_area_border)?;
721        apply_color(obj, "tab_active_bg", &mut theme.tab_active_bg)?;
722        apply_color(obj, "tab_active_fg", &mut theme.tab_active_fg)?;
723        apply_color(obj, "tab_inactive_fg", &mut theme.tab_inactive_fg)?;
724        apply_color(obj, "fn_bar_bg", &mut theme.fn_bar_bg)?;
725        apply_color(obj, "fn_bar_cell_bg", &mut theme.fn_bar_cell_bg)?;
726        apply_color(obj, "fn_bar_key_bg", &mut theme.fn_bar_key_bg)?;
727        apply_color(obj, "fn_bar_key_fg", &mut theme.fn_bar_key_fg)?;
728        apply_color(obj, "fn_bar_text_fg", &mut theme.fn_bar_text_fg)?;
729        apply_color(obj, "fn_bar_dim_fg", &mut theme.fn_bar_dim_fg)?;
730        apply_color(obj, "fn_bar_border", &mut theme.fn_bar_border)?;
731        apply_color(obj, "ime_bg", &mut theme.ime_bg)?;
732        apply_color(obj, "ime_border", &mut theme.ime_border)?;
733        apply_color(obj, "ime_text", &mut theme.ime_text)?;
734        apply_color(obj, "win_btn_close", &mut theme.win_btn_close)?;
735        apply_color(obj, "win_btn_fullscreen", &mut theme.win_btn_fullscreen)?;
736        apply_color(obj, "win_btn_maximize", &mut theme.win_btn_maximize)?;
737        apply_color(obj, "win_btn_restore", &mut theme.win_btn_restore)?;
738        apply_color(obj, "win_btn_minimize", &mut theme.win_btn_minimize)?;
739        apply_color(obj, "error_fg", &mut theme.error_fg)?;
740        apply_color(obj, "success_fg", &mut theme.success_fg)?;
741        apply_color(obj, "warning_fg", &mut theme.warning_fg)?;
742        apply_color(obj, "ui_overlay_bg", &mut theme.ui_overlay_bg)?;
743        apply_color(obj, "ui_overlay_accent", &mut theme.ui_overlay_accent)?;
744        apply_color(obj, "ui_input_bg", &mut theme.ui_input_bg)?;
745        apply_color(obj, "browser_bg", &mut theme.browser_bg)?;
746        apply_color(obj, "browser_fg", &mut theme.browser_fg)?;
747        apply_color(obj, "browser_secondary", &mut theme.browser_secondary)?;
748        apply_color(obj, "login_caret", &mut theme.login_caret)?;
749        apply_color(obj, "login_hint_fg", &mut theme.login_hint_fg)?;
750        apply_color(obj, "scrollbar_thumb", &mut theme.scrollbar_thumb)?;
751        apply_color(obj, "form_input_bg", &mut theme.form_input_bg)?;
752        apply_color(obj, "form_input_fg", &mut theme.form_input_fg)?;
753        apply_color(obj, "form_input_border", &mut theme.form_input_border)?;
754        apply_color(obj, "form_input_border_focus", &mut theme.form_input_border_focus)?;
755        apply_color(obj, "form_input_accent", &mut theme.form_input_accent)?;
756        apply_color(obj, "form_checkbox_checked", &mut theme.form_checkbox_checked)?;
757        apply_color(obj, "form_checkbox_unchecked", &mut theme.form_checkbox_unchecked)?;
758        apply_color(obj, "btn_default_bg", &mut theme.btn_default_bg)?;
759        apply_color(obj, "btn_default_border", &mut theme.btn_default_border)?;
760        apply_color(obj, "btn_hover_bg", &mut theme.btn_hover_bg)?;
761        apply_color(obj, "btn_hover_border", &mut theme.btn_hover_border)?;
762        apply_color(obj, "editor_line_highlight", &mut theme.editor_line_highlight)?;
763        apply_color(obj, "editor_dim_fg", &mut theme.editor_dim_fg)?;
764        apply_color(obj, "video_bar_bg", &mut theme.video_bar_bg)?;
765        apply_color(obj, "video_bar_sep", &mut theme.video_bar_sep)?;
766        apply_color(obj, "video_progress_fg", &mut theme.video_progress_fg)?;
767        apply_color(obj, "video_thumb_fg", &mut theme.video_thumb_fg)?;
768        apply_color(obj, "privacy_badge_bg", &mut theme.privacy_badge_bg)?;
769        apply_color(obj, "privacy_badge_fg", &mut theme.privacy_badge_fg)?;
770    }
771    Ok(())
772}
773
774/// JSON オブジェクトから真偽値を取り出す。
775/// キーが無いのは正常(未指定)。**あるのに型が違う場合はエラー**を返す。
776fn take_bool(
777    obj: &BTreeMap<String, JsonValue>,
778    key: &'static str,
779    dst: &mut bool,
780) -> Result<(), &'static str> {
781    match obj.get(key) {
782        None => Ok(()),
783        Some(JsonValue::Bool(b)) => {
784            *dst = *b;
785            Ok(())
786        }
787        Some(_) => {
788            crate::error!("[SYS] config: '{}' must be a boolean", key);
789            Err("config: value has wrong type")
790        }
791    }
792}
793
794/// JSON オブジェクトから文字列を取り出す(型違いはエラー)。
795fn take_string(
796    obj: &BTreeMap<String, JsonValue>,
797    key: &'static str,
798    dst: &mut String,
799) -> Result<(), &'static str> {
800    match obj.get(key) {
801        None => Ok(()),
802        Some(JsonValue::String(s)) => {
803            *dst = s.clone();
804            Ok(())
805        }
806        Some(_) => {
807            crate::error!("[SYS] config: '{}' must be a string", key);
808            Err("config: value has wrong type")
809        }
810    }
811}
812
813/// JSON オブジェクトから数値を取り出す(型違い・負値・非有限はエラー)。
814fn take_u32(
815    obj: &BTreeMap<String, JsonValue>,
816    key: &'static str,
817    dst: &mut u32,
818) -> Result<(), &'static str> {
819    match obj.get(key) {
820        None => Ok(()),
821        Some(JsonValue::Number(n)) => {
822            if !n.is_finite() || *n < 0.0 || *n > u32::MAX as f64 {
823                crate::error!("[SYS] config: '{}' is out of range", key);
824                return Err("config: numeric value out of range");
825            }
826            *dst = *n as u32;
827            Ok(())
828        }
829        Some(_) => {
830            crate::error!("[SYS] config: '{}' must be a number", key);
831            Err("config: value has wrong type")
832        }
833    }
834}
835
836/// `system` セクションを `current_config` へ適用する。
837/// 有効値は採用し、**異常値は採用せずエラーとして返す**(最初の 1 件)。
838fn apply_system_section(
839    system: &BTreeMap<String, JsonValue>,
840    current_config: &mut OsConfig,
841    status: &mut Result<(), &'static str>,
842) {
843    // キーボードレイアウトの適用
844    match system.get("keyboard_layout") {
845        None => {}
846        Some(JsonValue::String(layout)) => match KeyboardLayout::parse(layout) {
847            Some(l) => current_config.keyboard_layout = l,
848            None => {
849                crate::error!("[SYS] config: unknown keyboard_layout '{}'", layout);
850                record_failure(status, "config: unknown keyboard_layout");
851            }
852        },
853        Some(_) => {
854            crate::error!("[SYS] config: 'keyboard_layout' must be a string");
855            record_failure(status, "config: value has wrong type");
856        }
857    }
858
859    // カスタム色の読み込み
860    match system.get("custom_colors") {
861        None => {}
862        Some(JsonValue::Object(custom_colors)) => {
863            let t = &mut current_config.custom_theme;
864            let r = (|| -> Result<(), &'static str> {
865                apply_color(custom_colors, "desktop_bg", &mut t.desktop_bg)?;
866                apply_color(custom_colors, "terminal_bg", &mut t.terminal_bg)?;
867                apply_color(custom_colors, "terminal_fg", &mut t.terminal_fg)?;
868                apply_color(custom_colors, "active_window_border", &mut t.active_window_border)?;
869                apply_color(
870                    custom_colors,
871                    "non_active_window_border",
872                    &mut t.non_active_window_border,
873                )?;
874                apply_color(custom_colors, "window_title_bar", &mut t.window_title_bar)?;
875                apply_color(custom_colors, "window_title_fg", &mut t.window_title_fg)?;
876                apply_color(custom_colors, "app_bg", &mut t.app_bg)?;
877                apply_color(custom_colors, "app_fg", &mut t.app_fg)?;
878                apply_color(custom_colors, "button_bg", &mut t.button_bg)?;
879                apply_color(custom_colors, "button_fg", &mut t.button_fg)?;
880                apply_color(custom_colors, "button_active_bg", &mut t.button_active_bg)?;
881                apply_color(custom_colors, "button_active_fg", &mut t.button_active_fg)?;
882                apply_color(custom_colors, "focus_highlight", &mut t.focus_highlight)?;
883                Ok(())
884            })();
885            if let Err(e) = r {
886                record_failure(status, e);
887            }
888        }
889        Some(_) => {
890            crate::error!("[SYS] config: 'custom_colors' must be an object");
891            record_failure(status, "config: value has wrong type");
892        }
893    }
894
895    // テーマの適用
896    // NOTE: プリセット名に一致しないテーマ名("earth" 等のカラーセット名)は
897    // 設計上の正常系。色は後段の colorset.json 側で上書きされる。
898    match system.get("theme_name") {
899        None => {}
900        Some(JsonValue::String(theme_name)) => {
901            current_config.theme_name = theme_name.clone();
902            if theme_name == "Custom" {
903                current_config.theme = current_config.custom_theme;
904            } else {
905                for t in PRESET_THEMES {
906                    if t.name == theme_name {
907                        current_config.theme = *t;
908                        break;
909                    }
910                }
911            }
912        }
913        Some(_) => {
914            crate::error!("[SYS] config: 'theme_name' must be a string");
915            record_failure(status, "config: value has wrong type");
916        }
917    }
918
919    // フォントの適用: 新形式 "font_names":[a,b,c]。旧形式 "font_name":"x" は [0] に。
920    match system.get("font_names") {
921        Some(JsonValue::Array(arr)) => {
922            if arr.len() > 3 {
923                crate::error!("[SYS] config: 'font_names' has more than 3 entries");
924                record_failure(status, "config: too many font names");
925            }
926            for (i, v) in arr.iter().take(3).enumerate() {
927                match v {
928                    JsonValue::String(name) if !name.is_empty() => {
929                        current_config.font_names[i] = name.clone()
930                    }
931                    _ => {
932                        crate::error!("[SYS] config: 'font_names[{}]' is not a valid name", i);
933                        record_failure(status, "config: invalid font name");
934                    }
935                }
936            }
937        }
938        Some(_) => {
939            crate::error!("[SYS] config: 'font_names' must be an array");
940            record_failure(status, "config: value has wrong type");
941        }
942        None => {
943            if let Err(e) = take_string(system, "font_name", &mut current_config.font_names[0]) {
944                record_failure(status, e);
945            }
946        }
947    }
948
949    // 単純な値の適用(型違いはすべてエラーとして記録する)
950    if let Err(e) = take_bool(system, "enable_sound", &mut current_config.enable_sound) {
951        record_failure(status, e);
952    }
953    if let Err(e) = take_bool(
954        system,
955        "enable_custom_colors",
956        &mut current_config.enable_custom_colors,
957    ) {
958        record_failure(status, e);
959    }
960    if current_config.enable_custom_colors {
961        current_config.theme = current_config.custom_theme;
962    }
963    if let Err(e) = take_bool(system, "swap_caps_ctrl", &mut current_config.swap_caps_ctrl) {
964        record_failure(status, e);
965    }
966    if let Err(e) = take_bool(system, "auto_dhcp", &mut current_config.auto_dhcp) {
967        record_failure(status, e);
968    }
969    if let Err(e) = take_string(system, "wifi_ssid", &mut current_config.wifi_ssid) {
970        record_failure(status, e);
971    }
972    if let Err(e) = take_string(system, "wifi_password", &mut current_config.wifi_password) {
973        record_failure(status, e);
974    }
975
976    // キーリピート設定の適用
977    match system.get("keyrepeat") {
978        None => {}
979        Some(JsonValue::Object(repeat)) => {
980            // `static mut` への参照を作らないよう、いったんローカルへ複製して
981            // 検証し、すべて有効値だった項目だけ書き戻す。
982            let (mut fd, mut fi, mut sc, mut sd, mut si) = unsafe {
983                (
984                    crate::REPEAT_PARAMS.first_delay_ms,
985                    crate::REPEAT_PARAMS.first_interval_ms,
986                    crate::REPEAT_PARAMS.second_repeat_count,
987                    crate::REPEAT_PARAMS.second_delay_ms,
988                    crate::REPEAT_PARAMS.second_interval_ms,
989                )
990            };
991            if let Err(e) = take_u32(repeat, "first_delay_ms", &mut fd) {
992                record_failure(status, e);
993            }
994            if let Err(e) = take_u32(repeat, "first_interval_ms", &mut fi) {
995                record_failure(status, e);
996            }
997            if let Err(e) = take_u32(repeat, "second_repeat_count", &mut sc) {
998                record_failure(status, e);
999            }
1000            if let Err(e) = take_u32(repeat, "second_delay_ms", &mut sd) {
1001                record_failure(status, e);
1002            }
1003            if let Err(e) = take_u32(repeat, "second_interval_ms", &mut si) {
1004                record_failure(status, e);
1005            }
1006            unsafe {
1007                crate::REPEAT_PARAMS.first_delay_ms = fd;
1008                crate::REPEAT_PARAMS.first_interval_ms = fi;
1009                crate::REPEAT_PARAMS.second_repeat_count = sc;
1010                crate::REPEAT_PARAMS.second_delay_ms = sd;
1011                crate::REPEAT_PARAMS.second_interval_ms = si;
1012            }
1013        }
1014        Some(_) => {
1015            crate::error!("[SYS] config: 'keyrepeat' must be an object");
1016            record_failure(status, "config: value has wrong type");
1017        }
1018    }
1019}
1020
1021/// `/os/config.json` と `/os/colorset.json` を読み込む。
1022///
1023/// 戻り値の `config` は常に利用可能だが、**途中で起きた失敗は `status` として
1024/// 呼び出し側へ返す**(従来はすべて握り潰していた)。
1025pub fn load_config() -> ConfigLoad {
1026    let mut current_config = OsConfig::default();
1027    let mut status: Result<(), &'static str> = Ok(());
1028    let fs = fs::get_fs();
1029
1030    let mut config_ok = false;
1031    // 【2026-07-28】「存在しない」と「存在するが読めない」を区別する。
1032    // 読めなかっただけのファイルを「無い」と解釈して既定値で上書きすると、
1033    // ユーザーの設定を**こちらから破棄する**ことになる。
1034    let mut unreadable = false;
1035    let read_result = fs.read_file_checked("/os/config.json");
1036    let config_exists = if let Some((_, content)) = match read_result {
1037        Ok(v) => v,
1038        Err(e) => {
1039            crate::error!("[SYS] config: /os/config.json could not be read: {}", e);
1040            record_failure(&mut status, e);
1041            unreadable = true;
1042            None
1043        }
1044    } {
1045        match core::str::from_utf8(&content) {
1046            Err(_) => {
1047                crate::error!("[SYS] config: /os/config.json is not valid UTF-8");
1048            }
1049            Ok(content_str) => match json::parse(content_str) {
1050                Ok(JsonValue::Object(obj)) => {
1051                    config_ok = true;
1052                    match obj.get("system") {
1053                        Some(JsonValue::Object(system)) => {
1054                            apply_system_section(system, &mut current_config, &mut status);
1055                        }
1056                        Some(_) => {
1057                            crate::error!("[SYS] config: 'system' must be an object");
1058                            record_failure(&mut status, "config: value has wrong type");
1059                        }
1060                        None => {
1061                            crate::error!("[SYS] config: 'system' section is missing");
1062                            record_failure(&mut status, "config: 'system' section is missing");
1063                        }
1064                    }
1065                }
1066                _ => {
1067                    crate::error!("[SYS] config: /os/config.json is not a JSON object");
1068                }
1069            },
1070        }
1071        true
1072    } else {
1073        false
1074    };
1075
1076    if unreadable {
1077        // 読み出しに失敗しているだけで、中身は無事かもしれない。
1078        // 退避も既定値の書き出しも行わない(消してしまわないことを優先する)。
1079        crate::error!(
1080            "[SYS] config: keeping /os/config.json untouched because it could not be read"
1081        );
1082    } else if config_exists && !config_ok {
1083        crate::error!("[SYS] /os/config.json is corrupted! Renaming to config.json.bak and recovering default...");
1084        record_failure(&mut status, "config: /os/config.json is corrupted");
1085        if let Some((_, bad_content)) = fs.read_file("/os/config.json") {
1086            if let Err(e) = fs.delete_file("/os/config.json") {
1087                crate::error!("[SYS] config: failed to remove corrupted config: {}", e);
1088                record_failure(&mut status, e);
1089            }
1090            if let Err(e) = fs.save_file("/os/config.json.bak", &bad_content, "system,backup") {
1091                crate::error!("[SYS] config: failed to write config.json.bak: {}", e);
1092                record_failure(&mut status, e);
1093            }
1094        }
1095        // デフォルトの config.json を再書き出し
1096        if let Err(e) = save_config_internal(&current_config) {
1097            record_failure(&mut status, e);
1098        }
1099    } else if !config_exists {
1100        crate::info!("[SYS] Config file not found, creating default /os/config.json");
1101        if let Err(e) = save_config_internal(&current_config) {
1102            record_failure(&mut status, e);
1103        }
1104    }
1105
1106    // --- colorsets JSON の埋め込みと初期自動生成 ---
1107    let json_default = include_str!("../../colorset/default.json");
1108    let json_earth = include_str!("../../colorset/earth.json");
1109    let json_dracula = include_str!("../../colorset/dracula.json");
1110    let json_nord = include_str!("../../colorset/nord.json");
1111    let json_sakura = include_str!("../../colorset/sakura.json");
1112
1113    for (path, body) in [
1114        ("/os/default.json", json_default),
1115        ("/os/earth.json", json_earth),
1116        ("/os/dracula.json", json_dracula),
1117        ("/os/nord.json", json_nord),
1118        ("/os/sakura.json", json_sakura),
1119        // デフォルトアースカラーを初期カラーセットに設定
1120        ("/os/colorset.json", json_default),
1121    ] {
1122        // 「読めなかった」ものを「無い」と誤認して上書きしないよう、
1123        // 本当に存在しないと確認できたときだけ作成する。
1124        match fs.read_file_checked(path) {
1125            Ok(None) => {
1126                if let Err(e) = fs.save_file(path, body.as_bytes(), "system,colorset") {
1127                    crate::error!("[SYS] config: failed to create {}: {}", path, e);
1128                    record_failure(&mut status, e);
1129                }
1130            }
1131            Ok(Some(_)) => {}
1132            Err(e) => {
1133                crate::error!("[SYS] config: {} could not be read: {}", path, e);
1134                record_failure(&mut status, e);
1135            }
1136        }
1137    }
1138
1139    let mut colorset_loaded = false;
1140    let colorset_read = match fs.read_file_checked("/os/colorset.json") {
1141        Ok(v) => v,
1142        Err(e) => {
1143            // 読めないカラーセットを .bak へ退避して既定で上書きすると、
1144            // 一時的な読み出し失敗でユーザーの配色を失う。触らずに返す。
1145            crate::error!("[SYS] config: /os/colorset.json could not be read: {}", e);
1146            record_failure(&mut status, e);
1147            return ConfigLoad {
1148                config: current_config,
1149                status,
1150            };
1151        }
1152    };
1153    if let Some((_, colorset_content)) = colorset_read {
1154        match core::str::from_utf8(&colorset_content) {
1155            Err(_) => {
1156                crate::error!("[SYS] config: /os/colorset.json is not valid UTF-8");
1157                record_failure(&mut status, "colorset: not valid UTF-8");
1158            }
1159            Ok(colorset_str) => match load_colorset_from_json(colorset_str, &mut current_config.theme) {
1160                Ok(()) => {
1161                    if let Err(e) =
1162                        load_colorset_from_json(colorset_str, &mut current_config.custom_theme)
1163                    {
1164                        record_failure(&mut status, e);
1165                    }
1166                    colorset_loaded = true;
1167                }
1168                Err(e) => record_failure(&mut status, e),
1169            },
1170        }
1171    }
1172
1173    if !colorset_loaded {
1174        crate::error!("[SYS] /os/colorset.json is corrupted! Renaming to colorset.json.bak and recovering default...");
1175        record_failure(&mut status, "colorset: /os/colorset.json is corrupted");
1176        if let Some((_, bad_content)) = fs.read_file("/os/colorset.json") {
1177            if let Err(e) = fs.delete_file("/os/colorset.json") {
1178                crate::error!("[SYS] config: failed to remove corrupted colorset: {}", e);
1179                record_failure(&mut status, e);
1180            }
1181            if let Err(e) = fs.save_file("/os/colorset.json.bak", &bad_content, "system,backup") {
1182                crate::error!("[SYS] config: failed to write colorset.json.bak: {}", e);
1183                record_failure(&mut status, e);
1184            }
1185        }
1186        // デフォルトアースカラーを再適用・保存
1187        if let Err(e) = fs.save_file("/os/colorset.json", json_default.as_bytes(), "system,colorset")
1188        {
1189            crate::error!("[SYS] config: failed to restore default colorset: {}", e);
1190            record_failure(&mut status, e);
1191        }
1192        // 埋め込みの既定カラーセットは常に正しい前提だが、万一壊れていれば
1193        // それも報告する(黙って既定色のままにしない)。
1194        if let Err(e) = load_colorset_from_json(json_default, &mut current_config.theme) {
1195            record_failure(&mut status, e);
1196        }
1197        if let Err(e) = load_colorset_from_json(json_default, &mut current_config.custom_theme) {
1198            record_failure(&mut status, e);
1199        }
1200    }
1201
1202    ConfigLoad {
1203        config: current_config,
1204        status,
1205    }
1206}
1207
1208/// 保存しようとしている設定が妥当かどうかを検査する。
1209///
1210/// 異常値のまま書き出すと、次回起動時に「なぜか設定が効かない」状態に
1211/// なってしまうため、**書き込む前に**弾いて呼び出し側へ返す。
1212pub fn validate_config(config: &OsConfig) -> Result<(), &'static str> {
1213    if config.theme_name.is_empty() {
1214        return Err("config: theme_name is empty");
1215    }
1216    for name in config.font_names.iter() {
1217        if name.is_empty() {
1218            return Err("config: font name is empty");
1219        }
1220    }
1221    Ok(())
1222}
1223
1224/// 設定を保存する。呼び出し側は**戻り値を必ず確認すること**
1225/// (保存できていないのに「保存しました」と表示しないため)。
1226pub fn save_config(
1227    layout: KeyboardLayout,
1228    theme_name: &str,
1229    font_names: &[String],
1230    enable_sound: bool,
1231    swap_caps_ctrl: bool,
1232    auto_dhcp: bool,
1233    enable_custom_colors: bool,
1234) -> Result<(), &'static str> {
1235    let current_custom = get_config().custom_theme;
1236    save_config_with_wifi(
1237        layout,
1238        theme_name,
1239        font_names,
1240        enable_sound,
1241        swap_caps_ctrl,
1242        auto_dhcp,
1243        "",
1244        "",
1245        &current_custom,
1246        enable_custom_colors,
1247    )
1248}
1249
1250/// 設定を WiFi 認証情報つきで保存する。
1251///
1252/// 異常な引数(空のテーマ名/空のフォント名/4 件以上のフォント指定)は
1253/// **1 バイトも書き込まずに** `Err` を返す。従来は 4 件目以降を黙って捨て、
1254/// 空文字も素通ししていた。
1255#[allow(clippy::too_many_arguments)]
1256pub fn save_config_with_wifi(
1257    layout: KeyboardLayout,
1258    theme_name: &str,
1259    font_names: &[String],
1260    enable_sound: bool,
1261    swap_caps_ctrl: bool,
1262    auto_dhcp: bool,
1263    wifi_ssid: &str,
1264    wifi_password: &str,
1265    custom_theme: &ThemeConfig,
1266    enable_custom_colors: bool,
1267) -> Result<(), &'static str> {
1268    if font_names.is_empty() {
1269        return Err("config: no font specified");
1270    }
1271    if font_names.len() > 3 {
1272        crate::error!(
1273            "[SYS] config: {} font names given (max 3)",
1274            font_names.len()
1275        );
1276        return Err("config: too many font names");
1277    }
1278
1279    let mut config = OsConfig {
1280        keyboard_layout: layout,
1281        theme_name: String::from(theme_name),
1282        custom_theme: *custom_theme,
1283        enable_custom_colors,
1284        ..Default::default()
1285    };
1286    for (i, n) in font_names.iter().enumerate() {
1287        config.font_names[i] = n.clone();
1288    }
1289    config.enable_sound = enable_sound;
1290    config.swap_caps_ctrl = swap_caps_ctrl;
1291    config.auto_dhcp = auto_dhcp;
1292    config.wifi_ssid = String::from(wifi_ssid);
1293    config.wifi_password = String::from(wifi_password);
1294
1295    if enable_custom_colors {
1296        config.theme = *custom_theme;
1297    } else {
1298        for t in PRESET_THEMES {
1299            if t.name == theme_name {
1300                config.theme = *t;
1301                break;
1302            }
1303        }
1304    }
1305
1306    validate_config(&config)?;
1307    save_config_internal(&config)
1308}
1309
1310/// 設定を JSON 化して `/os/config.json` へ書き出す。
1311/// **書き込み失敗はそのまま呼び出し側へ返す**(従来は `let _ =` で捨てていた)。
1312pub fn save_config_internal(config: &OsConfig) -> Result<(), &'static str> {
1313    let mut repeat_obj = BTreeMap::new();
1314    unsafe {
1315        repeat_obj.insert(
1316            String::from("first_delay_ms"),
1317            JsonValue::Number(crate::REPEAT_PARAMS.first_delay_ms as f64),
1318        );
1319        repeat_obj.insert(
1320            String::from("first_interval_ms"),
1321            JsonValue::Number(crate::REPEAT_PARAMS.first_interval_ms as f64),
1322        );
1323        repeat_obj.insert(
1324            String::from("second_repeat_count"),
1325            JsonValue::Number(crate::REPEAT_PARAMS.second_repeat_count as f64),
1326        );
1327        repeat_obj.insert(
1328            String::from("second_delay_ms"),
1329            JsonValue::Number(crate::REPEAT_PARAMS.second_delay_ms as f64),
1330        );
1331        repeat_obj.insert(
1332            String::from("second_interval_ms"),
1333            JsonValue::Number(crate::REPEAT_PARAMS.second_interval_ms as f64),
1334        );
1335    }
1336
1337    let mut custom_obj = BTreeMap::new();
1338    custom_obj.insert(
1339        String::from("desktop_bg"),
1340        JsonValue::Number(config.custom_theme.desktop_bg as f64),
1341    );
1342    custom_obj.insert(
1343        String::from("terminal_bg"),
1344        JsonValue::Number(config.custom_theme.terminal_bg as f64),
1345    );
1346    custom_obj.insert(
1347        String::from("terminal_fg"),
1348        JsonValue::Number(config.custom_theme.terminal_fg as f64),
1349    );
1350    custom_obj.insert(
1351        String::from("active_window_border"),
1352        JsonValue::Number(config.custom_theme.active_window_border as f64),
1353    );
1354    custom_obj.insert(
1355        String::from("non_active_window_border"),
1356        JsonValue::Number(config.custom_theme.non_active_window_border as f64),
1357    );
1358    custom_obj.insert(
1359        String::from("window_title_bar"),
1360        JsonValue::Number(config.custom_theme.window_title_bar as f64),
1361    );
1362    custom_obj.insert(
1363        String::from("app_bg"),
1364        JsonValue::Number(config.custom_theme.app_bg as f64),
1365    );
1366    custom_obj.insert(
1367        String::from("app_fg"),
1368        JsonValue::Number(config.custom_theme.app_fg as f64),
1369    );
1370    custom_obj.insert(
1371        String::from("button_bg"),
1372        JsonValue::Number(config.custom_theme.button_bg as f64),
1373    );
1374    custom_obj.insert(
1375        String::from("button_fg"),
1376        JsonValue::Number(config.custom_theme.button_fg as f64),
1377    );
1378    custom_obj.insert(
1379        String::from("button_active_bg"),
1380        JsonValue::Number(config.custom_theme.button_active_bg as f64),
1381    );
1382    custom_obj.insert(
1383        String::from("button_active_fg"),
1384        JsonValue::Number(config.custom_theme.button_active_fg as f64),
1385    );
1386    custom_obj.insert(
1387        String::from("focus_highlight"),
1388        JsonValue::Number(config.custom_theme.focus_highlight as f64),
1389    );
1390
1391    let mut system_obj = BTreeMap::new();
1392    let layout_str = match config.keyboard_layout {
1393        KeyboardLayout::Us => "us",
1394        KeyboardLayout::Jis => "jis",
1395    };
1396    system_obj.insert(
1397        String::from("keyboard_layout"),
1398        JsonValue::String(String::from(layout_str)),
1399    );
1400    system_obj.insert(
1401        String::from("theme_name"),
1402        JsonValue::String(config.theme_name.clone()),
1403    );
1404    system_obj.insert(
1405        String::from("font_names"),
1406        JsonValue::Array(
1407            config
1408                .font_names
1409                .iter()
1410                .map(|n| JsonValue::String(n.clone()))
1411                .collect(),
1412        ),
1413    );
1414    system_obj.insert(
1415        String::from("enable_sound"),
1416        JsonValue::Bool(config.enable_sound),
1417    );
1418    system_obj.insert(
1419        String::from("swap_caps_ctrl"),
1420        JsonValue::Bool(config.swap_caps_ctrl),
1421    );
1422    system_obj.insert(String::from("auto_dhcp"), JsonValue::Bool(config.auto_dhcp));
1423    system_obj.insert(
1424        String::from("enable_custom_colors"),
1425        JsonValue::Bool(config.enable_custom_colors),
1426    );
1427    system_obj.insert(
1428        String::from("wifi_ssid"),
1429        JsonValue::String(config.wifi_ssid.clone()),
1430    );
1431    system_obj.insert(
1432        String::from("wifi_password"),
1433        JsonValue::String(config.wifi_password.clone()),
1434    );
1435    system_obj.insert(String::from("keyrepeat"), JsonValue::Object(repeat_obj));
1436    system_obj.insert(String::from("custom_colors"), JsonValue::Object(custom_obj));
1437
1438    let mut root_obj = BTreeMap::new();
1439    root_obj.insert(String::from("system"), JsonValue::Object(system_obj));
1440
1441    let json_str = json::to_string(&JsonValue::Object(root_obj));
1442    let fs = fs::get_fs();
1443    if let Err(e) = fs.save_file("/os/config.json", json_str.as_bytes(), "system,config") {
1444        crate::error!("[SYS] config: failed to write /os/config.json: {}", e);
1445        return Err(e);
1446    }
1447    Ok(())
1448}