Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--book/src/themes.md11
-rw-r--r--helix-term/src/application.rs40
-rw-r--r--helix-term/src/config.rs6
-rw-r--r--helix-tui/src/backend/crossterm.rs4
-rw-r--r--helix-tui/src/backend/mod.rs1
-rw-r--r--helix-tui/src/backend/termina.rs24
-rw-r--r--helix-tui/src/backend/test.rs4
-rw-r--r--helix-view/src/theme.rs69
8 files changed, 150 insertions, 9 deletions
diff --git a/book/src/themes.md b/book/src/themes.md
index 8140120b..353a4684 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -2,6 +2,17 @@
To use a theme add `theme = "<name>"` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme <name>`.
+Separate themes can be configured for light and dark modes. On terminals supporting [mode 2031 dark/light detection](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md), the theme mode is detected from the terminal.
+
+```toml
+[theme]
+dark = "catppuccin_frappe"
+light = "catppuccin_latte"
+## Optional. Used if the terminal doesn't declare a preference.
+## Defaults to the theme set for `dark` if not specified.
+# fallback = "catppuccin_frappe"
+```
+
## Creating a theme
Create a file with the name of your theme as the file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes` or `%AppData%\helix\themes` on Windows). The directory might have to be created beforehand.
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 9ee02a53..cf8ab233 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -77,6 +77,8 @@ pub struct Application {
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
+
+ theme_mode: Option<theme::Mode>,
}
#[cfg(feature = "integration")]
@@ -121,6 +123,7 @@ impl Application {
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
+ let theme_mode = backend.get_theme_mode();
let terminal = Terminal::new(backend)?;
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
@@ -139,6 +142,7 @@ impl Application {
&mut editor,
&config.load(),
terminal.backend().supports_true_color(),
+ theme_mode,
);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@@ -258,6 +262,7 @@ impl Application {
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
+ theme_mode,
};
Ok(app)
@@ -416,6 +421,7 @@ impl Application {
&mut self.editor,
&default_config,
self.terminal.backend().supports_true_color(),
+ self.theme_mode,
);
// Re-parse any open documents with the new language config.
@@ -449,12 +455,18 @@ impl Application {
}
/// Load the theme set in configuration
- fn load_configured_theme(editor: &mut Editor, config: &Config, terminal_true_color: bool) {
+ fn load_configured_theme(
+ editor: &mut Editor,
+ config: &Config,
+ terminal_true_color: bool,
+ mode: Option<theme::Mode>,
+ ) {
let true_color = terminal_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(mode);
editor
.theme_loader
.load(theme)
@@ -672,6 +684,9 @@ impl Application {
}
pub async fn handle_terminal_events(&mut self, event: std::io::Result<TerminalEvent>) {
+ #[cfg(not(windows))]
+ use termina::escape::csi;
+
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
@@ -698,6 +713,16 @@ impl Application {
kind: termina::event::KeyEventKind::Release,
..
}) => false,
+ #[cfg(not(windows))]
+ termina::Event::Csi(csi::Csi::Mode(csi::Mode::ReportTheme(mode))) => {
+ Self::load_configured_theme(
+ &mut self.editor,
+ &self.config.load(),
+ self.terminal.backend().supports_true_color(),
+ Some(mode.into()),
+ );
+ true
+ }
#[cfg(windows)]
TerminalEvent::Resize(width, height) => {
self.terminal
@@ -1167,9 +1192,16 @@ impl Application {
#[cfg(all(not(feature = "integration"), not(windows)))]
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin {
- use termina::Terminal as _;
+ use termina::{escape::csi, Terminal as _};
let reader = self.terminal.backend().terminal().event_reader();
- termina::EventStream::new(reader, |event| !event.is_escape())
+ termina::EventStream::new(reader, |event| {
+ // Accept either non-escape sequences or theme mode updates.
+ !event.is_escape()
+ || matches!(
+ event,
+ termina::Event::Csi(csi::Csi::Mode(csi::Mode::ReportTheme(_)))
+ )
+ })
}
#[cfg(all(not(feature = "integration"), windows))]
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index bcba8d8e..dd051984 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -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 @@ use toml::de::Error as TomlError;
#[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>,
}
diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs
index 943821b9..3b53c21f 100644
--- a/helix-tui/src/backend/crossterm.rs
+++ b/helix-tui/src/backend/crossterm.rs
@@ -324,6 +324,10 @@ where
fn supports_true_color(&self) -> bool {
false
}
+
+ fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
+ None
+ }
}
#[derive(Debug)]
diff --git a/helix-tui/src/backend/mod.rs b/helix-tui/src/backend/mod.rs
index 37160d4f..368a1b66 100644
--- a/helix-tui/src/backend/mod.rs
+++ b/helix-tui/src/backend/mod.rs
@@ -44,4 +44,5 @@ pub trait Backend {
/// Flushes the terminal buffer
fn flush(&mut self) -> Result<(), io::Error>;
fn supports_true_color(&self) -> bool;
+ fn get_theme_mode(&self) -> Option<helix_view::theme::Mode>;
}
diff --git a/helix-tui/src/backend/termina.rs b/helix-tui/src/backend/termina.rs
index 52912161..ad1c7c68 100644
--- a/helix-tui/src/backend/termina.rs
+++ b/helix-tui/src/backend/termina.rs
@@ -3,7 +3,7 @@ use std::io::{self, Write as _};
use helix_view::{
editor::KittyKeyboardProtocolConfig,
graphics::{CursorKind, Rect, UnderlineStyle},
- theme::{Color, Modifier},
+ theme::{self, Color, Modifier},
};
use termina::{
escape::{
@@ -52,6 +52,7 @@ struct Capabilities {
synchronized_output: bool,
true_color: bool,
extended_underlines: bool,
+ theme_mode: Option<theme::Mode>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -148,11 +149,13 @@ impl TerminaBackend {
// If we only receive the device attributes then we know it is not.
write!(
terminal,
- "{}{}{}{}{}{}",
+ "{}{}{}{}{}{}{}",
// Synchronized output
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
csi::DecPrivateModeCode::SynchronizedOutput
))),
+ // Mode 2031 theme updates. Query the current theme.
+ Csi::Mode(csi::Mode::QueryTheme),
// True color and while we're at it, extended underlines:
// <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
@@ -184,6 +187,9 @@ impl TerminaBackend {
})) => {
capabilities.synchronized_output = true;
}
+ Event::Csi(Csi::Mode(csi::Mode::ReportTheme(mode))) => {
+ capabilities.theme_mode = Some(mode.into());
+ }
Event::Dcs(dcs::Dcs::Response {
value: dcs::DcsResponse::GraphicRendition(sgrs),
..
@@ -320,6 +326,11 @@ impl TerminaBackend {
}
}
+ if self.capabilities.theme_mode.is_some() {
+ // Enable mode 2031 theme mode notifications:
+ write!(self.terminal, "{}", decset!(Theme))?;
+ }
+
Ok(())
}
@@ -332,6 +343,11 @@ impl TerminaBackend {
)?;
}
+ if self.capabilities.theme_mode.is_some() {
+ // Mode 2031 theme notifications.
+ write!(self.terminal, "{}", decreset!(Theme))?;
+ }
+
Ok(())
}
@@ -550,6 +566,10 @@ impl Backend for TerminaBackend {
fn supports_true_color(&self) -> bool {
self.capabilities.true_color
}
+
+ fn get_theme_mode(&self) -> Option<theme::Mode> {
+ self.capabilities.theme_mode
+ }
}
impl Drop for TerminaBackend {
diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs
index 37b5ff5c..b048cefc 100644
--- a/helix-tui/src/backend/test.rs
+++ b/helix-tui/src/backend/test.rs
@@ -160,4 +160,8 @@ impl Backend for TestBackend {
fn supports_true_color(&self) -> bool {
false
}
+
+ fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
+ None
+ }
}
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index e2e10932..173a40f3 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -35,6 +35,75 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
});
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Mode {
+ Dark,
+ Light,
+}
+
+#[cfg(feature = "term")]
+impl From<termina::escape::csi::ThemeMode> for Mode {
+ fn from(mode: termina::escape::csi::ThemeMode) -> Self {
+ match mode {
+ termina::escape::csi::ThemeMode::Dark => Self::Dark,
+ termina::escape::csi::ThemeMode::Light => Self::Light,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Config {
+ light: String,
+ dark: String,
+ /// A theme to choose when the terminal did not declare either light or dark mode.
+ /// When not specified the dark theme is preferred.
+ fallback: Option<String>,
+}
+
+impl Config {
+ pub fn choose(&self, preference: Option<Mode>) -> &str {
+ match preference {
+ Some(Mode::Light) => &self.light,
+ Some(Mode::Dark) => &self.dark,
+ None => self.fallback.as_ref().unwrap_or(&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, rename_all = "kebab-case")]
+ enum InnerConfig {
+ Constant(String),
+ Adaptive {
+ dark: String,
+ light: String,
+ fallback: Option<String>,
+ },
+ }
+
+ let inner = InnerConfig::deserialize(deserializer)?;
+
+ let (light, dark, fallback) = match inner {
+ InnerConfig::Constant(theme) => (theme.clone(), theme.clone(), None),
+ InnerConfig::Adaptive {
+ light,
+ dark,
+ fallback,
+ } => (light, dark, fallback),
+ };
+
+ Ok(Self {
+ light,
+ dark,
+ fallback,
+ })
+ }
+}
#[derive(Clone, Debug)]
pub struct Loader {
/// Theme directories to search from highest to lowest priority