mirror of
https://github.com/helix-editor/crossterm.git
synced 2025-10-06 00:12:40 +02:00
Support querying and detecting updates to terminal theme mode
This change adds support for dark and light mode detection by means of a VT extension proposed[^1] by the Contour terminal. This has four parts: * A command to subscribe to color scheme updates * The inverse: a command to unsubscribe * A new event emitted when subscribed in a terminal supporting the extension, upon direction of that terminal that its color scheme has changed. * A function under `terminal` to query the current theme mode - this should be used on application startup to determine the initial theme mode. [^1]: https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
This commit is contained in:
70
src/event.rs
70
src/event.rs
@@ -54,6 +54,7 @@
|
||||
//! #[cfg(feature = "bracketed-paste")]
|
||||
//! Event::Paste(data) => println!("{:?}", data),
|
||||
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
|
||||
//! Event::ThemeModeChanged(mode) => println!("New theme mode {:?}", mode),
|
||||
//! }
|
||||
//! }
|
||||
//! execute!(
|
||||
@@ -100,6 +101,7 @@
|
||||
//! #[cfg(feature = "bracketed-paste")]
|
||||
//! Event::Paste(data) => println!("Pasted {:?}", data),
|
||||
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
|
||||
//! Event::ThemeModeChanged(mode) => println!("New theme mode {:?}", mode),
|
||||
//! }
|
||||
//! } else {
|
||||
//! // Timeout expired and no `Event` is available
|
||||
@@ -542,6 +544,60 @@ impl Command for PopKeyboardEnhancementFlags {
|
||||
}
|
||||
}
|
||||
|
||||
/// A command which subscribes to updates of the terminal's selected theme mode (dark/light).
|
||||
///
|
||||
/// See [`ThemeMode`] for more information.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct EnableThemeModeUpdates;
|
||||
|
||||
impl Command for EnableThemeModeUpdates {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2031h"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
use std::io;
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Theme mode updates are not implemented for the legacy Windows API",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A command which unsubscribes to updates of the terminal's selected theme mode (dark/light).
|
||||
///
|
||||
/// See [ThemeMode] and [EnableThemeModeUpdates] for more information.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DisableThemeModeUpdates;
|
||||
|
||||
impl Command for DisableThemeModeUpdates {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2031l"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
use std::io;
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Theme mode updates are not implemented for the legacy Windows API",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an event.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "derive-more", derive(IsVariant))]
|
||||
@@ -563,6 +619,7 @@ pub enum Event {
|
||||
/// An resize event with new dimensions after resize (columns, rows).
|
||||
/// **Note** that resize events can occur in batches.
|
||||
Resize(u16, u16),
|
||||
ThemeModeChanged(ThemeMode),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
@@ -1486,6 +1543,19 @@ pub(crate) enum InternalEvent {
|
||||
PrimaryDeviceAttributes,
|
||||
}
|
||||
|
||||
/// The selected color scheme of the terminal.
|
||||
///
|
||||
/// This information can be queried from terminals implementing Contour's [VT extension for theme
|
||||
/// mode updates](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md).
|
||||
/// Applications can subscribe to updates to the theme mode with [EnableThemeModeUpdates] and
|
||||
/// unsubscribe with [DisableThemeModeUpdates].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
@@ -46,6 +46,25 @@ impl Filter for PrimaryDeviceAttributesFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ThemeModeFilter;
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Filter for ThemeModeFilter {
|
||||
fn eval(&self, event: &InternalEvent) -> bool {
|
||||
use crate::event::Event;
|
||||
// See `KeyboardEnhancementFlagsFilter` above: `PrimaryDeviceAttributes` is
|
||||
// used to elicit a response from the terminal even if it doesn't support the
|
||||
// theme mode query.
|
||||
matches!(
|
||||
*event,
|
||||
InternalEvent::Event(Event::ThemeModeChanged(_))
|
||||
| InternalEvent::PrimaryDeviceAttributes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct EventFilter;
|
||||
|
||||
|
@@ -2,7 +2,7 @@ use std::io;
|
||||
|
||||
use crate::event::{
|
||||
Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyboardEnhancementFlags,
|
||||
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
|
||||
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind, ThemeMode,
|
||||
};
|
||||
|
||||
use super::super::super::InternalEvent;
|
||||
@@ -180,6 +180,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
b'?' => match buffer[buffer.len() - 1] {
|
||||
b'u' => return parse_csi_keyboard_enhancement_flags(buffer),
|
||||
b'c' => return parse_csi_primary_device_attributes(buffer),
|
||||
b'n' => return parse_csi_theme_mode(buffer),
|
||||
_ => None,
|
||||
},
|
||||
b'0'..=b'9' => {
|
||||
@@ -300,6 +301,32 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Inter
|
||||
Ok(Some(InternalEvent::PrimaryDeviceAttributes))
|
||||
}
|
||||
|
||||
fn parse_csi_theme_mode(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
// dark mode: CSI ? 997 ; 1 n
|
||||
// light mode: CSI ? 997 ; 2 n
|
||||
assert!(buffer.starts_with(b"\x1B[?"));
|
||||
assert!(buffer.ends_with(b"n"));
|
||||
|
||||
let s = std::str::from_utf8(&buffer[3..buffer.len() - 1])
|
||||
.map_err(|_| could_not_parse_event_error())?;
|
||||
|
||||
let mut split = s.split(';');
|
||||
|
||||
if next_parsed::<u16>(&mut split)? != 997 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let theme_mode = match next_parsed::<u8>(&mut split)? {
|
||||
1 => ThemeMode::Dark,
|
||||
2 => ThemeMode::Light,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(InternalEvent::Event(Event::ThemeModeChanged(
|
||||
theme_mode,
|
||||
))))
|
||||
}
|
||||
|
||||
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
||||
let modifier_mask = mask.saturating_sub(1);
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
|
@@ -99,7 +99,9 @@ use crate::{csi, impl_display};
|
||||
pub(crate) mod sys;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub use sys::{query_keyboard_enhancement_flags, supports_keyboard_enhancement};
|
||||
pub use sys::{
|
||||
query_keyboard_enhancement_flags, query_terminal_theme_mode, supports_keyboard_enhancement,
|
||||
};
|
||||
|
||||
/// Tells whether the raw mode is enabled.
|
||||
///
|
||||
|
@@ -6,7 +6,9 @@ pub(crate) use self::unix::{
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::unix::{query_keyboard_enhancement_flags, supports_keyboard_enhancement};
|
||||
pub use self::unix::{
|
||||
query_keyboard_enhancement_flags, query_terminal_theme_mode, supports_keyboard_enhancement,
|
||||
};
|
||||
#[cfg(all(windows, test))]
|
||||
pub(crate) use self::windows::temp_screen_buffer;
|
||||
#[cfg(windows)]
|
||||
@@ -16,7 +18,9 @@ pub(crate) use self::windows::{
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::windows::{query_keyboard_enhancement_flags, supports_keyboard_enhancement};
|
||||
pub use self::windows::{
|
||||
query_keyboard_enhancement_flags, query_terminal_theme_mode, supports_keyboard_enhancement,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
//! UNIX related logic for terminal manipulation.
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
use crate::event::KeyboardEnhancementFlags;
|
||||
use crate::event::{KeyboardEnhancementFlags, ThemeMode};
|
||||
use crate::terminal::{
|
||||
sys::file_descriptor::{tty_fd, FileDesc},
|
||||
WindowSize,
|
||||
@@ -180,6 +180,71 @@ fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the currently selected theme mode (dark/light) from the terminal.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn query_terminal_theme_mode() -> io::Result<Option<ThemeMode>> {
|
||||
if is_raw_mode_enabled() {
|
||||
query_terminal_theme_mode_raw()
|
||||
} else {
|
||||
query_terminal_theme_mode_nonraw()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_terminal_theme_mode_nonraw() -> io::Result<Option<ThemeMode>> {
|
||||
enable_raw_mode()?;
|
||||
let theme_mode = query_terminal_theme_mode_raw();
|
||||
disable_raw_mode()?;
|
||||
theme_mode
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_terminal_theme_mode_raw() -> io::Result<Option<ThemeMode>> {
|
||||
use crate::event::{
|
||||
filter::{PrimaryDeviceAttributesFilter, ThemeModeFilter},
|
||||
poll_internal, read_internal, Event, InternalEvent,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
// ESC [ ? 996 n Query current terminal theme mode
|
||||
// ESC [ c Query primary device attributes (widely supported)
|
||||
const QUERY: &[u8] = b"\x1B[?996n\x1B[c";
|
||||
|
||||
let result = File::open("/dev/tty").and_then(|mut file| {
|
||||
file.write_all(QUERY)?;
|
||||
file.flush()
|
||||
});
|
||||
if result.is_err() {
|
||||
let mut stdout = io::stdout();
|
||||
stdout.write_all(QUERY)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match poll_internal(Some(Duration::from_millis(2000)), &ThemeModeFilter) {
|
||||
Ok(true) => match read_internal(&ThemeModeFilter) {
|
||||
Ok(InternalEvent::Event(Event::ThemeModeChanged(theme_mode))) => {
|
||||
// Flush the PrimaryDeviceAttributes out of the event queue.
|
||||
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
||||
return Ok(Some(theme_mode));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
},
|
||||
Ok(false) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The theme mode could not be read in a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
|
@@ -10,7 +10,7 @@ use winapi::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
use crate::event::KeyboardEnhancementFlags;
|
||||
use crate::event::{KeyboardEnhancementFlags, ThemeMode};
|
||||
use crate::{
|
||||
cursor,
|
||||
terminal::{ClearType, WindowSize},
|
||||
@@ -86,6 +86,14 @@ pub fn query_keyboard_enhancement_flags() -> io::Result<Option<KeyboardEnhanceme
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Queries the currently selected theme mode (dark/light) from the terminal.
|
||||
///
|
||||
/// This always returns `Ok(None)` on Windows.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn query_terminal_theme_mode() -> io::Result<Option<ThemeMode>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
Reference in New Issue
Block a user