diff --git a/crates/jp_cli/src/cmd/conversation/print.rs b/crates/jp_cli/src/cmd/conversation/print.rs index 8e06b5fb..fcdef01e 100644 --- a/crates/jp_cli/src/cmd/conversation/print.rs +++ b/crates/jp_cli/src/cmd/conversation/print.rs @@ -207,6 +207,12 @@ fn build_formatter(cfg: &AppConfig, pretty: bool) -> Formatter { None }) .pretty_hr(pretty && cfg.style.markdown.hr_style.is_line()) + .inline_code_bg( + cfg.style + .inline_code + .background + .map(crate::format::color_to_bg_param), + ) } #[cfg(test)] diff --git a/crates/jp_cli/src/cmd/query.rs b/crates/jp_cli/src/cmd/query.rs index acd7deba..871aab72 100644 --- a/crates/jp_cli/src/cmd/query.rs +++ b/crates/jp_cli/src/cmd/query.rs @@ -345,7 +345,13 @@ impl Query { } else { None }) - .pretty_hr(pretty && cfg.style.markdown.hr_style.is_line()); + .pretty_hr(pretty && cfg.style.markdown.hr_style.is_line()) + .inline_code_bg( + cfg.style + .inline_code + .background + .map(crate::format::color_to_bg_param), + ); let formatted = formatter.format_terminal(&format!("{}\n\n---\n\n", chat_request.content))?; diff --git a/crates/jp_cli/src/cmd/query/stream/renderer.rs b/crates/jp_cli/src/cmd/query/stream/renderer.rs index c707ea45..6065c45f 100644 --- a/crates/jp_cli/src/cmd/query/stream/renderer.rs +++ b/crates/jp_cli/src/cmd/query/stream/renderer.rs @@ -30,7 +30,6 @@ use std::sync::Arc; use jp_config::style::{ StyleConfig, - markdown::MarkdownConfig, reasoning::{ReasoningDisplayConfig, TruncateChars}, }; use jp_conversation::event::ChatResponse; @@ -68,7 +67,7 @@ pub struct ChatResponseRenderer { impl ChatResponseRenderer { pub fn new(printer: Arc, config: StyleConfig) -> Self { let pretty = printer.pretty_printing(); - let formatter = formatter_from_config(&config.markdown, pretty); + let formatter = formatter_from_config(&config, pretty); Self { buffer: Buffer::new(), formatter, @@ -241,16 +240,16 @@ impl ChatResponseRenderer { // so the background is continuous across paragraphs. match opts.default_background { Some(DefaultBackground { - color, + ref param, fill: BackgroundFill::Terminal, }) => { - formatted.push_str(&format!("\x1b[48;5;{color}m\x1b[K\x1b[49m\n")); + formatted.push_str(&format!("\x1b[{param}m\x1b[K\x1b[49m\n")); } Some(DefaultBackground { - color, + ref param, fill: BackgroundFill::Column(width), }) => { - formatted.push_str(&format!("\x1b[48;5;{color}m")); + formatted.push_str(&format!("\x1b[{param}m")); for _ in 0..width { formatted.push(' '); } @@ -271,7 +270,7 @@ impl ChatResponseRenderer { .reasoning .background .map(|color| DefaultBackground { - color, + param: crate::format::color_to_bg_param(color), fill: BackgroundFill::Terminal, }) } else { @@ -310,22 +309,28 @@ impl ChatResponseRenderer { pub fn reset(&mut self) { self.buffer = Buffer::new(); let pretty = self.printer.pretty_printing(); - self.formatter = formatter_from_config(&self.config.markdown, pretty); + self.formatter = formatter_from_config(&self.config, pretty); self.last_content_kind = None; self.reasoning_chars_count = 0; self.code_highlight = None; } } -fn formatter_from_config(config: &MarkdownConfig, pretty: bool) -> Formatter { - Formatter::with_width(config.wrap_width) - .table_max_column_width(config.table_max_column_width) +fn formatter_from_config(config: &StyleConfig, pretty: bool) -> Formatter { + Formatter::with_width(config.markdown.wrap_width) + .table_max_column_width(config.markdown.table_max_column_width) .theme(if pretty { - config.theme.as_deref() + config.markdown.theme.as_deref() } else { None }) - .pretty_hr(pretty && config.hr_style.is_line()) + .pretty_hr(pretty && config.markdown.hr_style.is_line()) + .inline_code_bg( + config + .inline_code + .background + .map(crate::format::color_to_bg_param), + ) } #[cfg(test)] diff --git a/crates/jp_cli/src/cmd/query/stream/renderer_tests.rs b/crates/jp_cli/src/cmd/query/stream/renderer_tests.rs index 4a702c4f..c4d4e854 100644 --- a/crates/jp_cli/src/cmd/query/stream/renderer_tests.rs +++ b/crates/jp_cli/src/cmd/query/stream/renderer_tests.rs @@ -1,4 +1,4 @@ -use jp_config::AppConfig; +use jp_config::{AppConfig, types::color::Color}; use jp_printer::{OutputFormat, SharedBuffer}; use super::*; @@ -216,7 +216,7 @@ fn test_whitespace_only_block_not_printed() { fn test_reasoning_background_color_applied() { let mut config = AppConfig::new_test(); config.style.reasoning.display = ReasoningDisplayConfig::Full; - config.style.reasoning.background = Some(236); + config.style.reasoning.background = Some(Color::Ansi256(236)); let (mut renderer, out) = create_renderer_with_config(config); renderer.render(&ChatResponse::Reasoning { @@ -243,7 +243,7 @@ fn test_reasoning_background_color_applied() { fn test_reasoning_background_not_applied_to_messages() { let mut config = AppConfig::new_test(); config.style.reasoning.display = ReasoningDisplayConfig::Full; - config.style.reasoning.background = Some(236); + config.style.reasoning.background = Some(Color::Ansi256(236)); let (mut renderer, out) = create_renderer_with_config(config); renderer.render(&ChatResponse::Message { diff --git a/crates/jp_cli/src/format.rs b/crates/jp_cli/src/format.rs index 8376df43..85654e0b 100644 --- a/crates/jp_cli/src/format.rs +++ b/crates/jp_cli/src/format.rs @@ -1,2 +1,12 @@ pub(crate) mod conversation; pub(crate) mod datetime; + +use jp_config::types::color::Color; + +/// Convert a [`Color`] to an SGR background parameter string. +pub(crate) fn color_to_bg_param(color: Color) -> String { + match color { + Color::Ansi256(n) => format!("48;5;{n}"), + Color::Rgb { r, g, b } => format!("48;2;{r};{g};{b}"), + } +} diff --git a/crates/jp_config/src/assignment.rs b/crates/jp_config/src/assignment.rs index 84970ab3..11f9a609 100644 --- a/crates/jp_config/src/assignment.rs +++ b/crates/jp_config/src/assignment.rs @@ -415,6 +415,42 @@ impl KvAssignment { self.try_from_str().map(Some) } + /// Like [`Self::try_from_str`], but also accepts JSON numbers by + /// converting them to their string representation first. + /// + /// Useful for types like [`Color`](crate::types::color::Color) that + /// accept both `236` (integer) and `"#504945"` (string). + pub(crate) fn try_number_or_from_str(self) -> Result + where + T: FromStr, + E: Into, + { + let Self { key, value, .. } = self; + + match value { + KvValue::Json(Value::Number(n)) => { + let s = n.to_string(); + T::from_str(&s) + .map_err(Into::into) + .or_else(|err| assignment_error(&key, Value::String(s), err)) + } + KvValue::Json(Value::String(s)) | KvValue::String(s) => T::from_str(&s) + .map_err(Into::into) + .or_else(|err| assignment_error(&key, Value::String(s), err)), + KvValue::Json(_) => type_error(&key, &value, &["number", "string"]), + } + } + + /// Convenience method for [`Self::try_number_or_from_str`] that wraps the + /// `Ok` value into `Some`. + pub(crate) fn try_some_number_or_from_str(self) -> Result, KvAssignmentError> + where + T: FromStr, + E: Into, + { + self.try_number_or_from_str().map(Some) + } + /// Try to parse the value as a string. pub(crate) fn try_string(self) -> Result { let Self { key, value, .. } = self; @@ -498,26 +534,6 @@ impl KvAssignment { self.try_u32().map(Some) } - /// Try to parse the value as an unsigned 8-bit integer. - pub(crate) fn try_u8(self) -> Result { - let Self { key, value, .. } = self; - - match value { - #[expect(clippy::cast_possible_truncation)] - KvValue::Json(Value::Number(v)) if v.is_u64() => Ok(v.as_u64().expect("is u64") as u8), - KvValue::Json(_) => type_error(&key, &value, &["number", "string"]), - KvValue::String(v) => Ok(v - .parse() - .map_err(|err| KvAssignmentError::new(key.full_path.clone(), err))?), - } - } - - /// Convenience method for [`Self::try_u8`] that wraps the `Ok` value into - /// `Some`. - pub(crate) fn try_some_u8(self) -> Result, KvAssignmentError> { - self.try_u8().map(Some) - } - /// Try to parse the value as a 32-bit floating point number. pub(crate) fn try_f32(self) -> Result { let Self { key, value, .. } = self; diff --git a/crates/jp_config/src/snapshots/jp_config__tests__app_config_fields.snap b/crates/jp_config/src/snapshots/jp_config__tests__app_config_fields.snap index 4e155d9f..4864981f 100644 --- a/crates/jp_config/src/snapshots/jp_config__tests__app_config_fields.snap +++ b/crates/jp_config/src/snapshots/jp_config__tests__app_config_fields.snap @@ -26,11 +26,11 @@ expression: "AppConfig::fields()" "style.markdown.table_max_column_width", "style.markdown.theme", "style.markdown.wrap_width", + "style.inline_code.background", "style.code.color", "style.code.copy_link", "style.code.file_link", "style.code.line_numbers", - "style.code.theme", "providers.mcp", "providers.llm.aliases", "providers.llm.openrouter.api_key_env", diff --git a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default.snap b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default.snap index 070b110b..166c8f38 100644 --- a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default.snap +++ b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default.snap @@ -64,12 +64,14 @@ PartialAppConfig { }, style: PartialStyleConfig { code: PartialCodeConfig { - theme: None, color: None, line_numbers: None, file_link: None, copy_link: None, }, + inline_code: PartialInlineCodeConfig { + background: None, + }, markdown: PartialMarkdownConfig { wrap_width: None, table_max_column_width: None, diff --git a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default_values.snap b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default_values.snap index 6fb4b96f..490d32d8 100644 --- a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default_values.snap +++ b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_default_values.snap @@ -117,9 +117,6 @@ Ok( }, style: PartialStyleConfig { code: PartialCodeConfig { - theme: Some( - "base16-mocha.dark", - ), color: Some( true, ), @@ -133,6 +130,9 @@ Ok( Off, ), }, + inline_code: PartialInlineCodeConfig { + background: None, + }, markdown: PartialMarkdownConfig { wrap_width: Some( 80, @@ -149,7 +149,9 @@ Ok( display: None, summary_model: None, background: Some( - 236, + Ansi256( + 236, + ), ), }, streaming: PartialStreamingConfig { diff --git a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_empty_serialize.snap b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_empty_serialize.snap index b9ffb57a..54483560 100644 --- a/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_empty_serialize.snap +++ b/crates/jp_config/src/snapshots/jp_config__tests__partial_app_config_empty_serialize.snap @@ -64,12 +64,14 @@ PartialAppConfig { }, style: PartialStyleConfig { code: PartialCodeConfig { - theme: None, color: None, line_numbers: None, file_link: None, copy_link: None, }, + inline_code: PartialInlineCodeConfig { + background: None, + }, markdown: PartialMarkdownConfig { wrap_width: None, table_max_column_width: None, diff --git a/crates/jp_config/src/style.rs b/crates/jp_config/src/style.rs index e4c00922..67714daa 100644 --- a/crates/jp_config/src/style.rs +++ b/crates/jp_config/src/style.rs @@ -1,6 +1,7 @@ //! Style configuration for output formatting. pub mod code; +pub mod inline_code; pub mod markdown; pub mod reasoning; pub mod streaming; @@ -18,6 +19,7 @@ use crate::{ partial::ToPartial, style::{ code::{CodeConfig, PartialCodeConfig}, + inline_code::{InlineCodeConfig, PartialInlineCodeConfig}, markdown::{MarkdownConfig, PartialMarkdownConfig}, reasoning::{PartialReasoningConfig, ReasoningConfig}, streaming::{PartialStreamingConfig, StreamingConfig}, @@ -36,6 +38,12 @@ pub struct StyleConfig { #[setting(nested)] pub code: CodeConfig, + /// Inline code span style. + /// + /// Configures how inline code (`` `like this` ``) is rendered. + #[setting(nested)] + pub inline_code: InlineCodeConfig, + /// Markdown rendering style. /// /// Configures how markdown content is rendered in the terminal. @@ -73,6 +81,7 @@ impl AssignKeyValue for PartialStyleConfig { match kv.key_string().as_str() { "" => *self = kv.try_object()?, _ if kv.p("code") => self.code.assign(kv)?, + _ if kv.p("inline_code") => self.inline_code.assign(kv)?, _ if kv.p("markdown") => self.markdown.assign(kv)?, _ if kv.p("reasoning") => self.reasoning.assign(kv)?, _ if kv.p("streaming") => self.streaming.assign(kv)?, @@ -89,6 +98,7 @@ impl PartialConfigDelta for PartialStyleConfig { fn delta(&self, next: Self) -> Self { Self { code: self.code.delta(next.code), + inline_code: self.inline_code.delta(next.inline_code), markdown: self.markdown.delta(next.markdown), reasoning: self.reasoning.delta(next.reasoning), streaming: self.streaming.delta(next.streaming), @@ -102,6 +112,7 @@ impl ToPartial for StyleConfig { fn to_partial(&self) -> Self::Partial { Self::Partial { code: self.code.to_partial(), + inline_code: self.inline_code.to_partial(), markdown: self.markdown.to_partial(), reasoning: self.reasoning.to_partial(), streaming: self.streaming.to_partial(), diff --git a/crates/jp_config/src/style/code.rs b/crates/jp_config/src/style/code.rs index 961a4cc8..58187208 100644 --- a/crates/jp_config/src/style/code.rs +++ b/crates/jp_config/src/style/code.rs @@ -13,12 +13,6 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Config)] #[config(rename_all = "snake_case")] pub struct CodeConfig { - /// Theme to use for code blocks. - /// - /// This uses [syntect](https://github.com/trishume/syntect) theme names. - #[setting(default = "base16-mocha.dark")] - pub theme: String, - /// Whether to colorize code blocks. #[setting(default = true)] pub color: bool, @@ -65,7 +59,6 @@ impl AssignKeyValue for PartialCodeConfig { fn assign(&mut self, kv: KvAssignment) -> AssignResult { match kv.key_string().as_str() { "" => *self = kv.try_object()?, - "theme" => self.theme = kv.try_some_string()?, "color" => self.color = kv.try_some_bool()?, "line_numbers" => self.line_numbers = kv.try_some_bool()?, "file_link" => self.file_link = kv.try_some_from_str()?, @@ -80,7 +73,6 @@ impl AssignKeyValue for PartialCodeConfig { impl PartialConfigDelta for PartialCodeConfig { fn delta(&self, next: Self) -> Self { Self { - theme: delta_opt(self.theme.as_ref(), next.theme), color: delta_opt(self.color.as_ref(), next.color), line_numbers: delta_opt(self.line_numbers.as_ref(), next.line_numbers), file_link: delta_opt(self.file_link.as_ref(), next.file_link), @@ -94,7 +86,6 @@ impl ToPartial for CodeConfig { let defaults = Self::Partial::default(); Self::Partial { - theme: partial_opt(&self.theme, defaults.theme), color: partial_opt(&self.color, defaults.color), line_numbers: partial_opt(&self.line_numbers, defaults.line_numbers), file_link: partial_opt(&self.file_link, defaults.file_link), diff --git a/crates/jp_config/src/style/inline_code.rs b/crates/jp_config/src/style/inline_code.rs new file mode 100644 index 00000000..8c7ca0a2 --- /dev/null +++ b/crates/jp_config/src/style/inline_code.rs @@ -0,0 +1,57 @@ +//! Inline code span styling configuration. + +use schematic::Config; + +use crate::{ + assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key}, + delta::{PartialConfigDelta, delta_opt}, + partial::ToPartial, + types::color::Color, +}; + +/// Inline code span style configuration. +/// +/// Controls the visual appearance of inline code (`` `like this` ``) in +/// terminal output, independently from fenced code block styling. +#[derive(Debug, Clone, PartialEq, Config)] +#[config(rename_all = "snake_case")] +pub struct InlineCodeConfig { + /// Background color for inline code spans. + /// + /// Overrides the background derived from the syntax highlighting theme. + /// Accepts either an ANSI 256-color index (e.g. `236`) or a hex RGB + /// string (e.g. `"#504945"`). + /// + /// When unset, the theme's background color is used (the default). + pub background: Option, +} + +impl AssignKeyValue for PartialInlineCodeConfig { + fn assign(&mut self, kv: KvAssignment) -> AssignResult { + match kv.key_string().as_str() { + "" => *self = kv.try_object()?, + "background" => self.background = kv.try_some_number_or_from_str()?, + _ => return missing_key(&kv), + } + + Ok(()) + } +} + +impl PartialConfigDelta for PartialInlineCodeConfig { + fn delta(&self, next: Self) -> Self { + Self { + background: delta_opt(self.background.as_ref(), next.background), + } + } +} + +impl ToPartial for InlineCodeConfig { + fn to_partial(&self) -> Self::Partial { + let defaults = Self::Partial::default(); + + Self::Partial { + background: delta_opt(defaults.background.as_ref(), self.background), + } + } +} diff --git a/crates/jp_config/src/style/reasoning.rs b/crates/jp_config/src/style/reasoning.rs index 73d09b51..76aaa05a 100644 --- a/crates/jp_config/src/style/reasoning.rs +++ b/crates/jp_config/src/style/reasoning.rs @@ -10,6 +10,7 @@ use crate::{ delta::{PartialConfigDelta, delta_opt, delta_opt_partial}, model::{ModelConfig, PartialModelConfig}, partial::{ToPartial, partial_opt, partial_opt_config, partial_opts}, + types::color::Color, }; /// Reasoning content style configuration. @@ -35,21 +36,22 @@ pub struct ReasoningConfig { #[setting(nested)] pub summary_model: Option, - /// Background color for reasoning content (ANSI 256-color index). + /// Background color for reasoning content. /// /// When set, reasoning blocks are rendered with this background /// color spanning the full terminal width, visually distinguishing /// them from regular message content. /// - /// Example: `236` for a subtle dark gray on dark terminals. + /// Accepts either an ANSI 256-color index (e.g. `236`) or a hex RGB + /// string (e.g. `"#1d2021"`). #[setting(default = default_reasoning_background)] - pub background: Option, + pub background: Option, } -/// The default system prompt for the assistant. +/// The default reasoning background color. #[expect(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)] -const fn default_reasoning_background(_: &()) -> TransformResult> { - Ok(Some(236)) +const fn default_reasoning_background(_: &()) -> TransformResult> { + Ok(Some(Color::Ansi256(236))) } impl AssignKeyValue for PartialReasoningConfig { @@ -57,7 +59,7 @@ impl AssignKeyValue for PartialReasoningConfig { match kv.key_string().as_str() { "" => *self = kv.try_object()?, "display" => self.display = kv.try_some_from_str()?, - "background" => self.background = kv.try_some_u8()?, + "background" => self.background = kv.try_some_number_or_from_str()?, _ if kv.p("summary_model") => self.summary_model.assign(kv)?, _ => return missing_key(&kv), } diff --git a/crates/jp_config/src/types.rs b/crates/jp_config/src/types.rs index 0b026dea..fe32a307 100644 --- a/crates/jp_config/src/types.rs +++ b/crates/jp_config/src/types.rs @@ -1,5 +1,6 @@ //! Extended configuration types. +pub mod color; pub mod extending_path; pub mod string; pub mod vec; diff --git a/crates/jp_config/src/types/color.rs b/crates/jp_config/src/types/color.rs new file mode 100644 index 00000000..200da104 --- /dev/null +++ b/crates/jp_config/src/types/color.rs @@ -0,0 +1,135 @@ +//! Terminal color type for configuration values. + +use std::{fmt, str::FromStr}; + +use schematic::{Schema, SchemaBuilder, Schematic}; +use serde::{Deserialize, Serialize}; + +/// A terminal color, either an ANSI 256-color palette index or 24-bit RGB. +/// +/// Accepts both integer and string representations: +/// +/// - `236` or `"236"` → ANSI 256-color index +/// - `"#504945"` → 24-bit RGB +/// +/// # Examples +/// +/// ```toml +/// # ANSI 256-color +/// background = 236 +/// +/// # Hex RGB +/// background = "#504945" +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Color { + /// ANSI 256-color palette index (0–255). + Ansi256(u8), + /// 24-bit RGB color. + Rgb { + /// Red channel. + r: u8, + /// Green channel. + g: u8, + /// Blue channel. + b: u8, + }, +} + +impl Schematic for Color { + fn schema_name() -> Option { + Some("Color".into()) + } + + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.union(schematic::schema::UnionType { + variants_types: vec![ + Box::new(schema.infer::()), + Box::new(schema.infer::()), + ], + ..Default::default() + }) + } +} + +/// Error when parsing a [`Color`] from a string. +#[derive(Debug, thiserror::Error)] +#[error("invalid color: {0:?} (expected a number 0-255 or #RRGGBB)")] +pub struct ColorParseError(String); + +impl FromStr for Color { + type Err = ColorParseError; + + fn from_str(s: &str) -> Result { + s.strip_prefix('#').map_or_else( + || { + s.parse::() + .map(Self::Ansi256) + .map_err(|_| ColorParseError(s.to_owned())) + }, + |hex| parse_hex_rgb(hex).ok_or_else(|| ColorParseError(s.to_owned())), + ) + } +} + +/// Parse a 6-digit hex string (without `#`) into an RGB color. +fn parse_hex_rgb(hex: &str) -> Option { + if hex.len() != 6 { + return None; + } + let r = u8::from_str_radix(&hex[0..2], 16).ok()?; + let g = u8::from_str_radix(&hex[2..4], 16).ok()?; + let b = u8::from_str_radix(&hex[4..6], 16).ok()?; + Some(Color::Rgb { r, g, b }) +} + +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Ansi256(n) => serializer.serialize_u8(*n), + Self::Rgb { r, g, b } => serializer.serialize_str(&format!("#{r:02x}{g:02x}{b:02x}")), + } + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ColorVisitor; + + impl serde::de::Visitor<'_> for ColorVisitor { + type Value = Color; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an integer 0-255 or a hex color string like \"#504945\"") + } + + fn visit_u64(self, v: u64) -> Result { + u8::try_from(v) + .map(Color::Ansi256) + .map_err(|_| E::custom(format!("color index {v} out of range 0-255"))) + } + + fn visit_i64(self, v: i64) -> Result { + u8::try_from(v) + .map(Color::Ansi256) + .map_err(|_| E::custom(format!("color index {v} out of range 0-255"))) + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } + } + + deserializer.deserialize_any(ColorVisitor) + } +} + +#[cfg(test)] +#[path = "color_tests.rs"] +mod tests; diff --git a/crates/jp_config/src/types/color_tests.rs b/crates/jp_config/src/types/color_tests.rs new file mode 100644 index 00000000..d964e420 --- /dev/null +++ b/crates/jp_config/src/types/color_tests.rs @@ -0,0 +1,63 @@ +use serde_json::{from_str, to_string}; + +use super::*; + +#[test] +fn parse_ansi256_from_str() { + assert_eq!("236".parse::().unwrap(), Color::Ansi256(236)); + assert_eq!("0".parse::().unwrap(), Color::Ansi256(0)); + assert_eq!("255".parse::().unwrap(), Color::Ansi256(255)); +} + +#[test] +fn parse_hex_rgb_from_str() { + assert_eq!("#504945".parse::().unwrap(), Color::Rgb { + r: 80, + g: 73, + b: 69 + }); + assert_eq!("#FFFFFF".parse::().unwrap(), Color::Rgb { + r: 255, + g: 255, + b: 255 + }); + assert_eq!("#000000".parse::().unwrap(), Color::Rgb { + r: 0, + g: 0, + b: 0 + }); +} + +#[test] +fn parse_invalid() { + assert!("256".parse::().is_err()); + assert!("-1".parse::().is_err()); + assert!("#50494".parse::().is_err()); + assert!("#GGGGGG".parse::().is_err()); + assert!("hello".parse::().is_err()); +} + +#[test] +fn serde_roundtrip_ansi256() { + let c = Color::Ansi256(236); + let json = to_string(&c).unwrap(); + assert_eq!(json, "236"); + assert_eq!(from_str::(&json).unwrap(), c); +} + +#[test] +fn serde_roundtrip_rgb() { + let c = Color::Rgb { + r: 80, + g: 73, + b: 69, + }; + let json = to_string(&c).unwrap(); + assert_eq!(json, "\"#504945\""); + assert_eq!(from_str::(&json).unwrap(), c); +} + +#[test] +fn deserialize_string_number() { + assert_eq!(from_str::("\"236\"").unwrap(), Color::Ansi256(236)); +} diff --git a/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip-2.snap b/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip-2.snap index 0123c072..85a6be4e 100644 --- a/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip-2.snap +++ b/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip-2.snap @@ -53,7 +53,6 @@ expression: "&stream" }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip.snap b/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip.snap index 8b40533c..3af7e826 100644 --- a/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip.snap +++ b/crates/jp_conversation/src/snapshots/jp_conversation__stream__tests__conversation_stream_serialization_roundtrip.snap @@ -53,7 +53,6 @@ expression: "&stream" }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_chat_completion_stream__conversation_stream.snap index 8107bc50..4f23392d 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_image_attachment__conversation_stream.snap index 1cd44444..a25ff4b9 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_multi_turn_conversation__conversation_stream.snap index e64765b6..7987d0a1 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_adaptive_thinking__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_adaptive_thinking__conversation_stream.snap index 209d80a1..21fa843b 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_adaptive_thinking__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_adaptive_thinking__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_max_effort__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_max_effort__conversation_stream.snap index eaee3d68..0a2f8a49 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_max_effort__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_opus_4_6_max_effort__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_redacted_thinking__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_redacted_thinking__conversation_stream.snap index 2ee1edc4..a6194244 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_redacted_thinking__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_redacted_thinking__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_request_chaining__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_request_chaining__conversation_stream.snap index 208c1628..f1ea83ba 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_request_chaining__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_request_chaining__conversation_stream.snap @@ -59,7 +59,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_structured_output__conversation_stream.snap index 448a0315..9662f8bd 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_auto__conversation_stream.snap index af63c59a..b50d8757 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_function__conversation_stream.snap index c12d524d..4ff8c8dd 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_reasoning__conversation_stream.snap index d8c60945..1180dddd 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_no_reasoning__conversation_stream.snap index 70e42fe1..ef83c6b4 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_reasoning__conversation_stream.snap index 75eb3305..fe95f2f5 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_required_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_stream__conversation_stream.snap index b62da0e3..222a69c0 100644 --- a/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/anthropic/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_chat_completion_stream__conversation_stream.snap index 6d92cbcb..9bf14d41 100644 --- a/crates/jp_llm/tests/fixtures/google/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_gemini_3_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_gemini_3_reasoning__conversation_stream.snap index 949e02f1..888e94b0 100644 --- a/crates/jp_llm/tests/fixtures/google/test_gemini_3_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_gemini_3_reasoning__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_image_attachment__conversation_stream.snap index 1934297d..45c2f01c 100644 --- a/crates/jp_llm/tests/fixtures/google/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_multi_turn_conversation__conversation_stream.snap index 714acb09..6fa6cb0c 100644 --- a/crates/jp_llm/tests/fixtures/google/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_structured_output__conversation_stream.snap index 037ac692..82912ae0 100644 --- a/crates/jp_llm/tests/fixtures/google/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_auto__conversation_stream.snap index 5d4822eb..cc1af05c 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_function__conversation_stream.snap index 8f3ae006..e91f9656 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_reasoning__conversation_stream.snap index f0638b7b..45dbddd3 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_required_no_reasoning__conversation_stream.snap index de4ca48c..2aaf9fd6 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_required_reasoning__conversation_stream.snap index c6f77d5b..918c9d68 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_required_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/google/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/google/test_tool_call_stream__conversation_stream.snap index 421e4385..6f7bdd55 100644 --- a/crates/jp_llm/tests/fixtures/google/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/google/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_chat_completion_stream__conversation_stream.snap index 61064a2a..1d05e755 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_image_attachment__conversation_stream.snap index b2be62d5..a03456aa 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_multi_turn_conversation__conversation_stream.snap index 77022204..ca2c2e3b 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_structured_output__conversation_stream.snap index 6078df4f..f6966a63 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_auto__conversation_stream.snap index 89469fc8..d98ffe5c 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_function__conversation_stream.snap index 57fa3a7d..a95c4a5c 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_reasoning__conversation_stream.snap index 9fd02c7b..27573a8c 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_reasoning__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_no_reasoning__conversation_stream.snap index e3bfdf48..dc347924 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_reasoning__conversation_stream.snap index 9fd02c7b..27573a8c 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_required_reasoning__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_stream__conversation_stream.snap index 89469fc8..d98ffe5c 100644 --- a/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/llamacpp/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_chat_completion_stream__conversation_stream.snap index e00f1b69..7a4453a4 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_image_attachment__conversation_stream.snap index 1a25f4aa..c381d41e 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_multi_turn_conversation__conversation_stream.snap index f8b29679..4b77f341 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_structured_output__conversation_stream.snap index 524c3644..bf400d08 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_auto__conversation_stream.snap index da214757..b78cafc5 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_function__conversation_stream.snap index 6fb90cbd..f8214c75 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_reasoning__conversation_stream.snap index 2559fae3..e3b860ba 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_no_reasoning__conversation_stream.snap index 9482c4dd..3f14e2ff 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_reasoning__conversation_stream.snap index 0905110e..5ddc62e5 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_required_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_stream__conversation_stream.snap index f40c0091..ed2e0f23 100644 --- a/crates/jp_llm/tests/fixtures/ollama/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/ollama/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_chat_completion_stream__conversation_stream.snap index 696da68a..091c86ca 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_image_attachment__conversation_stream.snap index 2ffacc4a..0dad48bb 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_multi_turn_conversation__conversation_stream.snap index 96dee2e8..9c04876c 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_structured_output__conversation_stream.snap index 71435471..bc276e67 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_auto__conversation_stream.snap index e0897e6b..3234fd5b 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_function__conversation_stream.snap index d71a5882..894e45f4 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_reasoning__conversation_stream.snap index 431a6fe4..005775d4 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_no_reasoning__conversation_stream.snap index c6c05fcb..a2f60504 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_reasoning__conversation_stream.snap index b6274747..2dbdd128 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_required_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openai/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openai/test_tool_call_stream__conversation_stream.snap index a1167747..597ba493 100644 --- a/crates/jp_llm/tests/fixtures/openai/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openai/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/anthropic_test_sub_provider_event_metadata__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/anthropic_test_sub_provider_event_metadata__conversation_stream.snap index 98a7c490..b337f133 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/anthropic_test_sub_provider_event_metadata__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/anthropic_test_sub_provider_event_metadata__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/google_test_sub_provider_event_metadata__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/google_test_sub_provider_event_metadata__conversation_stream.snap index 0189b313..a88adae7 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/google_test_sub_provider_event_metadata__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/google_test_sub_provider_event_metadata__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/minimax_test_sub_provider_event_metadata__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/minimax_test_sub_provider_event_metadata__conversation_stream.snap index b4b7ea6e..7679d0bb 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/minimax_test_sub_provider_event_metadata__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/minimax_test_sub_provider_event_metadata__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_chat_completion_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_chat_completion_stream__conversation_stream.snap index dd78027c..fa6c7c37 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_chat_completion_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_chat_completion_stream__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_image_attachment__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_image_attachment__conversation_stream.snap index 63ae615f..ff21b6a3 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_image_attachment__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_image_attachment__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_multi_turn_conversation__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_multi_turn_conversation__conversation_stream.snap index beaee735..77585b19 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_multi_turn_conversation__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_multi_turn_conversation__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_structured_output__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_structured_output__conversation_stream.snap index 0e89f1b7..b34bba3b 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_structured_output__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_structured_output__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_auto__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_auto__conversation_stream.snap index 506b51bc..6dab341b 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_auto__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_auto__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_function__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_function__conversation_stream.snap index d7bcca72..8f3857a1 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_function__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_function__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_reasoning__conversation_stream.snap index c24d443a..96dc5191 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_no_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_no_reasoning__conversation_stream.snap index dd4cb981..b0135502 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_no_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_no_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_reasoning__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_reasoning__conversation_stream.snap index 61f5dc70..f25c6988 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_reasoning__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_required_reasoning__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_stream__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_stream__conversation_stream.snap index db794232..5d23e7f3 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_stream__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/test_tool_call_stream__conversation_stream.snap @@ -54,7 +54,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_llm/tests/fixtures/openrouter/x-ai_test_sub_provider_event_metadata__conversation_stream.snap b/crates/jp_llm/tests/fixtures/openrouter/x-ai_test_sub_provider_event_metadata__conversation_stream.snap index c7936d49..34c81321 100644 --- a/crates/jp_llm/tests/fixtures/openrouter/x-ai_test_sub_provider_event_metadata__conversation_stream.snap +++ b/crates/jp_llm/tests/fixtures/openrouter/x-ai_test_sub_provider_event_metadata__conversation_stream.snap @@ -57,7 +57,6 @@ expression: v }, "style": { "code": { - "theme": "", "color": false, "line_numbers": false, "file_link": "osc8", diff --git a/crates/jp_md/src/format.rs b/crates/jp_md/src/format.rs index a9eac07c..1422dc6c 100644 --- a/crates/jp_md/src/format.rs +++ b/crates/jp_md/src/format.rs @@ -48,17 +48,17 @@ pub enum HrStyle { /// A default background color applied to all content, with a fill mode /// controlling how far it extends on each line. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DefaultBackground { - /// ANSI 256-color index. - pub color: u8, + /// SGR background parameter, e.g. `"48;5;236"` or `"48;2;80;73;69"`. + pub param: String, /// How far the background extends on each line. pub fill: BackgroundFill, } /// Per-call options for [`Formatter::format_terminal_with`]. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct TerminalOptions { /// Default background color applied to all content in this block. /// @@ -87,6 +87,12 @@ pub struct Formatter { /// terminal width. When `None`, the configured `width` is used as a /// fallback. terminal_width: Option, + + /// Override background color for inline code spans. + /// + /// When set, inline code uses this color instead of the theme's background. + /// Stored as a pre-resolved `(sgr_param, full_escape)` pair. + inline_code_bg: Option<(String, String)>, } impl fmt::Debug for Formatter { @@ -97,6 +103,7 @@ impl fmt::Debug for Formatter { .field("theme", &"") .field("hr_style", &self.hr_style) .field("terminal_width", &self.terminal_width) + .field("inline_code_bg", &self.inline_code_bg) .finish() } } @@ -117,6 +124,7 @@ impl Formatter { theme: theme::resolve(None), hr_style: HrStyle::default(), terminal_width: None, + inline_code_bg: None, } } @@ -131,6 +139,7 @@ impl Formatter { theme: theme::resolve(None), hr_style: HrStyle::default(), terminal_width: None, + inline_code_bg: None, } } @@ -170,6 +179,19 @@ impl Formatter { self } + /// Override the background color for inline code spans. + /// + /// When set, inline code uses this color instead of the theme's + /// background. The color is pre-resolved to an SGR `(param, escape)` pair. + #[must_use] + pub fn inline_code_bg(mut self, param: Option) -> Self { + self.inline_code_bg = param.map(|p| { + let escape = format!("\x1b[{p}m"); + (p, escape) + }); + self + } + /// Format the markdown into a consistent style. /// /// # Errors @@ -222,6 +244,7 @@ impl Formatter { &hr_options, &self.theme, options.default_background.as_ref(), + self.inline_code_bg.as_ref(), &mut buf, )?; Ok(buf) diff --git a/crates/jp_md/src/format_tests.rs b/crates/jp_md/src/format_tests.rs index 5443cdf2..6b49339a 100644 --- a/crates/jp_md/src/format_tests.rs +++ b/crates/jp_md/src/format_tests.rs @@ -288,7 +288,7 @@ fn test_terminal_blockquote_nested() { fn test_default_background_terminal_fill() { let opts = TerminalOptions { default_background: Some(DefaultBackground { - color: 236, + param: "48;5;236".into(), fill: BackgroundFill::Terminal, }), }; diff --git a/crates/jp_md/src/render.rs b/crates/jp_md/src/render.rs index 9071ff3b..5a539941 100644 --- a/crates/jp_md/src/render.rs +++ b/crates/jp_md/src/render.rs @@ -68,6 +68,7 @@ pub fn format_terminal( hr_options: &HrOptions, theme: &Theme, default_background: Option<&DefaultBackground>, + inline_code_bg: Option<&(String, String)>, output: &mut dyn Write, ) -> fmt::Result { let mut f = TerminalFormatter::new( @@ -77,6 +78,7 @@ pub fn format_terminal( hr_options, theme, default_background, + inline_code_bg, output, ); f.format(root) @@ -102,6 +104,9 @@ pub struct TerminalFormatter<'a, 'w> { /// Syntax highlighting theme. theme: &'w Theme, + /// Override background for inline code spans, if configured. + inline_code_bg: Option<&'w (String, String)>, + /// Stack of ordered list start numbers. ol_stack: Vec, @@ -121,6 +126,7 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { hr_options: &'w HrOptions, theme: &'w Theme, default_background: Option<&DefaultBackground>, + inline_code_bg: Option<&'w (String, String)>, output: &'w mut dyn Write, ) -> Self { Self { @@ -129,6 +135,7 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { table_options, hr_options, theme, + inline_code_bg, ol_stack: vec![], blockquote_depth: 0, blockquote_fg: theme_blockquote_fg(theme), @@ -146,9 +153,8 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { } // Emit the default background escape at the very start. - if let Some(bg) = self.writer.default_background { - self.writer - .write_escape(&format!("\x1b[48;5;{}m", bg.color))?; + if let Some(ref bg) = self.writer.default_background { + self.writer.write_escape(&format!("\x1b[{}m", bg.param))?; } let mut stack = vec![(root, Phase::Pre)]; @@ -574,7 +580,9 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { let numticks = shortest_unused_sequence(literal.as_bytes(), b'`'); - let (bg_param, bg_escape) = theme_bg(self.theme); + let (bg_param, bg_escape) = self + .inline_code_bg + .map_or_else(|| theme_bg(self.theme), |(p, e)| (p.clone(), e.clone())); self.writer.attrs.background = Some(bg_param); self.writer.write_escape(&bg_escape)?; @@ -604,8 +612,8 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { } // Restore default background if one is set, otherwise clear. - if let Some(bg) = self.writer.default_background { - let param = format!("48;5;{}", bg.color); + if let Some(ref bg) = self.writer.default_background { + let param = bg.param.clone(); let esc = format!("\x1b[{param}m"); self.writer.attrs.background = Some(param); self.writer.write_escape(&esc)?; @@ -802,6 +810,7 @@ impl<'a, 'w> TerminalFormatter<'a, 'w> { self.hr_options, self.theme, self.writer.default_background.as_ref(), + self.inline_code_bg, ) { self.writer.output(&rendered, false)?; } diff --git a/crates/jp_md/src/table.rs b/crates/jp_md/src/table.rs index 0964b779..fd14c36f 100644 --- a/crates/jp_md/src/table.rs +++ b/crates/jp_md/src/table.rs @@ -62,8 +62,16 @@ pub fn format_table( hr_options: &HrOptions, theme: &Theme, default_background: Option<&DefaultBackground>, + inline_code_bg: Option<&(String, String)>, ) -> Option { - let (alignments, rows) = extract_table(node, options, hr_options, theme, default_background)?; + let (alignments, rows) = extract_table( + node, + options, + hr_options, + theme, + default_background, + inline_code_bg, + )?; // Compute visual widths for each column. let num_cols = alignments.len(); @@ -154,6 +162,7 @@ fn extract_table( hr_options: &HrOptions, theme: &Theme, default_background: Option<&DefaultBackground>, + inline_code_bg: Option<&(String, String)>, ) -> Option<(Vec, Vec>)> { let alignments = match node.data().value { NodeValue::Table(ref nt) => nt.alignments.clone(), @@ -173,8 +182,14 @@ fn extract_table( continue; } - let rendered = - render_cell_content(cell_node, options, hr_options, theme, default_background); + let rendered = render_cell_content( + cell_node, + options, + hr_options, + theme, + default_background, + inline_code_bg, + ); cells.push(RenderedCell { rendered }); } rows.push(cells); @@ -192,6 +207,7 @@ fn render_cell_content( hr_options: &HrOptions, theme: &Theme, default_background: Option<&DefaultBackground>, + inline_code_bg: Option<&(String, String)>, ) -> String { let mut buf = String::new(); { @@ -210,6 +226,7 @@ fn render_cell_content( hr_options, theme, default_background, + inline_code_bg, &mut buf, ); diff --git a/crates/jp_md/src/table_tests.rs b/crates/jp_md/src/table_tests.rs index f88707c3..df3e2a5c 100644 --- a/crates/jp_md/src/table_tests.rs +++ b/crates/jp_md/src/table_tests.rs @@ -111,7 +111,8 @@ fn test_format_simple_table() { style: HrStyle::Markdown, terminal_width: None, }; - let result = format_table(table_node, &opts, &hr_opts, &theme, None).expect("should format"); + let result = + format_table(table_node, &opts, &hr_opts, &theme, None, None).expect("should format"); // Verify alignment: all columns should have consistent pipe positions. let lines: Vec<&str> = result.trim().lines().collect(); @@ -160,7 +161,8 @@ fn test_format_table_with_wrapping() { style: HrStyle::Markdown, terminal_width: None, }; - let result = format_table(table_node, &opts, &hr_opts, &theme, None).expect("should format"); + let result = + format_table(table_node, &opts, &hr_opts, &theme, None, None).expect("should format"); // Every line must respect the width cap. for line in result.lines() { @@ -211,7 +213,8 @@ fn test_format_table_wrapping_respects_alignment() { style: HrStyle::Markdown, terminal_width: None, }; - let result = format_table(table_node, &opts, &hr_opts, &theme, None).expect("should format"); + let result = + format_table(table_node, &opts, &hr_opts, &theme, None, None).expect("should format"); // All data lines should have consistent pipe positions. let data_lines: Vec<&str> = result @@ -273,7 +276,8 @@ fn test_format_aligned_table() { style: HrStyle::Markdown, terminal_width: None, }; - let result = format_table(table_node, &opts, &hr_opts, &theme, None).expect("should format"); + let result = + format_table(table_node, &opts, &hr_opts, &theme, None, None).expect("should format"); // Separator should contain alignment markers. let lines: Vec<&str> = result.trim().lines().collect(); diff --git a/crates/jp_md/src/writer.rs b/crates/jp_md/src/writer.rs index 222bd31a..4679c63a 100644 --- a/crates/jp_md/src/writer.rs +++ b/crates/jp_md/src/writer.rs @@ -84,14 +84,16 @@ impl<'w> TerminalWriter<'w> { width: usize, default_background: Option<&DefaultBackground>, ) -> Self { - let default_background = default_background.copied(); + let default_background = default_background.cloned(); // Pre-populate attrs so the restore logic keeps the default background // active across line breaks. - let attrs = default_background.map_or_else(AnsiState::default, |bg| AnsiState { - background: Some(format!("48;5;{}", bg.color)), - ..Default::default() - }); + let attrs = default_background + .as_ref() + .map_or_else(AnsiState::default, |bg| AnsiState { + background: Some(bg.param.clone()), + ..Default::default() + }); Self { output, @@ -261,7 +263,7 @@ impl<'w> TerminalWriter<'w> { /// - pads with spaces to a fixed column (`Column`), /// - emits `\x1b[K` to fill to the terminal edge (`Terminal`). fn emit_line_fill(&mut self) -> fmt::Result { - let Some(bg) = self.default_background else { + let Some(ref bg) = self.default_background else { return Ok(()); }; @@ -289,7 +291,7 @@ impl<'w> TerminalWriter<'w> { /// Like [`emit_line_fill`](Self::emit_line_fill) but writes directly to the /// output writer. Used in the wrap-break path. fn emit_line_fill_direct(&mut self) -> fmt::Result { - let Some(bg) = self.default_background else { + let Some(ref bg) = self.default_background else { return Ok(()); };