aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <jonas@x77.dev>2026-05-23 14:48:51 +0200
committerJonas Maier <jonas@x77.dev>2026-05-23 14:48:51 +0200
commite4c0fc7beab2a6dd53210263a857f2b3ec29b604 (patch)
tree47137f101e5caaa93273cb03eeea92e0616537ca /src
parentba9414b17a107bdb30fe462aa2d42f6e91b3ad74 (diff)
downloadpish-e4c0fc7beab2a6dd53210263a857f2b3ec29b604.tar.gz
customizable syntax highlighting
Diffstat (limited to 'src')
-rw-r--r--src/ansi/colors.rs28
-rw-r--r--src/ansi/mod.rs2
-rw-r--r--src/lib.rs1
-rw-r--r--src/parse/mod.rs99
-rw-r--r--src/run/builtin.rs40
-rw-r--r--src/run/mod.rs10
-rw-r--r--src/syntax_highlighting.rs53
-rw-r--r--src/variants.rs3
8 files changed, 215 insertions, 21 deletions
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<u8> {
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<Item = HighlightKind> {
+ 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<HighlightKind> {
+ 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<BString> {
+ 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<BString> = 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<PreExpansion> {
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<Mutex<Session>>,
+ args: &[BString],
+ _stdin: &mut dyn Read,
+ stdout: &mut dyn Write,
+ ) -> Result {
+ if args.len() != 2 {
+ stdout.write_all(b"usage: pish_theme <kind> <color>\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<BString, &'static dyn BuiltinClone> {
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<HighlightKind, BString>,
+}
+
+#[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];
+}