From 10a78b5dbc53905ef023e53f41f8b59f355396ad Mon Sep 17 00:00:00 2001 From: Sam Mohr Date: Mon, 1 Jan 2024 06:07:06 -0800 Subject: [PATCH] feat: support ANSI themes (#460) --- yazi-core/Cargo.toml | 2 +- yazi-plugin/Cargo.toml | 4 +- yazi-plugin/src/external/highlighter.rs | 105 ++++++++++++++++++++---- yazi-plugin/src/utils/preview.rs | 22 ++--- 4 files changed, 102 insertions(+), 31 deletions(-) diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 81ad2873..7e60a52e 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -28,7 +28,7 @@ parking_lot = "^0" ratatui = "^0" regex = "^1" serde = "^1" -syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } +syntect = { version = "^5", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs", "process", "io-std", "io-util" ] } tokio-stream = "^0" tokio-util = "^0" diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 75c47a63..e777ba3e 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -25,9 +25,9 @@ parking_lot = "^0" ratatui = "^0" serde = "^1" serde_json = "^1" -syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } +syntect = { version = "^5", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } tokio = { version = "^1", features = [ "parking_lot", "rt-multi-thread" ] } tokio-util = "^0" tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } unicode-width = "^0" -yazi-prebuild = "^0" +yazi-prebuild = "0.1.2" diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index a04b89ff..1fc52154 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,9 +1,10 @@ -use std::{mem, path::{Path, PathBuf}, sync::{atomic::{AtomicUsize, Ordering}, OnceLock}}; +use std::{io::Cursor, mem, path::{Path, PathBuf}, sync::{atomic::{AtomicUsize, Ordering}, OnceLock}}; use anyhow::{anyhow, Result}; -use syntect::{dumps::from_uncompressed_data, easy::HighlightLines, highlighting::{Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}, util::as_24_bit_terminal_escaped}; +use ratatui::text::{Line, Span, Text}; +use syntect::{dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}}; -use yazi_config::THEME; +use yazi_config::{PREVIEW, THEME}; use yazi_shared::PeekError; static INCR: AtomicUsize = AtomicUsize::new(0); @@ -26,10 +27,13 @@ impl Highlighter { } let theme = SYNTECT_THEME.get_or_init(|| { - from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone()) + from_file().unwrap_or_else(|_| { + ThemeSet::load_from_reader(&mut Cursor::new(yazi_prebuild::ansi_theme())).unwrap() + }) }); - let syntaxes = - SYNTECT_SYNTAX.get_or_init(|| from_uncompressed_data(yazi_prebuild::syntaxes()).unwrap()); + + let syntaxes = SYNTECT_SYNTAX + .get_or_init(|| dumps::from_uncompressed_data(yazi_prebuild::syntaxes()).unwrap()); (theme, syntaxes) } @@ -52,7 +56,7 @@ impl Highlighter { syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) } - pub async fn highlight(&self, skip: usize, limit: usize) -> Result { + pub async fn highlight(&self, skip: usize, limit: usize) -> Result, PeekError> { let mut reader = BufReader::new(File::open(&self.path).await?).lines(); let syntax = Self::find_syntax(&self.path).await; @@ -69,8 +73,8 @@ impl Highlighter { } if !plain && line.len() > 6000 { - mem::take(&mut before); plain = true; + drop(mem::take(&mut before)); } if i > skip { @@ -87,7 +91,7 @@ impl Highlighter { } if plain { - Ok(after.join("")) + Ok(Text::from(after.join(""))) } else { Self::highlight_with(before, after, syntax.unwrap()).await } @@ -97,13 +101,12 @@ impl Highlighter { before: Vec, after: Vec, syntax: &'static SyntaxReference, - ) -> Result { + ) -> Result, PeekError> { let ticket = INCR.load(Ordering::Relaxed); tokio::task::spawn_blocking(move || { let (theme, syntaxes) = Self::init(); let mut h = HighlightLines::new(syntax, theme); - let mut result = String::new(); for line in before { if ticket != INCR.load(Ordering::Relaxed) { @@ -111,17 +114,18 @@ impl Highlighter { } h.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; } + + let mut lines = Vec::with_capacity(after.len()); for line in after { if ticket != INCR.load(Ordering::Relaxed) { return Err("Highlighting cancelled".into()); } let regions = h.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; - result.push_str(&as_24_bit_terminal_escaped(®ions, false)); + lines.push(Self::to_line_widget(regions)); } - result.push_str("\x1b[0m"); - Ok(result) + Ok(Text::from(lines)) }) .await? } @@ -129,3 +133,76 @@ impl Highlighter { #[inline] pub fn abort() { INCR.fetch_add(1, Ordering::Relaxed); } } + +impl Highlighter { + // Copy from https://github.com/sharkdp/bat/blob/master/src/terminal.rs + fn to_ansi_color(color: highlighting::Color) -> Option { + if color.a == 0 { + // Themes can specify one of the user-configurable terminal colors by + // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set + // to the 8-bit color palette number. The built-in themes ansi, base16, + // and base16-256 use this. + Some(match color.r { + // For the first 8 colors, use the Color enum to produce ANSI escape + // sequences using codes 30-37 (foreground) and 40-47 (background). + // For example, red foreground is \x1b[31m. This works on terminals + // without 256-color support. + 0x00 => ratatui::style::Color::Black, + 0x01 => ratatui::style::Color::Red, + 0x02 => ratatui::style::Color::Green, + 0x03 => ratatui::style::Color::Yellow, + 0x04 => ratatui::style::Color::Blue, + 0x05 => ratatui::style::Color::Magenta, + 0x06 => ratatui::style::Color::Cyan, + 0x07 => ratatui::style::Color::White, + // For all other colors, use Fixed to produce escape sequences using + // codes 38;5 (foreground) and 48;5 (background). For example, + // bright red foreground is \x1b[38;5;9m. This only works on + // terminals with 256-color support. + // + // TODO: When ansi_term adds support for bright variants using codes + // 90-97 (foreground) and 100-107 (background), we should use those + // for values 0x08 to 0x0f and only use Fixed for 0x10 to 0xff. + n => ratatui::style::Color::Indexed(n), + }) + } else if color.a == 1 { + // Themes can specify the terminal's default foreground/background color + // (i.e. no escape sequence) using the encoding #RRGGBBAA with AA set to + // 01. The built-in theme ansi uses this. + None + } else { + Some(ratatui::style::Color::Rgb(color.r, color.g, color.b)) + } + } + + fn to_line_widget(regions: Vec<(highlighting::Style, &str)>) -> Line<'static> { + let indent = " ".repeat(PREVIEW.tab_size as usize); + let spans: Vec<_> = regions + .into_iter() + .map(|(style, s)| { + let mut modifier = ratatui::style::Modifier::empty(); + if style.font_style.contains(highlighting::FontStyle::BOLD) { + modifier |= ratatui::style::Modifier::BOLD; + } + if style.font_style.contains(highlighting::FontStyle::ITALIC) { + modifier |= ratatui::style::Modifier::ITALIC; + } + if style.font_style.contains(highlighting::FontStyle::UNDERLINE) { + modifier |= ratatui::style::Modifier::UNDERLINED; + } + + Span { + content: s.replace('\t', &indent).into(), + style: ratatui::style::Style { + fg: Self::to_ansi_color(style.foreground), + // bg: Self::to_ansi_color(style.background), + add_modifier: modifier, + ..Default::default() + }, + } + }) + .collect(); + + Line::from(spans) + } +} diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index b3f42f69..372d275e 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -1,6 +1,4 @@ -use ansi_to_tui::IntoText; -use mlua::{AnyUserData, ExternalResult, IntoLuaMulti, Lua, Table, Value}; -use yazi_config::PREVIEW; +use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value}; use yazi_shared::{emit, event::Exec, Layer, PeekError}; use super::Utils; @@ -38,17 +36,13 @@ impl Utils { let area: RectRef = t.get("area")?; let mut lock = PreviewLock::try_from(t)?; - let s = match Highlighter::new(&lock.url).highlight(lock.skip, area.height as usize).await { - Ok(s) => s.replace('\t', &" ".repeat(PREVIEW.tab_size as usize)), - Err(PeekError::Exceed(max)) => return (false, max).into_lua_multi(lua), - Err(_) => return (false, Value::Nil).into_lua_multi(lua), - }; - - lock.data = vec![Box::new(Paragraph { - area: *area, - text: s.into_text().into_lua_err()?, - ..Default::default() - })]; + let text = + match Highlighter::new(&lock.url).highlight(lock.skip, area.height as usize).await { + Ok(text) => text, + Err(PeekError::Exceed(max)) => return (false, max).into_lua_multi(lua), + Err(_) => return (false, Value::Nil).into_lua_multi(lua), + }; + lock.data = vec![Box::new(Paragraph { area: *area, text, ..Default::default() })]; emit!(Call(Exec::call("preview", vec![]).with_data(lock).vec(), Layer::Manager)); (true, Value::Nil).into_lua_multi(lua)