mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
1 Commits
filesentry
...
mode-2031
Author | SHA1 | Date | |
---|---|---|---|
|
7b4f90ee79 |
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
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>`.
|
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. Defaults to the theme set for `dark` if not specified.
|
||||||
|
# no-preference = "catppuccin_frappe"
|
||||||
|
```
|
||||||
|
|
||||||
## Creating a theme
|
## 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.
|
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.
|
||||||
|
@@ -67,6 +67,8 @@ pub struct Application {
|
|||||||
signals: Signals,
|
signals: Signals,
|
||||||
jobs: Jobs,
|
jobs: Jobs,
|
||||||
lsp_progress: LspProgressMap,
|
lsp_progress: LspProgressMap,
|
||||||
|
|
||||||
|
theme_mode: Option<theme::Mode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "integration")]
|
#[cfg(feature = "integration")]
|
||||||
@@ -109,6 +111,7 @@ impl Application {
|
|||||||
#[cfg(feature = "integration")]
|
#[cfg(feature = "integration")]
|
||||||
let backend = TestBackend::new(120, 150);
|
let backend = TestBackend::new(120, 150);
|
||||||
|
|
||||||
|
let theme_mode = backend.get_theme_mode();
|
||||||
let terminal = Terminal::new(backend)?;
|
let terminal = Terminal::new(backend)?;
|
||||||
let area = terminal.size().expect("couldn't get terminal size");
|
let area = terminal.size().expect("couldn't get terminal size");
|
||||||
let mut compositor = Compositor::new(area);
|
let mut compositor = Compositor::new(area);
|
||||||
@@ -127,6 +130,7 @@ impl Application {
|
|||||||
&mut editor,
|
&mut editor,
|
||||||
&config.load(),
|
&config.load(),
|
||||||
terminal.backend().supports_true_color(),
|
terminal.backend().supports_true_color(),
|
||||||
|
theme_mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||||
@@ -246,6 +250,7 @@ impl Application {
|
|||||||
signals,
|
signals,
|
||||||
jobs: Jobs::new(),
|
jobs: Jobs::new(),
|
||||||
lsp_progress: LspProgressMap::new(),
|
lsp_progress: LspProgressMap::new(),
|
||||||
|
theme_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(app)
|
Ok(app)
|
||||||
@@ -404,6 +409,7 @@ impl Application {
|
|||||||
&mut self.editor,
|
&mut self.editor,
|
||||||
&default_config,
|
&default_config,
|
||||||
self.terminal.backend().supports_true_color(),
|
self.terminal.backend().supports_true_color(),
|
||||||
|
self.theme_mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-parse any open documents with the new language config.
|
// Re-parse any open documents with the new language config.
|
||||||
@@ -437,12 +443,18 @@ impl Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load the theme set in configuration
|
/// 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 true_color = terminal_true_color || config.editor.true_color || crate::true_color();
|
||||||
let theme = config
|
let theme = config
|
||||||
.theme
|
.theme
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|theme| {
|
.and_then(|theme_config| {
|
||||||
|
let theme = theme_config.choose(mode);
|
||||||
editor
|
editor
|
||||||
.theme_loader
|
.theme_loader
|
||||||
.load(theme)
|
.load(theme)
|
||||||
@@ -643,6 +655,8 @@ impl Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_terminal_events(&mut self, event: std::io::Result<termina::Event>) {
|
pub async fn handle_terminal_events(&mut self, event: std::io::Result<termina::Event>) {
|
||||||
|
use termina::escape::csi;
|
||||||
|
|
||||||
let mut cx = crate::compositor::Context {
|
let mut cx = crate::compositor::Context {
|
||||||
editor: &mut self.editor,
|
editor: &mut self.editor,
|
||||||
jobs: &mut self.jobs,
|
jobs: &mut self.jobs,
|
||||||
@@ -667,6 +681,15 @@ impl Application {
|
|||||||
kind: termina::event::KeyEventKind::Release,
|
kind: termina::event::KeyEventKind::Release,
|
||||||
..
|
..
|
||||||
}) => false,
|
}) => false,
|
||||||
|
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
|
||||||
|
}
|
||||||
event => self.compositor.handle_event(&event.into(), &mut cx),
|
event => self.compositor.handle_event(&event.into(), &mut cx),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1117,9 +1140,16 @@ impl Application {
|
|||||||
|
|
||||||
#[cfg(not(feature = "integration"))]
|
#[cfg(not(feature = "integration"))]
|
||||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
|
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
|
||||||
use termina::Terminal as _;
|
use termina::{escape::csi, Terminal as _};
|
||||||
let reader = self.terminal.backend().terminal().event_reader();
|
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(feature = "integration")]
|
#[cfg(feature = "integration")]
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use crate::keymap;
|
use crate::keymap;
|
||||||
use crate::keymap::{merge_keys, KeyTrie};
|
use crate::keymap::{merge_keys, KeyTrie};
|
||||||
use helix_loader::merge_toml_values;
|
use helix_loader::merge_toml_values;
|
||||||
use helix_view::document::Mode;
|
use helix_view::{document::Mode, theme};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -11,7 +11,7 @@ use toml::de::Error as TomlError;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub theme: Option<String>,
|
pub theme: Option<theme::Config>,
|
||||||
pub keys: HashMap<Mode, KeyTrie>,
|
pub keys: HashMap<Mode, KeyTrie>,
|
||||||
pub editor: helix_view::editor::Config,
|
pub editor: helix_view::editor::Config,
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ pub struct Config {
|
|||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ConfigRaw {
|
pub struct ConfigRaw {
|
||||||
pub theme: Option<String>,
|
pub theme: Option<theme::Config>,
|
||||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||||
pub editor: Option<toml::Value>,
|
pub editor: Option<toml::Value>,
|
||||||
}
|
}
|
||||||
|
@@ -39,4 +39,5 @@ pub trait Backend {
|
|||||||
/// Flushes the terminal buffer
|
/// Flushes the terminal buffer
|
||||||
fn flush(&mut self) -> Result<(), io::Error>;
|
fn flush(&mut self) -> Result<(), io::Error>;
|
||||||
fn supports_true_color(&self) -> bool;
|
fn supports_true_color(&self) -> bool;
|
||||||
|
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode>;
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ use std::io::{self, Write as _};
|
|||||||
|
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
graphics::{CursorKind, Rect, UnderlineStyle},
|
graphics::{CursorKind, Rect, UnderlineStyle},
|
||||||
theme::{Color, Modifier},
|
theme::{self, Color, Modifier},
|
||||||
};
|
};
|
||||||
use termina::{
|
use termina::{
|
||||||
escape::{
|
escape::{
|
||||||
@@ -67,6 +67,7 @@ struct Capabilities {
|
|||||||
synchronized_output: bool,
|
synchronized_output: bool,
|
||||||
true_color: bool,
|
true_color: bool,
|
||||||
extended_underlines: bool,
|
extended_underlines: bool,
|
||||||
|
theme_mode: Option<theme::Mode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -154,13 +155,15 @@ impl TerminaBackend {
|
|||||||
// If we only receive the device attributes then we know it is not.
|
// If we only receive the device attributes then we know it is not.
|
||||||
write!(
|
write!(
|
||||||
terminal,
|
terminal,
|
||||||
"{}{}{}{}{}{}{}",
|
"{}{}{}{}{}{}{}{}",
|
||||||
// Kitty keyboard
|
// Kitty keyboard
|
||||||
Csi::Keyboard(csi::Keyboard::QueryFlags),
|
Csi::Keyboard(csi::Keyboard::QueryFlags),
|
||||||
// Synchronized output
|
// Synchronized output
|
||||||
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
|
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
|
||||||
csi::DecPrivateModeCode::SynchronizedOutput
|
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:
|
// True color and while we're at it, extended underlines:
|
||||||
// <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
|
// <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
|
||||||
Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
|
Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
|
||||||
@@ -192,6 +195,9 @@ impl TerminaBackend {
|
|||||||
})) => {
|
})) => {
|
||||||
capabilities.synchronized_output = true;
|
capabilities.synchronized_output = true;
|
||||||
}
|
}
|
||||||
|
Event::Csi(Csi::Mode(csi::Mode::ReportTheme(mode))) => {
|
||||||
|
capabilities.theme_mode = Some(mode.into());
|
||||||
|
}
|
||||||
Event::Dcs(dcs::Dcs::Response {
|
Event::Dcs(dcs::Dcs::Response {
|
||||||
value: dcs::DcsResponse::GraphicRendition(sgrs),
|
value: dcs::DcsResponse::GraphicRendition(sgrs),
|
||||||
..
|
..
|
||||||
@@ -317,6 +323,11 @@ impl TerminaBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.capabilities.theme_mode.is_some() {
|
||||||
|
// Enable mode 2031 theme mode notifications:
|
||||||
|
write!(self.terminal, "{}", decset!(Theme))?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +340,11 @@ impl TerminaBackend {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.capabilities.theme_mode.is_some() {
|
||||||
|
// Mode 2031 theme notifications.
|
||||||
|
write!(self.terminal, "{}", decreset!(Theme))?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +563,10 @@ impl Backend for TerminaBackend {
|
|||||||
fn supports_true_color(&self) -> bool {
|
fn supports_true_color(&self) -> bool {
|
||||||
self.capabilities.true_color
|
self.capabilities.true_color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_theme_mode(&self) -> Option<theme::Mode> {
|
||||||
|
self.capabilities.theme_mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TerminaBackend {
|
impl Drop for TerminaBackend {
|
||||||
|
@@ -160,4 +160,8 @@ impl Backend for TestBackend {
|
|||||||
fn supports_true_color(&self) -> bool {
|
fn supports_true_color(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,75 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
|
|||||||
..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
|
..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 {
|
||||||
|
pub light: String,
|
||||||
|
pub 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.
|
||||||
|
pub no_preference: 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.no_preference.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,
|
||||||
|
no_preference: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner = InnerConfig::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let (light, dark, no_preference) = match inner {
|
||||||
|
InnerConfig::Constant(theme) => (theme.clone(), theme.clone(), None),
|
||||||
|
InnerConfig::Adaptive {
|
||||||
|
light,
|
||||||
|
dark,
|
||||||
|
no_preference,
|
||||||
|
} => (light, dark, no_preference),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
light,
|
||||||
|
dark,
|
||||||
|
no_preference,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Loader {
|
pub struct Loader {
|
||||||
/// Theme directories to search from highest to lowest priority
|
/// Theme directories to search from highest to lowest priority
|
||||||
|
Reference in New Issue
Block a user