From e4c0fc7beab2a6dd53210263a857f2b3ec29b604 Mon Sep 17 00:00:00 2001 From: Jonas Maier Date: Sat, 23 May 2026 14:48:51 +0200 Subject: customizable syntax highlighting --- pish_derive/src/lib.rs | 64 ++++++++++++++++++++++++++++++ src/ansi/colors.rs | 28 +++++++++++++ src/ansi/mod.rs | 2 + src/lib.rs | 1 + src/parse/mod.rs | 99 +++++++++++++++++++++++++++++++++++++++++++--- src/run/builtin.rs | 40 +++++++++++++++++++ src/run/mod.rs | 10 ++++- src/syntax_highlighting.rs | 53 ++++++++++++++++++------- src/variants.rs | 3 ++ 9 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 src/ansi/colors.rs create mode 100644 src/variants.rs diff --git a/pish_derive/src/lib.rs b/pish_derive/src/lib.rs index 3a296fc..3a2fddc 100644 --- a/pish_derive/src/lib.rs +++ b/pish_derive/src/lib.rs @@ -94,3 +94,67 @@ pub fn derive_cli(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } + +// AI-generated +#[proc_macro_derive(Variants)] +pub fn derive_variants(item: TokenStream) -> TokenStream { + let original = item.to_string(); + + // ---- extract enum name ---- + let mut tokens = original.split_whitespace(); + + let mut enum_name = None; + while let Some(t) = tokens.next() { + if t == "enum" { + enum_name = tokens.next().map(|s| s.trim_matches('{').to_string()); + break; + } + } + + let enum_name = match enum_name { + Some(n) => n, + None => return r#"compile_error!("expected enum")"#.parse().unwrap(), + }; + + // ---- extract body between braces ---- + let start = original.find('{').expect("missing '{'"); + let end = original.rfind('}').expect("missing '}'"); + let body = &original[start + 1..end]; + + // ---- parse variants ---- + let mut variants = Vec::new(); + + for part in body.split(',') { + let v = part.trim(); + if v.is_empty() { + continue; + } + + // take first token as variant name + let name = v.split_whitespace().next().unwrap_or("").trim(); + + if name.is_empty() { + continue; + } + + // reject non-unit variants + if v.contains('(') || v.contains('{') || v.contains('=') { + return r#"compile_error!("only unit variants supported")"#.parse().unwrap(); + } + + variants.push(format!("{}::{}", enum_name, name)); + } + + let variants_joined = variants.join(", "); + + let expanded = format!( + " +impl crate::variants::Variants for {enum_name} {{ + const VARIANTS: &'static [Self] = &[ + {variants_joined} + ]; +}}" + ); + + expanded.parse().unwrap() +} diff --git a/src/ansi/colors.rs b/src/ansi/colors.rs new file mode 100644 index 0000000..d14dc81 --- /dev/null +++ b/src/ansi/colors.rs @@ -0,0 +1,28 @@ +macro_rules! color { + ($($name: ident = $num:expr ;)*) => { + $( + #[allow(unused)] + pub const $name: &str = concat!("\x1b[", stringify!($num), "m"); + )* + }; +} + +color! { + RESET = 0; + BLACK_FG = 30; + BLACK_BG = 40; + RED_FG = 31; + RED_BG = 41; + GREEN_FG = 32; + GREEN_BG = 42; + YELLOW_FG = 33; + YELLOW_BG = 43; + BLUE_FG = 34; + BLUE_BG = 44; + MAGENTA_FG = 35; + MAGENTA_BG = 45; + CYAN_FG = 36; + CYAN_BG = 46; + WHITE_FG = 37; + WHITE_BG = 47; +} diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs index 7a38ae4..d7e2ab3 100644 --- a/src/ansi/mod.rs +++ b/src/ansi/mod.rs @@ -1,5 +1,7 @@ use std::{collections::BTreeMap, io::Read, sync::RwLock}; +pub mod colors; + fn read1() -> Option { let mut buf = [0]; match std::io::stdin().lock().read_exact(&mut buf) { diff --git a/src/lib.rs b/src/lib.rs index 3f3daf2..4bdbebe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub mod rw; pub mod serialization; pub mod syntax_highlighting; pub mod wait; +pub mod variants; use raw::*; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 10ec666..8970fc3 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,4 +1,6 @@ -use crate::{BString, PushAll, bstr}; +use pish_derive::Variants; + +use crate::{BString, PushAll, bstr, variants::Variants}; #[cfg(test)] mod test; @@ -1042,7 +1044,7 @@ impl Parse for ExpString { let end = b.loc_u32(); b.highlights.push(Highlight { span: begin.to(end), - kind: HighlightKind::String, + kind: HighlightKind::Other(OtherHighlights::String), }); } } @@ -1296,11 +1298,94 @@ pub enum ParseMode { Completion, } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum HighlightKind { None, - String, Keyword(Keyword), + Other(OtherHighlights), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Variants)] +pub enum OtherHighlights { + String, +} + +impl OtherHighlights { + pub fn identifier(&self) -> &bstr { + match self { + OtherHighlights::String => b"string", + } + } +} + +impl HighlightKind { + /// all highlight kind variants *except* None + pub fn variants() -> impl Iterator { + let a = Keyword::VARIANTS + .into_iter() + .cloned() + .map(HighlightKind::Keyword); + let b = OtherHighlights::VARIANTS + .into_iter() + .cloned() + .map(HighlightKind::Other); + a.chain(b) + } + + /// an unique identifier such that we can refer to that in the builtin `pish_theme` + pub fn identifier(&self) -> &bstr { + match self { + HighlightKind::None => b"default", + HighlightKind::Keyword(keyword) => keyword.identifier(), + HighlightKind::Other(other) => other.identifier(), + } + } + + pub fn from_identifier(ident: &bstr) -> Vec { + match ident { + b"keywords" => { + return Keyword::VARIANTS + .into_iter() + .cloned() + .map(HighlightKind::Keyword) + .collect(); + } + b"braces" => { + return vec![ + HighlightKind::Keyword(Keyword::OpenBrace), + HighlightKind::Keyword(Keyword::CloseBrace), + ]; + } + b"all" | b"everything" => return Self::variants().collect(), + _ => (), + } + + Self::variants() + .into_iter() + .filter(|x| x.identifier() == ident) + .collect() + } + + pub fn all_identifiers() -> Vec { + let kw = Keyword::VARIANTS.into_iter().map(Keyword::identifier); + let ot = OtherHighlights::VARIANTS + .into_iter() + .map(OtherHighlights::identifier); + let groups = [&b"keywords"[..], b"braces", b"all", b"everything"]; + kw.chain(ot) + .chain(groups) + .map(|ident| ident.to_vec()) + .collect() + } +} + +#[test] +fn no_two_highlight_kinds_share_an_identifier() { + use std::collections::HashSet; + let unique_identifiers: HashSet = HighlightKind::variants() + .map(|x| x.identifier().to_vec()) + .collect(); + assert_eq!(unique_identifiers.len(), HighlightKind::variants().count()); } pub struct Highlight { @@ -1504,7 +1589,7 @@ impl<'a> Cursor<'a> { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Variants)] pub enum Keyword { If, While, @@ -1536,6 +1621,10 @@ impl Keyword { Keyword::CloseBrace => false, } } + + pub fn identifier(&self) -> &bstr { + self.as_bytes() + } } impl If { diff --git a/src/run/builtin.rs b/src/run/builtin.rs index c946472..2255997 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -967,6 +967,46 @@ impl Builtin for export { } } +#[derive(Copy, Clone)] +pub struct pish_theme; + +impl Builtin for pish_theme { + fn name(&self) -> &str { + "pish_theme" + } + + fn io( + &self, + session: Arc>, + args: &[BString], + _stdin: &mut dyn Read, + stdout: &mut dyn Write, + ) -> Result { + if args.len() != 2 { + stdout.write_all(b"usage: pish_theme \nwhere color is an ansi escape code,\nand where kind is one of the following: ")?; + for ident in crate::parse::HighlightKind::all_identifiers() { + stdout.write_all(&ident)?; + stdout.write_all(b" ")?; + } + stdout.write_all(b"\n")?; + return Err(Error::Exit(-1)); + } + + let mut se = session.lock().unwrap(); + match se.highlighter.set_color(&args[0], &args[1]) { + Ok(_) => Ok(()), + Err(e) => match e { + syntax_highlighting::SetColorError::NoSuchKeyword => { + stdout.write_all(b"no such kind: ")?; + stdout.write_all(&args[0])?; + stdout.write_all(b"\n")?; + Err(Error::Exit(-1)) + } + }, + } + } +} + #[cfg(debug_assertions)] mod dbg { use super::*; diff --git a/src/run/mod.rs b/src/run/mod.rs index f34b678..842a918 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -451,7 +451,14 @@ impl Executor { stdin: InputReader, stdout: OutputWriter, ) -> SpawnedCmd { - self.execute_block(parse::Block { commands: s.stmts, finished_parsing: true }, stdin, stdout) + self.execute_block( + parse::Block { + commands: s.stmts, + finished_parsing: true, + }, + stdin, + stdout, + ) } } @@ -711,6 +718,7 @@ const BUILTINS: &[&'static dyn BuiltinClone] = &[ &builtin::Here, &builtin::logo, &builtin::export, + &builtin::pish_theme, ]; pub fn builtin_map() -> HashMap { diff --git a/src/syntax_highlighting.rs b/src/syntax_highlighting.rs index 0b1e8c5..b76aa4b 100644 --- a/src/syntax_highlighting.rs +++ b/src/syntax_highlighting.rs @@ -1,28 +1,51 @@ -use crate::parse::{Highlight, HighlightKind, Keyword}; +use std::collections::HashMap; + +use crate::{ + BString, ansi, bstr, + parse::{Highlight, HighlightKind}, +}; pub struct Highlighter { pub enabled: bool, + colors: HashMap, +} + +#[derive(Debug)] +pub enum SetColorError { + NoSuchKeyword, } impl Highlighter { pub fn new() -> Self { - Self { enabled: true } + let mut this = Self { + enabled: true, + colors: HashMap::new(), + }; + let mut sc = |a: &str, b: &str| this.set_color(a.as_bytes(), b.as_bytes()).unwrap(); + use ansi::colors::*; + sc("keywords", GREEN_FG); + sc("braces", CYAN_FG); + sc("string", MAGENTA_FG); + this } - pub fn color(&self, h: HighlightKind) -> &[u8] { - // TODO: configurable - const GREEN: &[u8] = b"\x1b[32m"; - const BLUE: &[u8] = b"\x1b[36m"; - const MAGENTA: &[u8] = b"\x1b[95m"; - const COLOR_RESET: &[u8] = b"\x1b[0m"; - match h { - HighlightKind::Keyword( - Keyword::If | Keyword::Elif | Keyword::Else | Keyword::While, - ) => GREEN, - HighlightKind::Keyword(Keyword::OpenBrace | Keyword::CloseBrace) => BLUE, - HighlightKind::String => MAGENTA, - HighlightKind::None => COLOR_RESET, + pub fn set_color(&mut self, ident: &bstr, color: &bstr) -> Result<(), SetColorError> { + let kinds = HighlightKind::from_identifier(ident); + for kind in kinds.iter() { + self.colors.insert(*kind, color.to_vec()); } + if kinds.is_empty() { + Err(SetColorError::NoSuchKeyword) + } else { + Ok(()) + } + } + + pub fn color(&self, h: HighlightKind) -> &[u8] { + self.colors + .get(&h) + .map(|c| c.as_ref()) + .unwrap_or(ansi::colors::RESET.as_bytes()) } pub fn pretty_print( diff --git a/src/variants.rs b/src/variants.rs new file mode 100644 index 0000000..f2bb0a1 --- /dev/null +++ b/src/variants.rs @@ -0,0 +1,3 @@ +pub trait Variants: 'static + Sized { + const VARIANTS: &[Self]; +} -- cgit v1.2.3