Follow the OS native theme on macOS, Windows and Linux

This commit is contained in:
2026-10-21 13:57:01 +07:00
parent f7865780d9
commit 6122474f9e
8 changed files with 797 additions and 513 deletions
+2
View File
@@ -6,6 +6,7 @@
### Added
- Follow the operating system's native theme on macOS, Windows and Linux (KDE, GNOME) via `native-theme`, applying the OS colors, fonts, radii and accessibility preferences; the bundled themes remain as the fallback when the OS theme cannot be read
- Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI
- Add a dropdown menu on the signed-in user button in the sidebar
- Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential
@@ -41,6 +42,7 @@
### Removed
- Remove the light/dark theme pickers and the font size, corner radius, focus ring and shadow settings from the settings dialog; colors and geometry now come from the native theme
- Remove dead code: the unused `login`/`logout` family, the unwired `SyncProgress` pipeline, `merge_pull_request`, inbox mark-read/archive APIs and the wasm32-only code paths
### Deprecated
Generated
+714 -104
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -9,13 +9,16 @@ edition = "2024"
publish = false
[workspace.dependencies]
gpui = { package = "gpui-pre", version = "0.3.9" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.9", features = ["font-kit", "x11", "wayland"] }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.9" }
gpui = { package = "gpui-pre", version = "0.3.8" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
# GPUI Kit
gpui-component = { version = "0.8", features = ["tree-sitter-languages"] }
gpui-base = "0.8"
# GPUI Kit; exact pins keep the graph on the gpui-pre 0.3.8 closure that
# native-theme-gpui 0.6.1 is verified against (0.7.3 and 0.8 pin 0.3.9).
gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
gpui-base = "=0.7.1"
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
dock = { path = "crates/dock" }
settings = { path = "crates/settings" }
+2 -1
View File
@@ -1,4 +1,5 @@
use std::cell::Cell;
use std::ops::Deref as _;
use std::rc::Rc;
use std::sync::Arc;
@@ -172,7 +173,7 @@ impl SignedDockSkin {
.on_drag(ResizePanel, move |info, _, _, cx| {
cx.stop_propagation();
shared.resizing_dock().set(Some(placement));
cx.new(|_| info.clone())
cx.new(|_| info.deref().clone())
})
}
}
@@ -1,22 +1,15 @@
use std::cell::Cell;
use std::path::PathBuf;
use std::rc::Rc;
use gpui::prelude::*;
use gpui::{
AnyWindowHandle, App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px,
};
use gpui::{App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px};
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::{
Input, InputEvent, InputState, NumberInput, NumberInputEvent, StepAction,
};
use gpui_component::input::{Input, InputState};
use gpui_component::select::{Select, SelectEvent, SelectState};
use gpui_component::separator::Separator;
use gpui_component::setting::NumberFieldOptions;
use gpui_component::switch::Switch;
use gpui_component::{
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex,
v_flex,
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, WindowExt, h_flex, v_flex,
};
use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore};
use signed_ui::{SelectOption, setting_block, setting_row};
@@ -31,34 +24,10 @@ fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
.map(|row| IndexPath::default().row(row))
}
/// Registered themes split into light and dark options, light first.
fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
let registry = ThemeRegistry::global(cx);
let mut light = Vec::new();
let mut dark = Vec::new();
for config in registry.themes().values() {
let name = config.name.clone();
if config.mode.is_dark() {
dark.push(SelectOption::new(name.clone(), name));
} else {
light.push(SelectOption::new(name.clone(), name));
}
}
light.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
dark.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
(light, dark)
}
/// Created once when the dialog opens, so control state survives re-renders.
struct SettingsControls {
appearance: Entity<SelectState<Vec<SelectOption>>>,
event_fetching: Entity<SelectState<Vec<SelectOption>>>,
light_theme: Entity<SelectState<Vec<SelectOption>>>,
dark_theme: Entity<SelectState<Vec<SelectOption>>>,
font_size: Entity<InputState>,
mono_font_size: Entity<InputState>,
radius: Entity<InputState>,
radius_lg: Entity<InputState>,
grasp_server_input: Entity<InputState>,
/// The effective create-repository folder, shown in a disabled input.
default_folder: Entity<InputState>,
@@ -107,38 +76,9 @@ impl SettingsControls {
)
});
let (light_options, dark_options) = theme_options(cx);
let light_theme = cx.new(|cx| {
SelectState::new(
light_options.clone(),
selected_index(&light_options, &settings.theme.light_theme),
window,
cx,
)
});
let dark_theme = cx.new(|cx| {
SelectState::new(
dark_options.clone(),
selected_index(&dark_options, &settings.theme.dark_theme),
window,
cx,
)
});
let font_size = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.font_size.to_string())
});
let mono_font_size = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.mono_font_size.to_string())
});
let radius = cx
.new(|cx| InputState::new(window, cx).default_value(settings.theme.radius.to_string()));
let radius_lg = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.radius_lg.to_string())
});
let grasp_server_input =
cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
let default_folder = cx.new(|cx| {
let folder = settings
.create_repository
@@ -178,118 +118,9 @@ impl SettingsControls {
}
}));
subscriptions.push(cx.subscribe(&light_theme, |_, event, cx| {
if let SelectEvent::Confirm(Some(value)) = event {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(
|settings| settings.theme.light_theme = value.to_string(),
cx,
);
});
apply_theme(cx);
}
}));
subscriptions.push(cx.subscribe(&dark_theme, |_, event, cx| {
if let SelectEvent::Confirm(Some(value)) = event {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.dark_theme = value.to_string(), cx);
});
apply_theme(cx);
}
}));
wire_number_input(
&font_size,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 10.,
max: 32.,
..Default::default()
},
settings.theme.font_size as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.font_size = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&mono_font_size,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 8.,
max: 32.,
..Default::default()
},
settings.theme.mono_font_size as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.mono_font_size = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&radius,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 0.,
max: 24.,
..Default::default()
},
settings.theme.radius as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.radius = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&radius_lg,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 0.,
max: 24.,
..Default::default()
},
settings.theme.radius_lg as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.radius_lg = value as f32, cx);
});
apply_theme(cx);
},
);
Self {
appearance,
event_fetching,
light_theme,
dark_theme,
font_size,
mono_font_size,
radius,
radius_lg,
grasp_server_input,
default_folder,
_subscriptions: subscriptions,
@@ -320,8 +151,6 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement
.w_full()
.child(appearance_section(controls, cx))
.child(Separator::horizontal())
.child(theme_section(&settings, controls, cx))
.child(Separator::horizontal())
.child(tab_bar_section(&settings, cx))
.child(Separator::horizontal())
.child(grasp_servers_section(&settings, controls, cx))
@@ -350,76 +179,6 @@ fn event_fetching_section(controls: &SettingsControls, cx: &App) -> impl IntoEle
))
}
fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> impl IntoElement {
v_flex()
.gap_3()
.w_full()
.child(setting_row(
cx,
"Light Theme",
"The theme to use when the appearance is light.",
Select::new(&controls.light_theme).w_full(),
))
.child(setting_row(
cx,
"Dark Theme",
"The theme to use when the appearance is dark.",
Select::new(&controls.dark_theme).w_full(),
))
.child(setting_row(
cx,
"UI Font Size",
"Font size for the UI.",
NumberInput::new(&controls.font_size).w_full(),
))
.child(setting_row(
cx,
"Editor Font Size",
"Font size for editor text.",
NumberInput::new(&controls.mono_font_size).w_full(),
))
.child(setting_row(
cx,
"Corner Radius",
"The corner radius for UI elements.",
NumberInput::new(&controls.radius).w_full(),
))
.child(setting_row(
cx,
"Large Corner Radius",
"The corner radius for large UI elements (dialogs, notifications).",
NumberInput::new(&controls.radius_lg).w_full(),
))
.child(setting_row(
cx,
"Focus Ring",
"Draw a ring around focused controls.",
Switch::new("focus-ring")
.checked(settings.theme.focus_ring)
.on_click(move |checked: &bool, _window, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.focus_ring = *checked, cx);
});
apply_theme(cx);
}),
))
.child(setting_row(
cx,
"Shadows",
"The shadow effect for UI elements.",
Switch::new("shadows")
.checked(settings.theme.shadow)
.on_click(move |checked: &bool, _window, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.shadow = *checked, cx);
});
apply_theme(cx);
}),
))
}
/// Tab bar chrome, such as its next/previous tab buttons.
fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement {
v_flex().w_full().gap_3().child(setting_row(
@@ -724,79 +483,6 @@ fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Windo
.detach();
}
/// Step actions clamp and persist the value; typed changes parse, clamp and persist.
fn wire_number_input(
state: &Entity<InputState>,
subscriptions: &mut Vec<Subscription>,
window_handle: AnyWindowHandle,
cx: &mut App,
options: NumberFieldOptions,
initial: f64,
on_change: impl Fn(f64, &mut App) + 'static,
) {
let on_change = Rc::new(on_change);
let initial_value = Rc::new(Cell::new(initial));
let (min, max, step) = (options.min, options.max, options.step);
subscriptions.push(cx.subscribe(&state.clone(), {
let state = state.clone();
let initial_value = initial_value.clone();
let on_change = on_change.clone();
move |_, event: &NumberInputEvent, cx| {
let NumberInputEvent::Step(action) = event;
let value = state.read(cx).value();
if let Ok(value) = value.parse::<f64>() {
let new_value = match action {
StepAction::Increment => value + step,
StepAction::Decrement => value - step,
};
let clamped = new_value.clamp(min, max);
window_handle
.update(cx, |_, window, cx| {
state.update(cx, |input, cx| {
input.set_value(SharedString::from(clamped.to_string()), window, cx);
});
})
.ok();
initial_value.set(clamped);
on_change(clamped, cx);
}
}
}));
subscriptions.push(cx.subscribe(&state.clone(), {
let state = state.clone();
let initial_value = initial_value.clone();
let on_change = on_change.clone();
move |_, event: &InputEvent, cx| {
if matches!(event, InputEvent::Change) {
let value = state.read(cx).value();
if value == initial_value.get().to_string() {
return;
}
if let Ok(parsed) = value.parse::<f64>() {
let clamped = parsed.clamp(min, max);
initial_value.set(clamped);
on_change(clamped, cx);
if clamped != parsed {
window_handle
.update(cx, |_, window, cx| {
state.update(cx, |input, cx| {
input.set_value(
SharedString::from(clamped.to_string()),
window,
cx,
);
});
})
.ok();
}
}
}
}
}));
}
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
match appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
@@ -804,33 +490,3 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
}
}
fn apply_theme(cx: &mut App) {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().theme.clone();
let registry = ThemeRegistry::global(cx);
let light_config = registry
.themes()
.get(settings.light_theme.as_str())
.cloned();
let dark_config = registry.themes().get(settings.dark_theme.as_str()).cloned();
let mode = Theme::global(cx).mode;
let theme = Theme::global_mut(cx);
theme.radius = px(settings.radius);
theme.radius_lg = px(settings.radius_lg);
theme.focus_ring = settings.focus_ring;
theme.shadow = settings.shadow;
theme.font_size = px(settings.font_size);
theme.mono_font_size = px(settings.mono_font_size);
if let Some(config) = light_config {
theme.light_theme = config;
}
if let Some(config) = dark_config {
theme.dark_theme = config;
}
// Re-apply the active mode so the updated configs take effect.
Theme::change(mode, None, cx);
}
+1
View File
@@ -38,6 +38,7 @@ gpui.workspace = true
gpui_platform.workspace = true
dock = { workspace = true }
gpui-component.workspace = true
native-theme-gpui.workspace = true
reqwest_client.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+37 -31
View File
@@ -5,6 +5,7 @@ use dock::TAB_BAR_HEIGHT;
use gpui::*;
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
use gpui_platform::application;
use native_theme_gpui::{SystemTheme, apply_system_theme};
use settings::{AppearanceMode, SettingsStore};
actions!(signed, [Quit]);
@@ -25,47 +26,51 @@ fn main() {
// The persisted settings must load before the theme is applied.
let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx));
SettingsStore::set_global(store.clone(), cx);
let settings = store.read(cx).settings().clone();
let settings = store.read(cx).settings().clone();
let registry = ThemeRegistry::global_mut(cx);
for (name, content) in Assets.themes() {
if let Err(err) = registry.load_themes_from_str(&content) {
tracing::error!("Failed to load theme {name}: {err}");
}
}
let light_theme = registry
.themes()
.get(settings.theme.light_theme.as_str())
.cloned();
let dark_theme = registry
.themes()
.get(settings.theme.dark_theme.as_str())
.cloned();
let theme = Theme::global_mut(cx);
theme.radius = px(settings.theme.radius);
theme.radius_lg = px(settings.theme.radius_lg);
theme.focus_ring = settings.theme.focus_ring;
theme.shadow = settings.theme.shadow;
theme.font_size = px(settings.theme.font_size);
theme.mono_font_size = px(settings.theme.mono_font_size);
match SystemTheme::from_system() {
Ok(system) => apply_system_theme(&system, cx),
Err(error) => {
tracing::warn!("native theme unavailable ({error})");
if let Some(light) = light_theme {
theme.light_theme = light;
} else {
tracing::warn!(
"{} theme is missing from the registry",
settings.theme.light_theme
);
}
let light_theme = registry
.themes()
.get(settings.theme.light_theme.as_str())
.cloned();
if let Some(dark) = dark_theme {
theme.dark_theme = dark;
} else {
tracing::warn!(
"{} theme is missing from the registry",
settings.theme.dark_theme
);
let dark_theme = registry
.themes()
.get(settings.theme.dark_theme.as_str())
.cloned();
let theme = Theme::global_mut(cx);
theme.radius = px(settings.theme.radius);
theme.radius_lg = px(settings.theme.radius_lg);
theme.focus_ring = settings.theme.focus_ring;
theme.shadow = settings.theme.shadow;
theme.font_size = px(settings.theme.font_size);
theme.mono_font_size = px(settings.theme.mono_font_size);
if let Some(light) = light_theme {
theme.light_theme = light;
} else {
tracing::warn!("{} theme is missing", settings.theme.light_theme);
}
if let Some(dark) = dark_theme {
theme.dark_theme = dark;
} else {
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
}
}
}
match settings.appearance {
@@ -76,6 +81,7 @@ fn main() {
std::fs::create_dir_all(paths::nostr_dir()).ok();
std::fs::create_dir_all(paths::repos_dir()).ok();
signed_state::init(
paths::nostr_dir(),
paths::repos_dir().clone(),
+28 -23
View File
@@ -58,7 +58,8 @@ Verified against crates.io and the upstream repo on 2026-10-21:
| --- | --- |
| Latest connector | `native-theme-gpui` 0.6.1 (2026-10-09); upstream `main` is the same |
| Connector requires | `gpui-component ^0.7.1`, `gpui-base ^0.7.1`, `gpui-pre ^0.3.8`, Rust ≥ 1.95 |
| `gpui-component` 0.7.x pins | `gpui-pre =0.3.8` |
| `gpui-component`/`gpui-base` 0.7.1 pin | `gpui-pre =0.3.8` |
| `gpui-component`/`gpui-base` 0.7.3 pin | `gpui-pre =0.3.9` |
| `gpui-component` 0.8.0 pins | `gpui-pre =0.3.9` (0.8.0 published 2026-10-19) |
| This branch | moved 0.7.1 / 0.3.8 → 0.8 / 0.3.9 in `eb4fbb0` ("chore: update deps") |
@@ -79,23 +80,25 @@ gpui = { package = "gpui-pre", version = "0.3.8" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
gpui-component = { version = "0.7.1", features = ["tree-sitter-languages"] }
gpui-base = "0.7.1"
gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
gpui-base = "=0.7.1"
native-theme = "0.6.1"
native-theme-gpui = "0.6.1"
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
```
- This is exactly the state of `master` before `eb4fbb0`, which is also the
version set the connector was verified against (2026-10-08 stamp) and that
its nightly dependency canary exercises. `"0.7.1"` is a caret requirement;
it resolves the newest 0.7.x (currently 0.7.3). Pin `=0.7.1` instead if
strict reproducibility is preferred.
- The three `gpui-pre-*` family crates are published in lockstep; all three
move back to 0.3.8 together.
- This is the state of `master` before `eb4fbb0`, the version set the
connector was verified against (2026-10-08 stamp). The exact `=0.7.1`
pins are required, not a style choice: a caret `"0.7.1"` resolves 0.7.3,
whose `gpui-pre =0.3.9` pin drags the graph onto 0.3.9, outside the
closure the connector is verified against. The three `gpui-pre-*` family
crates are published in lockstep; all three move back to 0.3.8 together.
- The connector re-exports the `native-theme` types that appear in its
public signatures (`SystemTheme`, `AccessibilityPreferences`, …), so the
app does **not** need `native-theme` as a direct dependency.
- The cost of the revert is the two one-line tweaks `eb4fbb0` made for 0.8
(the `Deref` removal in `crates/dock/src/dock_area.rs`; verify it compiles
either way). The nostr changes in that commit are unrelated and stay.
(the `Deref` removal in `crates/dock/src/dock_area.rs` is restored; the
0.7.1 `ResizeHandle::on_drag` hands the closure an `Rc<T>` and requires
`E: Render`). The nostr changes in that commit are unrelated and stay.
- When `native-theme-gpui` releases gpui-component 0.8 support (upstream has
bumped within days of each gpui-component release so far), bump both sides
together in one commit.
@@ -128,8 +131,8 @@ Order is load-bearing:
4. Apply the native theme:
```rust
let prefs = native_theme_gpui::AccessibilityPreferences::from_system();
match native_theme::SystemTheme::from_system() {
// native_theme_gpui re-exports SystemTheme from native-theme.
match native_theme_gpui::SystemTheme::from_system() {
Ok(system) => native_theme_gpui::apply_system_theme(&system, cx),
Err(error) => {
tracing::warn!("native theme unavailable ({error}); using bundled themes");
@@ -182,9 +185,11 @@ Remove (the native theme now owns all of these):
size. Keeping manual overrides would require re-applying them after every
upstream rebuild, a permanent fight with the connector.
`apply_theme()` shrinks to the fallback-only body (registry lookups + global
field setup), called only when native detection failed at startup and by the
Appearance handler's fallback branch.
`apply_theme()` and `theme_options()` are deleted outright: with the pickers
and number fields gone they have no remaining callers. The fallback logic
lives only in `main.rs`'s `Err` branch; the Appearance handler keeps working
through `apply_appearance()` alone (`Theme::change` picks up whatever configs
are installed, bundled or native).
`ThemeSettings` keeps its fields and serde shape — the fallback branch still
reads them, existing settings files keep parsing, and the tests in
@@ -224,8 +229,8 @@ Adopting any of them is a separate, deliberate follow-up.
| File | Change |
| --- | --- |
| `Cargo.toml` | Revert gpui-side pins to 0.3.8 / 0.7.1 (Gate 0); add `native-theme` and `native-theme-gpui` workspace deps |
| `desktop/Cargo.toml` | Add `native-theme`, `native-theme-gpui` |
| `Cargo.toml` | Revert gpui-side pins to 0.3.8 / `=0.7.1` (Gate 0); add the `native-theme-gpui` workspace dep |
| `desktop/Cargo.toml` | Add `native-theme-gpui` |
| `desktop/src/main.rs` | Native apply with bundled fallback (§4.2); move the settings-field setup into the fallback branch |
| `crates/workspace/src/views/sidebar/settings_dialog.rs` | Remove theme picker + font/radius/focus/shadow rows; shrink `apply_theme` to fallback-only (§4.4) |
| `CHANGELOG.md` | Added: native OS theme via `native-theme`; Removed: manual theme/font/radius settings |
@@ -240,8 +245,8 @@ Unchanged:
## 6. Validation
1. `cargo check`, `cargo test`, `cargo clippy` across the workspace (toolchain
1.96 satisfies the connector's 1.95 MSRV).
1. `cargo check`, `cargo test --workspace`, `cargo clippy` across the workspace
(toolchain 1.96 satisfies the connector's 1.95 MSRV).
2. Manual matrix, per platform available (macOS first; KDE and GNOME Linux;
Windows):
- cold start matches the OS theme and accent; radius, fonts and controls