From ec2659a095c1e073f8918469e2528c277a76567f Mon Sep 17 00:00:00 2001 From: Tommy Han Date: Fri, 13 Mar 2026 03:35:42 +0800 Subject: [PATCH] Add hotkeys and actions for toggle light and dark theme (#49027) Mentioned in #47258 Release Notes: - Added hotkey options and actions for toggling light and dark theme. - Add default keymap as `cmd/ctrl+k cmd/ctrl+shift+t` --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/default-windows.json | 1 + crates/theme/src/settings.rs | 12 ++-- crates/workspace/src/workspace.rs | 91 ++++++++++++++++++++++++++++- crates/zed/src/zed.rs | 1 + crates/zed_actions/src/lib.rs | 6 ++ docs/src/appearance.md | 8 ++- docs/src/themes.md | 29 +++++++++ 9 files changed, 139 insertions(+), 11 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5780eedb4445f613cbbd4e9a09976f2d475b28c7..0516221b6e0849ab631c021d020050be99aaf728 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -624,6 +624,7 @@ "ctrl-shift-t": "pane::ReopenClosedItem", "ctrl-k ctrl-s": "zed::OpenKeymap", "ctrl-k ctrl-t": "theme_selector::Toggle", + "ctrl-k ctrl-shift-t": "theme::ToggleMode", "ctrl-alt-super-p": "settings_profile_selector::Toggle", "ctrl-t": "project_symbols::Toggle", "ctrl-p": "file_finder::Toggle", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 6fc6905dd5f4502ff7ee90e7f6f9499b2e03fa6a..a4aec7cfe8053f3f23b43652f7e58f319c9691f6 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -691,6 +691,7 @@ "cmd-shift-t": "pane::ReopenClosedItem", "cmd-k cmd-s": "zed::OpenKeymap", "cmd-k cmd-t": "theme_selector::Toggle", + "cmd-k cmd-shift-t": "theme::ToggleMode", "ctrl-alt-cmd-p": "settings_profile_selector::Toggle", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index ac23d45695e11ec46172c566282ea65bf7774ac8..c10054d5813c6deae33b7a790b3639e7f2c802aa 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -616,6 +616,7 @@ "ctrl-shift-t": "pane::ReopenClosedItem", "ctrl-k ctrl-s": "zed::OpenKeymap", "ctrl-k ctrl-t": "theme_selector::Toggle", + "ctrl-k ctrl-shift-t": "theme::ToggleMode", "ctrl-alt-super-p": "settings_profile_selector::Toggle", "ctrl-t": "project_symbols::Toggle", "ctrl-p": "file_finder::Toggle", diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index a092e2698722a980f0b2a4b5ea64b9bfa0f33d01..c09d3daf6074f24248de12e56ebc2122e2c123e7 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -378,14 +378,14 @@ pub fn set_mode(content: &mut SettingsContent, mode: ThemeAppearanceMode) { if let Some(selection) = theme.theme.as_mut() { match selection { - settings::ThemeSelection::Static(theme) => { + settings::ThemeSelection::Static(_) => { // If the theme was previously set to a single static theme, - // we don't know whether it was a light or dark theme, so we - // just use it for both. + // reset to the default dynamic light/dark pair and let users + // customize light/dark themes explicitly afterward. *selection = settings::ThemeSelection::Dynamic { - mode, - light: theme.clone(), - dark: theme.clone(), + mode: ThemeAppearanceMode::System, + light: ThemeName(settings::DEFAULT_LIGHT_THEME.into()), + dark: ThemeName(settings::DEFAULT_DARK_THEME.into()), }; } settings::ThemeSelection::Dynamic { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b57b5028a4e5558b1f90c715463165ba68d914e3..949dc127a7465c4cf3941ee4c4982fad37d06281 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -146,7 +146,7 @@ pub use workspace_settings::{ AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, WorkspaceSettings, }; -use zed_actions::{Spawn, feedback::FileBugReport}; +use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode}; use crate::{item::ItemBufferKind, notifications::NotificationId}; use crate::{ @@ -6499,6 +6499,7 @@ impl Workspace { .on_action(cx.listener(Self::move_item_to_pane_at_index)) .on_action(cx.listener(Self::move_focused_panel_to_next_position)) .on_action(cx.listener(Self::toggle_edit_predictions_all_files)) + .on_action(cx.listener(Self::toggle_theme_mode)) .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| { let pane = workspace.active_pane().clone(); workspace.unfollow_in_pane(&pane, window, cx); @@ -7153,6 +7154,23 @@ impl Workspace { }); } + fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context) { + let current_mode = ThemeSettings::get_global(cx).theme.mode(); + let next_mode = match current_mode { + Some(theme::ThemeAppearanceMode::Light) => theme::ThemeAppearanceMode::Dark, + Some(theme::ThemeAppearanceMode::Dark) => theme::ThemeAppearanceMode::Light, + Some(theme::ThemeAppearanceMode::System) | None => match cx.theme().appearance() { + theme::Appearance::Light => theme::ThemeAppearanceMode::Dark, + theme::Appearance::Dark => theme::ThemeAppearanceMode::Light, + }, + }; + + let fs = self.project().read(cx).fs().clone(); + settings::update_settings_file(fs, cx, move |settings, _cx| { + theme::set_mode(settings, next_mode); + }); + } + pub fn show_worktree_trust_security_modal( &mut self, toggle: bool, @@ -9964,7 +9982,7 @@ pub fn with_active_or_new_workspace( #[cfg(test)] mod tests { - use std::{cell::RefCell, rc::Rc}; + use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration}; use super::*; use crate::{ @@ -9982,6 +10000,7 @@ mod tests { use project::{Project, ProjectEntryId}; use serde_json::json; use settings::SettingsStore; + use util::path; use util::rel_path::rel_path; #[gpui::test] @@ -13540,6 +13559,74 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) { + use settings::{ThemeName, ThemeSelection}; + use theme::SystemAppearance; + use zed_actions::theme::ToggleMode; + + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let settings_fs: Arc = fs.clone(); + + fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" })) + .await; + + // Build a test project and workspace view so the test can invoke + // the workspace action handler the same way the UI would. + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + // Seed the settings file with a plain static light theme so the + // first toggle always starts from a known persisted state. + workspace.update_in(cx, |_workspace, _window, cx| { + *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light); + settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| { + settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into()))); + }); + }); + cx.executor().advance_clock(Duration::from_millis(200)); + cx.run_until_parked(); + + // Confirm the initial persisted settings contain the static theme + // we just wrote before any toggling happens. + let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap(); + assert!(settings_text.contains(r#""theme": "One Light""#)); + + // Toggle once. This should migrate the persisted theme settings + // into light/dark slots and enable system mode. + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_theme_mode(&ToggleMode, window, cx); + }); + cx.executor().advance_clock(Duration::from_millis(200)); + cx.run_until_parked(); + + // 1. Static -> Dynamic + // this assertion checks theme changed from static to dynamic. + let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap(); + let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap(); + assert_eq!( + parsed["theme"], + serde_json::json!({ + "mode": "system", + "light": "One Light", + "dark": "One Dark" + }) + ); + + // 2. Toggle again, suppose it will change the mode to light + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_theme_mode(&ToggleMode, window, cx); + }); + cx.executor().advance_clock(Duration::from_millis(200)); + cx.run_until_parked(); + + let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap(); + assert!(settings_text.contains(r#""mode": "light""#)); + } + fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity { let item = TestProjectItem::new(id, path, cx); item.update(cx, |item, _| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 25defa1dde5977bd94935dafd60d97ae84b5a323..511b0edc6ac168fa47b52e66c9632487de86acf4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4878,6 +4878,7 @@ mod tests { "task", "terminal", "terminal_panel", + "theme", "theme_selector", "toast", "toolchain", diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 854f71175e79c84f03261a3d58f89638b7259e54..8edc80b4ec7816cd9e2ae2d7b995dd74b8128a9a 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -325,6 +325,12 @@ pub mod feedback { ); } +pub mod theme { + use gpui::actions; + + actions!(theme, [ToggleMode]); +} + pub mod theme_selector { use gpui::Action; use schemars::JsonSchema; diff --git a/docs/src/appearance.md b/docs/src/appearance.md index fdf5e239ccf581988e439845d0c2f94e4bb1b95c..1c26d67100379462298c4026dbf578b936b61fb1 100644 --- a/docs/src/appearance.md +++ b/docs/src/appearance.md @@ -15,11 +15,13 @@ Here's how to make Zed feel like home: 1. **Pick a theme**: Press {#kb theme_selector::Toggle} to open the Theme Selector. Arrow through the list to preview themes in real time, and press Enter to apply. -2. **Choose an icon theme**: Run `icon theme selector: toggle` from the command palette to browse icon themes. +2. **Toggle light/dark mode quickly**: Press {#kb theme::ToggleMode}. If you currently use a static `"theme": "..."` value, the first toggle converts it to dynamic mode settings with default themes. -3. **Set your font**: Open the Settings Editor with {#kb zed::OpenSettings} and search for `buffer_font_family`. Set it to your preferred coding font. +3. **Choose an icon theme**: Run `icon theme selector: toggle` from the command palette to browse icon themes. -4. **Adjust font size**: In the same Settings Editor, search for `buffer_font_size` and `ui_font_size` to tweak the editor and interface text sizes. +4. **Set your font**: Open the Settings Editor with {#kb zed::OpenSettings} and search for `buffer_font_family`. Set it to your preferred coding font. + +5. **Adjust font size**: In the same Settings Editor, search for `buffer_font_size` and `ui_font_size` to tweak the editor and interface text sizes. That's it. You now have a personalized Zed setup. diff --git a/docs/src/themes.md b/docs/src/themes.md index 0d3103eaab46fefff22095d14cab02f799ef851d..1dd2c144e2a2a53a50e21f6fc51f3b0c121eca25 100644 --- a/docs/src/themes.md +++ b/docs/src/themes.md @@ -44,6 +44,35 @@ You can set the mode to `"dark"` or `"light"` to ignore the current system mode. } ``` +### Toggle Theme Mode from the Keyboard + +Use {#kb theme::ToggleMode} to switch the current theme mode between light and dark. + +If your settings currently use a static theme value, like: + +```json [settings] +{ + "theme": "Any Theme" +} +``` + +the first toggle converts it to dynamic theme selection with default themes: + +```json [settings] +{ + "theme": { + "mode": "system", + "light": "One Light", + "dark": "One Dark" + } +} +``` + +You are required to set both `light` and `dark` themes manually after the first toggle. + +After that, toggling updates only `theme.mode`. +If `light` and `dark` are the same theme, the first toggle may not produce a visible UI change until you set different values for `light` and `dark`. + ## Theme Overrides To override specific attributes of a theme, use the `theme_overrides` setting.