Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>Tauri + React + Typescript</title>
</head>

Expand Down
2 changes: 2 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ tauri-plugin-updater = "2"
cpal = "0.15"
whisper-rs = "0.12"
sha2 = "0.10"

[target."cfg(target_os = \"macos\")".dependencies]
objc2-app-kit = { version = "0.3", features = ["NSAppearance", "NSResponder", "NSWindow"] }
objc2-foundation = { version = "0.3", features = ["NSString"] }
10 changes: 3 additions & 7 deletions src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[allow(dead_code)]
#[path = "../backend/mod.rs"]
mod backend;
#[path = "../codex_home.rs"]
Expand All @@ -8,6 +9,7 @@ mod codex_config;
mod rules;
#[path = "../storage.rs"]
mod storage;
#[allow(dead_code)]
#[path = "../types.rs"]
mod types;

Expand Down Expand Up @@ -42,6 +44,7 @@ struct DaemonEventSink {
#[derive(Clone)]
enum DaemonEvent {
AppServer(AppServerEvent),
#[allow(dead_code)]
TerminalOutput(TerminalOutput),
}

Expand Down Expand Up @@ -1088,13 +1091,6 @@ fn parse_optional_u32(value: &Value, key: &str) -> Option<u32> {
}
}

fn parse_optional_bool(value: &Value, key: &str) -> Option<bool> {
match value {
Value::Object(map) => map.get(key).and_then(|value| value.as_bool()),
_ => None,
}
}

fn parse_optional_string_array(value: &Value, key: &str) -> Option<Vec<String>> {
match value {
Value::Object(map) => map.get(key).and_then(|value| value.as_array()).map(|items| {
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod rules;
mod settings;
mod state;
mod terminal;
mod window;
mod storage;
mod types;
mod utils;
Expand Down
11 changes: 9 additions & 2 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use tauri::State;
use tauri::{State, Window};

use crate::codex_config;
use crate::state::AppState;
use crate::storage::write_settings;
use crate::types::AppSettings;
use crate::window;

#[tauri::command]
pub(crate) async fn get_app_settings(state: State<'_, AppState>) -> Result<AppSettings, String> {
pub(crate) async fn get_app_settings(
state: State<'_, AppState>,
window: Window,
) -> Result<AppSettings, String> {
let mut settings = state.app_settings.lock().await.clone();
if let Ok(Some(collab_enabled)) = codex_config::read_collab_enabled() {
settings.experimental_collab_enabled = collab_enabled;
Expand All @@ -17,19 +21,22 @@ pub(crate) async fn get_app_settings(state: State<'_, AppState>) -> Result<AppSe
if let Ok(Some(unified_exec_enabled)) = codex_config::read_unified_exec_enabled() {
settings.experimental_unified_exec_enabled = unified_exec_enabled;
}
let _ = window::apply_window_appearance(&window, settings.theme.as_str());
Ok(settings)
}

#[tauri::command]
pub(crate) async fn update_app_settings(
settings: AppSettings,
state: State<'_, AppState>,
window: Window,
) -> Result<AppSettings, String> {
let _ = codex_config::write_collab_enabled(settings.experimental_collab_enabled);
let _ = codex_config::write_steer_enabled(settings.experimental_steer_enabled);
let _ = codex_config::write_unified_exec_enabled(settings.experimental_unified_exec_enabled);
write_settings(&state.settings_path, &settings)?;
let mut current = state.app_settings.lock().await;
*current = settings.clone();
let _ = window::apply_window_appearance(&window, settings.theme.as_str());
Ok(settings)
}
8 changes: 8 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ pub(crate) struct AppSettings {
pub(crate) last_composer_reasoning_effort: Option<String>,
#[serde(default = "default_ui_scale", rename = "uiScale")]
pub(crate) ui_scale: f64,
#[serde(default = "default_theme", rename = "theme")]
pub(crate) theme: String,
#[serde(
default = "default_notification_sounds_enabled",
rename = "notificationSoundsEnabled"
Expand Down Expand Up @@ -325,6 +327,10 @@ fn default_ui_scale() -> f64 {
1.0
}

fn default_theme() -> String {
"system".to_string()
}

fn default_composer_model_shortcut() -> Option<String> {
Some("cmd+shift+m".to_string())
}
Expand Down Expand Up @@ -383,6 +389,7 @@ impl Default for AppSettings {
last_composer_model_id: None,
last_composer_reasoning_effort: None,
ui_scale: 1.0,
theme: default_theme(),
notification_sounds_enabled: true,
experimental_collab_enabled: false,
experimental_steer_enabled: false,
Expand Down Expand Up @@ -425,6 +432,7 @@ mod tests {
assert!(settings.last_composer_model_id.is_none());
assert!(settings.last_composer_reasoning_effort.is_none());
assert!((settings.ui_scale - 1.0).abs() < f64::EPSILON);
assert_eq!(settings.theme, "system");
assert!(settings.notification_sounds_enabled);
assert!(!settings.experimental_steer_enabled);
assert!(!settings.dictation_enabled);
Expand Down
53 changes: 53 additions & 0 deletions src-tauri/src/window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use tauri::{Theme, Window};

#[cfg(target_os = "macos")]
fn apply_macos_window_appearance(window: &Window, theme: &str) -> Result<(), String> {
use objc2_app_kit::{
NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua,
NSAppearanceNameDarkAqua, NSWindow,
};

let ns_window = window
.ns_window()
.map_err(|error| error.to_string())?;
let ns_window: &NSWindow = unsafe { &*ns_window.cast() };

if theme == "system" {
ns_window.setAppearance(None);
return Ok(());
}

let appearance_name = unsafe {
if theme == "dark" {
NSAppearanceNameDarkAqua
} else {
NSAppearanceNameAqua
}
};
let appearance =
NSAppearance::appearanceNamed(appearance_name).ok_or("NSAppearance missing")?;
ns_window.setAppearance(Some(&appearance));
Ok(())
}

pub(crate) fn apply_window_appearance(window: &Window, theme: &str) -> Result<(), String> {
let next_theme = match theme {
"light" => Some(Theme::Light),
"dark" => Some(Theme::Dark),
_ => None,
};
let _ = window.set_theme(next_theme);

#[cfg(target_os = "macos")]
{
let window_handle = window.clone();
let theme_value = theme.to_string();
window
.run_on_main_thread(move || {
let _ = apply_macos_window_appearance(&window_handle, theme_value.as_str());
})
.map_err(|error| error.to_string())?;
}

Ok(())
}
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { useResizablePanels } from "./features/layout/hooks/useResizablePanels";
import { useLayoutMode } from "./features/layout/hooks/useLayoutMode";
import { useSidebarToggles } from "./features/layout/hooks/useSidebarToggles";
import { useTransparencyPreference } from "./features/layout/hooks/useTransparencyPreference";
import { useThemePreference } from "./features/layout/hooks/useThemePreference";
import { useWindowLabel } from "./features/layout/hooks/useWindowLabel";
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import {
Expand Down Expand Up @@ -107,6 +108,7 @@ function MainApp() {
doctor,
isLoading: appSettingsLoading
} = useAppSettings();
useThemePreference(appSettings.theme);
const dictationModel = useDictationModel(appSettings.dictationModelId);
const {
state: dictationState,
Expand Down
13 changes: 13 additions & 0 deletions src/features/layout/hooks/useThemePreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from "react";
import type { ThemePreference } from "../../../types";

export function useThemePreference(theme: ThemePreference) {
useEffect(() => {
const root = document.documentElement;
if (theme === "system") {
delete root.dataset.theme;
return;
}
root.dataset.theme = theme;
}, [theme]);
}
20 changes: 20 additions & 0 deletions src/features/settings/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,26 @@ export function SettingsView({
<div className="settings-subsection-subtitle">
Adjust how the window renders backgrounds and effects.
</div>
<div className="settings-field">
<label className="settings-field-label" htmlFor="theme-select">
Theme
</label>
<select
id="theme-select"
className="settings-select"
value={appSettings.theme}
onChange={(event) =>
void onUpdateAppSettings({
...appSettings,
theme: event.target.value as AppSettings["theme"],
})
}
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="settings-toggle-row">
<div>
<div className="settings-toggle-title">Reduce transparency</div>
Expand Down
4 changes: 4 additions & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AppSettings } from "../../../types";
import { getAppSettings, runCodexDoctor, updateAppSettings } from "../../../services/tauri";
import { clampUiScale, UI_SCALE_DEFAULT } from "../../../utils/uiScale";

const allowedThemes = new Set(["system", "light", "dark"]);

const defaultSettings: AppSettings = {
codexBin: null,
backendMode: "local",
Expand All @@ -15,6 +17,7 @@ const defaultSettings: AppSettings = {
lastComposerModelId: null,
lastComposerReasoningEffort: null,
uiScale: UI_SCALE_DEFAULT,
theme: "system",
notificationSoundsEnabled: true,
experimentalCollabEnabled: false,
experimentalSteerEnabled: false,
Expand All @@ -30,6 +33,7 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
return {
...settings,
uiScale: clampUiScale(settings.uiScale),
theme: allowedThemes.has(settings.theme) ? settings.theme : "system",
};
}

Expand Down
Loading