mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-18 13:07:06 +04:00
Detect and respond to terminal theme mode (light/dark) updates
This commit is contained in:
parent
6d07ae4f07
commit
0caa2fe9be
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -239,11 +239,11 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
source = "git+https://github.com/the-mikedavis/crossterm?branch=md/theme-mode#dd0ae541da4b54f35eb93a29cd717f0e32d2ed77"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"document-features",
|
||||
"filedescriptor",
|
||||
"futures-core",
|
||||
"libc",
|
||||
@ -289,6 +289,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@ -1866,6 +1875,12 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
|
@ -43,6 +43,9 @@ nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "2.0"
|
||||
|
||||
[patch.crates-io]
|
||||
crossterm = { git = "https://github.com/the-mikedavis/crossterm", branch = "md/theme-mode" }
|
||||
|
||||
[workspace.package]
|
||||
version = "24.7.0"
|
||||
edition = "2021"
|
||||
|
@ -74,6 +74,9 @@ pub struct Application {
|
||||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
lsp_progress: LspProgressMap,
|
||||
|
||||
/// The theme mode (light/dark) detected from the terminal, if available.
|
||||
theme_mode: Option<theme::Mode>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
@ -105,15 +108,24 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Se
|
||||
|
||||
use helix_view::editor::Action;
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
let backend = TestBackend::new(120, 150);
|
||||
|
||||
let mut theme_parent_dirs = vec![helix_loader::config_dir()];
|
||||
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
|
||||
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
|
||||
|
||||
let theme_mode = backend.get_theme_mode();
|
||||
|
||||
let true_color = config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
.and_then(|theme_config| {
|
||||
let theme = theme_config.choose(theme_mode);
|
||||
theme_loader
|
||||
.load(theme)
|
||||
.map_err(|e| {
|
||||
@ -127,12 +139,6 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Se
|
||||
|
||||
let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader));
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
let backend = TestBackend::new(120, 150);
|
||||
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let area = terminal.size().expect("couldn't get terminal size");
|
||||
let mut compositor = Compositor::new(area);
|
||||
@ -151,7 +157,10 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Se
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.keys
|
||||
}));
|
||||
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
|
||||
let editor_view = Box::new(ui::EditorView::new(
|
||||
Keymaps::new(keys),
|
||||
Map::new(Arc::clone(&config), |config: &Config| &config.theme),
|
||||
));
|
||||
compositor.push(editor_view);
|
||||
|
||||
if args.load_tutor {
|
||||
@ -267,6 +276,7 @@ pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Se
|
||||
signals,
|
||||
jobs: Jobs::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
theme_mode,
|
||||
};
|
||||
|
||||
Ok(app)
|
||||
@ -434,7 +444,9 @@ fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> {
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
.and_then(|theme_config| {
|
||||
let theme = theme_config.choose(self.theme_mode);
|
||||
|
||||
self.theme_loader
|
||||
.load(theme)
|
||||
.map_err(|e| {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::keymap;
|
||||
use crate::keymap::{merge_keys, KeyTrie};
|
||||
use helix_loader::merge_toml_values;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::{document::Mode, theme};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
pub theme: Option<String>,
|
||||
pub theme: Option<theme::Config>,
|
||||
pub keys: HashMap<Mode, KeyTrie>,
|
||||
pub editor: helix_view::editor::Config,
|
||||
}
|
||||
@ -19,7 +19,7 @@ pub struct Config {
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigRaw {
|
||||
pub theme: Option<String>,
|
||||
pub theme: Option<theme::Config>,
|
||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||
pub editor: Option<toml::Value>,
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
use arc_swap::access::DynAccess;
|
||||
use helix_core::{
|
||||
diagnostic::NumberOrString,
|
||||
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
|
||||
@ -29,7 +30,7 @@
|
||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
Document, Editor, Theme, View,
|
||||
theme, Document, Editor, Theme, View,
|
||||
};
|
||||
use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
@ -37,6 +38,7 @@
|
||||
|
||||
pub struct EditorView {
|
||||
pub keymaps: Keymaps,
|
||||
theme_config: Box<dyn DynAccess<Option<theme::Config>>>,
|
||||
on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>,
|
||||
pseudo_pending: Vec<KeyEvent>,
|
||||
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
|
||||
@ -58,9 +60,13 @@ pub enum InsertEvent {
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new(keymaps: Keymaps) -> Self {
|
||||
pub fn new(
|
||||
keymaps: Keymaps,
|
||||
theme_config: impl DynAccess<Option<theme::Config>> + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
keymaps,
|
||||
theme_config: Box::new(theme_config),
|
||||
on_next_key: None,
|
||||
pseudo_pending: Vec::new(),
|
||||
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
|
||||
@ -1528,6 +1534,18 @@ fn handle_event(
|
||||
self.terminal_focused = false;
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::ThemeModeChanged(theme_mode) => {
|
||||
if let Some(theme_config) = self.theme_config.load().as_ref() {
|
||||
let theme_name = theme_config.choose(Some(*theme_mode));
|
||||
match context.editor.theme_loader.load(theme_name) {
|
||||
Ok(theme) => context.editor.set_theme(theme),
|
||||
Err(err) => {
|
||||
log::warn!("failed to load theme `{}` - {}", theme_name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, SetCursorStyle, Show},
|
||||
event::{
|
||||
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags,
|
||||
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
DisableBracketedPaste, DisableColorSchemeUpdates, DisableFocusChange, DisableMouseCapture,
|
||||
EnableBracketedPaste, EnableColorSchemeUpdates, EnableFocusChange, EnableMouseCapture,
|
||||
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
},
|
||||
execute, queue,
|
||||
style::{
|
||||
@ -17,6 +17,7 @@
|
||||
use helix_view::{
|
||||
editor::Config as EditorConfig,
|
||||
graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle},
|
||||
theme,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{
|
||||
@ -161,7 +162,8 @@ fn claim(&mut self, config: Config) -> io::Result<()> {
|
||||
execute!(
|
||||
self.buffer,
|
||||
terminal::EnterAlternateScreen,
|
||||
EnableFocusChange
|
||||
EnableFocusChange,
|
||||
EnableColorSchemeUpdates,
|
||||
)?;
|
||||
match execute!(self.buffer, EnableBracketedPaste,) {
|
||||
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
|
||||
@ -217,6 +219,7 @@ fn restore(&mut self, config: Config) -> io::Result<()> {
|
||||
execute!(
|
||||
self.buffer,
|
||||
DisableFocusChange,
|
||||
DisableColorSchemeUpdates,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
terminal::disable_raw_mode()
|
||||
@ -232,7 +235,12 @@ fn force_restore() -> io::Result<()> {
|
||||
let _ = execute!(stdout, DisableMouseCapture);
|
||||
let _ = execute!(stdout, PopKeyboardEnhancementFlags);
|
||||
let _ = execute!(stdout, DisableBracketedPaste);
|
||||
execute!(stdout, DisableFocusChange, terminal::LeaveAlternateScreen)?;
|
||||
execute!(
|
||||
stdout,
|
||||
DisableFocusChange,
|
||||
DisableColorSchemeUpdates,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
@ -338,6 +346,29 @@ fn size(&self) -> io::Result<Rect> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn get_theme_mode(&self) -> Option<theme::Mode> {
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let theme_mode = crossterm::terminal::query_terminal_theme_mode()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|theme_mode| match theme_mode {
|
||||
crossterm::event::ThemeMode::Light => theme::Mode::Light,
|
||||
crossterm::event::ThemeMode::Dark => theme::Mode::Dark,
|
||||
});
|
||||
let elapsed = Instant::now().duration_since(start).as_millis();
|
||||
if theme_mode.is_some() {
|
||||
log::debug!("detected terminal theme mode in {}ms", elapsed);
|
||||
} else {
|
||||
log::debug!(
|
||||
"failed to detect terminal theme mode (checked in {}ms)",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
theme_mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
use crate::{buffer::Cell, terminal::Config};
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use helix_view::{
|
||||
graphics::{CursorKind, Rect},
|
||||
theme,
|
||||
};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
@ -27,4 +30,5 @@ fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
fn get_theme_mode(&self) -> Option<theme::Mode>;
|
||||
}
|
||||
|
@ -164,4 +164,8 @@ fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
|
||||
pub enum Event {
|
||||
@ -14,6 +15,7 @@ pub enum Event {
|
||||
Mouse(MouseEvent),
|
||||
Paste(String),
|
||||
Resize(u16, u16),
|
||||
ThemeModeChanged(theme::Mode),
|
||||
IdleTimeout,
|
||||
}
|
||||
|
||||
@ -468,6 +470,12 @@ fn from(event: crossterm::event::Event) -> Self {
|
||||
crossterm::event::Event::FocusGained => Self::FocusGained,
|
||||
crossterm::event::Event::FocusLost => Self::FocusLost,
|
||||
crossterm::event::Event::Paste(s) => Self::Paste(s),
|
||||
crossterm::event::Event::ThemeModeChanged(theme_mode) => {
|
||||
Self::ThemeModeChanged(match theme_mode {
|
||||
crossterm::event::ThemeMode::Light => theme::Mode::Light,
|
||||
crossterm::event::ThemeMode::Dark => theme::Mode::Dark,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,50 @@
|
||||
..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
|
||||
});
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum Mode {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
light: String,
|
||||
dark: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn choose(&self, preference: Option<Mode>) -> &str {
|
||||
match preference {
|
||||
Some(Mode::Light) => &self.light,
|
||||
Some(Mode::Dark) | None => &self.dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
enum InnerConfig {
|
||||
Constant(String),
|
||||
Adaptive { dark: String, light: String },
|
||||
}
|
||||
|
||||
let inner = InnerConfig::deserialize(deserializer)?;
|
||||
|
||||
let (light, dark) = match inner {
|
||||
InnerConfig::Constant(theme) => (theme.clone(), theme),
|
||||
InnerConfig::Adaptive { light, dark } => (light, dark),
|
||||
};
|
||||
|
||||
Ok(Self { light, dark })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Loader {
|
||||
/// Theme directories to search from highest to lowest priority
|
||||
|
Loading…
Reference in New Issue
Block a user