diff --git a/CHANGELOG.md b/CHANGELOG.md index 5046bc2..5f4aca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project uses [independent versioning](README.md#versioning) for Framewo --- +## CLI 3.4.1 — Code-Block Background No Longer Fragments on Narrow Panels + +### Fixed (CLI) +- Fix the gray background of fenced code blocks in `devtrail explore` breaking into truncated stripes when the document panel is narrower than the longest code line. The renderer used to pad each code line to the longest line and let `Paragraph::wrap` re-flow it, which dropped trailing styled whitespace at the wrap point and left visible gaps between content rows. The code-block renderer now hard-wraps lines into chunks no wider than the panel itself (visual-column aware, UTF-8 / CJK safe, indentation preserved), so each visual row paints its own uninterrupted gray gutter regardless of terminal size or live resizes. Blank lines inside code blocks also keep their background. + +--- + ## CLI 3.4.0 — Language-Aware `devtrail explore` ### Added (CLI) diff --git a/README.md b/README.md index b7990a9..6d54aa6 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ DevTrail uses independent version tags for each component: | Component | Tag prefix | Example | Includes | |-----------|-----------|---------|----------| | Framework | `fw-` | `fw-4.3.0` | Templates (12 types), governance, directives | -| CLI | `cli-` | `cli-3.4.0` | The `devtrail` binary | +| CLI | `cli-` | `cli-3.4.1` | The `devtrail` binary | Check installed versions with `devtrail status` or `devtrail about`. diff --git a/cli/Cargo.lock b/cli/Cargo.lock index dd96be2..743f17d 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -537,7 +537,7 @@ dependencies = [ [[package]] name = "devtrail-cli" -version = "3.4.0" +version = "3.4.1" dependencies = [ "anyhow", "arborist-metrics", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8869d0a..feee0ab 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devtrail-cli" -version = "3.4.0" +version = "3.4.1" edition = "2021" description = "CLI tool for DevTrail - Documentation Governance for AI-Assisted Development" license = "MIT" diff --git a/cli/src/tui/markdown.rs b/cli/src/tui/markdown.rs index 5c2add2..065584d 100644 --- a/cli/src/tui/markdown.rs +++ b/cli/src/tui/markdown.rs @@ -153,26 +153,39 @@ pub fn markdown_to_lines(markdown: &str, available_width: usize) -> Vec { in_code_block = false; - // Measure in visual columns so CJK/emoji don't break alignment. - let max_cols = code_block_lines + // Compute a per-Line target width that fits inside the + // panel after subtracting the heading indent and the + // 2-column gutter we paint on each side. Pre-wrapping + // here (instead of letting `Paragraph::wrap` do it later) + // is what keeps the gray background uniform: ratatui's + // word-wrap drops trailing styled whitespace at the + // wrap point, which leaves the gutter stripes broken on + // narrow terminals. + let usable_width = available_width.saturating_sub(content_indent); + let inner_width = usable_width.saturating_sub(4).max(1); + let max_natural = code_block_lines .iter() .map(|l| UnicodeWidthStr::width(l.as_str())) .max() .unwrap_or(0); + let target_width = max_natural.min(inner_width).max(1); let code_bg = Style::default() .fg(Color::Rgb(210, 215, 235)) .bg(Color::Rgb(45, 45, 60)); for code_line in &code_block_lines { - let w = UnicodeWidthStr::width(code_line.as_str()); - let pad = max_cols.saturating_sub(w); - let padded = format!(" {}{} ", code_line, " ".repeat(pad)); - let mut spans: Vec> = Vec::new(); - if content_indent > 0 { - spans.push(Span::raw(" ".repeat(content_indent))); + for chunk in wrap_visual_columns(code_line, inner_width) { + let chunk_width = UnicodeWidthStr::width(chunk.as_str()); + let pad = target_width.saturating_sub(chunk_width); + let padded = + format!(" {}{} ", chunk, " ".repeat(pad)); + let mut spans: Vec> = Vec::new(); + if content_indent > 0 { + spans.push(Span::raw(" ".repeat(content_indent))); + } + spans.push(Span::styled(padded, code_bg)); + lines.push(Line::from(spans)); } - spans.push(Span::styled(padded, code_bg)); - lines.push(Line::from(spans)); } code_block_lines.clear(); lines.push(Line::from("")); @@ -409,6 +422,42 @@ fn compute_column_widths( /// slice offsets are always taken at `char_indices()` boundaries, and /// widths are measured with `unicode-width` so CJK and other double-wide /// characters account for two visual columns. +/// Hard-wrap a string into chunks each fitting within `width` visual columns. +/// Unlike `wrap_cell_text`, this never breaks on word boundaries and never +/// trims whitespace — preserving leading indentation is essential for code. +/// UTF-8 safe: every cut lands on a char boundary, and a double-wide char +/// (CJK / emoji) at the boundary moves whole to the next chunk rather than +/// being split. Empty input yields a single empty chunk so that a blank +/// line in the source still gets rendered as one styled line. +fn wrap_visual_columns(s: &str, width: usize) -> Vec { + if width == 0 || s.is_empty() { + return vec![s.to_string()]; + } + if UnicodeWidthStr::width(s) <= width { + return vec![s.to_string()]; + } + + let mut chunks: Vec = Vec::new(); + let mut current = String::new(); + let mut current_width = 0usize; + for ch in s.chars() { + let w = UnicodeWidthChar::width(ch).unwrap_or(0); + if current_width + w > width && !current.is_empty() { + chunks.push(std::mem::take(&mut current)); + current_width = 0; + } + current.push(ch); + current_width += w; + } + if !current.is_empty() { + chunks.push(current); + } + if chunks.is_empty() { + chunks.push(String::new()); + } + chunks +} + fn wrap_cell_text(text: &str, width: usize) -> Vec { if width == 0 { return vec![text.to_string()]; @@ -794,4 +843,107 @@ mod tests { assert!(*w <= naturals[i].max(3), "col {i} exceeded its natural"); } } + + #[test] + fn wrap_visual_columns_short_returns_single_chunk() { + let out = wrap_visual_columns("hello world", 80); + assert_eq!(out, vec!["hello world".to_string()]); + } + + #[test] + fn wrap_visual_columns_empty_yields_one_empty_chunk() { + // A blank line in a code block must still emit one styled line so + // the gray gutter is uninterrupted; otherwise the renderer would + // skip it and the bg would have a one-row gap. + let out = wrap_visual_columns("", 40); + assert_eq!(out, vec![String::new()]); + } + + #[test] + fn wrap_visual_columns_hard_wraps_long_line() { + let out = wrap_visual_columns( + "System(ecommerce, \"E-Commerce Platform\", \"Allows customers\")", + 20, + ); + for chunk in &out { + assert!( + UnicodeWidthStr::width(chunk.as_str()) <= 20, + "chunk {chunk:?} exceeds 20 cols", + ); + } + assert_eq!(out.concat().len(), "System(ecommerce, \"E-Commerce Platform\", \"Allows customers\")".len()); + } + + #[test] + fn wrap_visual_columns_preserves_leading_indentation() { + // Code indentation must survive: a 4-space-indented line should + // not be trimmed (which would corrupt Python/YAML/etc. semantics). + let out = wrap_visual_columns(" indented_call(arg)", 40); + assert!(out[0].starts_with(" ")); + } + + #[test] + fn wrap_visual_columns_cjk_does_not_split_double_wide_chars() { + // Width=3 with three double-wide chars: each chunk should hold one + // ideogram (visual width 2), never half of one. Forward progress + // is guaranteed even when no char fits within a strict width<2. + let out = wrap_visual_columns("数据表", 3); + for chunk in &out { + // Every chunk must be a valid UTF-8 string with whole ideograms. + assert!(std::str::from_utf8(chunk.as_bytes()).is_ok()); + assert!(UnicodeWidthStr::width(chunk.as_str()) <= 3); + } + assert_eq!(out.concat(), "数据表"); + } + + /// Regression: a code block whose longest line exceeds the available + /// width must produce one Line per visual row, none wider than + /// `available_width`. This is what keeps the gray background uniform + /// — without pre-wrapping, ratatui's `Paragraph::wrap` re-flows our + /// padded line and drops trailing styled spaces, leaving stripes. + #[test] + fn code_block_wraps_within_panel_width() { + let md = "```\nSystem(ecommerce, \"E-Commerce Platform\", \"Allows customers to browse and purchase products\")\n```\n"; + let body_width = 40; + let lines = markdown_to_lines(md, body_width); + for line in &lines { + let w: usize = line + .spans + .iter() + .map(|s| UnicodeWidthStr::width(s.content.as_ref())) + .sum(); + assert!( + w <= body_width, + "line wider than panel: {w} > {body_width} ({:?})", + line.spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>(), + ); + } + } + + /// Blank lines inside a code block must still emit a styled Line so the + /// gutter background runs continuously. Without this, the screenshot + /// the user reported showed truncated stripes between content rows. + #[test] + fn code_block_blank_lines_keep_background() { + let md = "```\nfirst\n\nthird\n```\n"; + let lines = markdown_to_lines(md, 80); + // First, blank, third → 3 styled lines. + let styled: Vec<_> = lines + .iter() + .filter(|l| { + l.spans + .iter() + .any(|s| s.style.bg == Some(Color::Rgb(45, 45, 60))) + }) + .collect(); + assert_eq!( + styled.len(), + 3, + "expected 3 styled code lines (incl. the blank), got {}", + styled.len() + ); + } } diff --git a/docs/adopters/CLI-REFERENCE.md b/docs/adopters/CLI-REFERENCE.md index 3f5a389..6126c94 100644 --- a/docs/adopters/CLI-REFERENCE.md +++ b/docs/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail uses **independent version tags** for each component: | Component | Tag prefix | Example | What it includes | |-----------|-----------|---------|------------------| | Framework | `fw-` | `fw-4.3.0` | Templates (12 types), governance docs, directives | -| CLI | `cli-` | `cli-3.4.0` | The `devtrail` binary | +| CLI | `cli-` | `cli-3.4.1` | The `devtrail` binary | Framework and CLI are released independently. A framework update does not require a CLI update, and vice versa. @@ -110,7 +110,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.3.0 Updating CLI... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -143,11 +143,11 @@ Use `--method` to override auto-detection: `--method=github` or `--method=cargo` ```bash $ devtrail update-cli -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -210,7 +210,7 @@ $ devtrail status ┌───────────┬──────────────────────────┐ │ Path │ /home/user/my-project │ │ Framework │ fw-4.3.0 │ - │ CLI │ cli-3.4.0 │ + │ CLI │ cli-3.4.1 │ │ Language │ en │ └───────────┴──────────────────────────┘ @@ -687,7 +687,7 @@ Show version, authorship, and license information. ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.4.0 + CLI version: cli-3.4.1 Framework version: fw-4.3.0 Author: Strange Days Tech, S.A.S. License: MIT diff --git a/docs/i18n/es/README.md b/docs/i18n/es/README.md index ad1a455..4fa92b5 100644 --- a/docs/i18n/es/README.md +++ b/docs/i18n/es/README.md @@ -150,7 +150,7 @@ DevTrail usa tags de versión independientes para cada componente: | Componente | Prefijo de tag | Ejemplo | Incluye | |------------|---------------|---------|---------| | Framework | `fw-` | `fw-4.3.0` | Plantillas (12 tipos), gobernanza, directivas | -| CLI | `cli-` | `cli-3.4.0` | El binario `devtrail` | +| CLI | `cli-` | `cli-3.4.1` | El binario `devtrail` | Verifica las versiones instaladas con `devtrail status` o `devtrail about`. diff --git a/docs/i18n/es/adopters/CLI-REFERENCE.md b/docs/i18n/es/adopters/CLI-REFERENCE.md index 0c71a84..2e93c81 100644 --- a/docs/i18n/es/adopters/CLI-REFERENCE.md +++ b/docs/i18n/es/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail usa **tags de versión independientes** para cada componente: | Componente | Prefijo de tag | Ejemplo | Qué incluye | |------------|---------------|---------|-------------| | Framework | `fw-` | `fw-4.3.0` | Plantillas (12 tipos), docs de gobernanza, directivas | -| CLI | `cli-` | `cli-3.4.0` | El binario `devtrail` | +| CLI | `cli-` | `cli-3.4.1` | El binario `devtrail` | Framework y CLI se publican de forma independiente. Una actualización del framework no requiere actualización del CLI, y viceversa. @@ -109,7 +109,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.3.0 Updating CLI... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -142,11 +142,11 @@ Usa `--method` para forzar el método: `--method=github` o `--method=cargo`. ```bash $ devtrail update-cli -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -204,7 +204,7 @@ DevTrail Status ─────────────── Path: /home/user/my-project Framework version: fw-4.3.0 -CLI version: cli-3.4.0 +CLI version: cli-3.4.1 Language: en Structure: ✔ Complete @@ -559,7 +559,7 @@ Muestra información de versión, autoría y licencia. ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.4.0 + CLI version: cli-3.4.1 Framework version: fw-4.3.0 Author: Strange Days Tech, S.A.S. License: MIT diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index d90c8c0..285267e 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -150,7 +150,7 @@ DevTrail 为每个组件使用独立的版本标签: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| | Framework | `fw-` | `fw-4.3.0` | 模板(12 种类型)、治理文档、指令 | -| CLI | `cli-` | `cli-3.4.0` | `devtrail` 二进制文件 | +| CLI | `cli-` | `cli-3.4.1` | `devtrail` 二进制文件 | 使用 `devtrail status` 或 `devtrail about` 查看已安装的版本。 diff --git a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md index 5d29715..6f7675f 100644 --- a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md +++ b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail 为每个组件使用**独立的版本标签**: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| | Framework | `fw-` | `fw-4.3.0` | 模板(12 种类型)、治理文档、指令 | -| CLI | `cli-` | `cli-3.4.0` | `devtrail` 二进制文件 | +| CLI | `cli-` | `cli-3.4.1` | `devtrail` 二进制文件 | Framework 和 CLI 独立发布。Framework 更新不需要 CLI 更新,反之亦然。 @@ -110,7 +110,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.3.0 Updating CLI... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -143,11 +143,11 @@ $ devtrail update-framework ```bash $ devtrail update-cli -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.4.0 +✔ CLI updated to cli-3.4.1 ``` --- @@ -210,7 +210,7 @@ $ devtrail status ┌───────────┬──────────────────────────┐ │ Path │ /home/user/my-project │ │ Framework │ fw-4.3.0 │ - │ CLI │ cli-3.4.0 │ + │ CLI │ cli-3.4.1 │ │ Language │ en │ └───────────┴──────────────────────────┘ @@ -680,7 +680,7 @@ $ devtrail explore --lang es # 会话内切换到西班牙语 ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.4.0 + CLI version: cli-3.4.1 Framework version: fw-4.3.0 Author: Strange Days Tech, S.A.S. License: MIT