From 581bb7da01c0c501af808f9514c90e093b422d24 Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Sun, 13 Sep 2026 20:58:29 +0100 Subject: [PATCH 1/2] fix(output): align ANSI-coloured cells under --table-style Every --table-style except the default renders through comfy-table, which measured cell width in bytes, so ANSI_COLOR output was padded as if the escape codes were visible and the table borders drifted. The default renderer already sized columns by display width. Enable comfy-table's custom_styling feature, which strips escapes when measuring. Tests cover coloured cells in all 13 styles, with and without --max-col-width truncation; 15 of them fail without the feature. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UT2eunuVZv5L2tBTs2zStB --- Cargo.toml | 4 +- .../test_table_output_alignment.py | 60 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f9d78fa8..b4b49e98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,9 @@ serde_json = "1.0" dirs = "5.0" reqwest = { version = "0.11", features = ["json", "blocking", "multipart"] } tokio = { version = "1.35", features = ["full"] } -comfy-table = "7.0" +# custom_styling: measure cell width with ANSI escapes stripped, so ANSI_COLOR +# output lines up under --table-style (the default renderer already does this) +comfy-table = { version = "7.0", features = ["custom_styling"] } csv = "1.3" ratatui = "0.29" tui-input = { version = "0.11", features = ["crossterm"] } diff --git a/tests/python_tests/test_table_output_alignment.py b/tests/python_tests/test_table_output_alignment.py index 1ef08466..ee8e7e86 100644 --- a/tests/python_tests/test_table_output_alignment.py +++ b/tests/python_tests/test_table_output_alignment.py @@ -11,6 +11,7 @@ """ import os +import re import subprocess import sys import unicodedata @@ -26,8 +27,15 @@ def _sql_cli_binary(): return base_dir / "target" / "release" / f"sql-cli{suffix}" +ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") + + def _display_width(text): - """Column count of a string, matching the Rust side's unicode-width use.""" + """Column count of a string, matching the Rust side's unicode-width use. + + ANSI escapes (from ANSI_COLOR and friends) take no columns on a terminal. + """ + text = ANSI_ESCAPE.sub("", text) return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in text) @@ -147,6 +155,56 @@ def test_markdown_style_is_also_aligned(tmp_path): assert_lines_aligned(lines) +# Coloured cells: the default renderer always sized columns by display width, but +# the comfy-table styles counted the escape bytes, so a cell like +# ANSI_COLOR('yellow', '3h') was padded as if it were 11 columns wide. + +ALL_TABLE_STYLES = [ + "default", + "ascii", + "ascii-condensed", + "ascii-borders", + "ascii-horizontal", + "ascii-noborders", + "markdown", + "utf8", + "utf8-condensed", + "utf8-borders", + "utf8-horizontal", + "utf8-noborders", + "plain", +] + +COLOURED_QUERY = ( + "SELECT ANSI_COLOR('yellow', Project) AS project, " + "ANSI_COLOR('green', DurationSecs) AS secs, DurationSecs AS raw " + "FROM projects" +) + + +@pytest.mark.parametrize("style", ALL_TABLE_STYLES) +def test_coloured_cells_are_aligned_in_every_style(tmp_path, style): + lines = render_table( + PROJECTS_CSV, + COLOURED_QUERY, + extra_args=["--table-style", style, "--max-col-width", "0"], + tmp_path=tmp_path, + ) + assert any("\x1b[" in line for line in lines), "expected ANSI colour in the output" + assert_lines_aligned(lines) + + +@pytest.mark.parametrize("style", ["default", "ascii", "utf8-noborders", "plain"]) +def test_coloured_cells_stay_aligned_when_truncated(tmp_path, style): + lines = render_table( + PROJECTS_CSV, + COLOURED_QUERY, + extra_args=["--table-style", style, "--max-col-width", "12"], + tmp_path=tmp_path, + ) + assert_lines_aligned(lines) + + # Markdown is a --table-style, but it reads as an output format, so `-o markdown` # used to be rejected outright. It is now shorthand for the table + markdown style. From fe4dddb13b1bdd7b15cca5407583376681eb3f45 Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Sun, 13 Sep 2026 21:00:27 +0100 Subject: [PATCH 2/2] feat(scripts): add lrt, an ls -alrt built on sql-cli PowerShell function listing the most recently modified files through the FILE CTE: separate date and time columns in local time, a coloured human-readable age, FORMAT_BYTES sizes and names coloured by type. Supports recursion, glob filters, directories and any --table-style. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UT2eunuVZv5L2tBTs2zStB --- scripts/lrt.ps1 | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 scripts/lrt.ps1 diff --git a/scripts/lrt.ps1 b/scripts/lrt.ps1 new file mode 100644 index 00000000..d9c0b55b --- /dev/null +++ b/scripts/lrt.ps1 @@ -0,0 +1,70 @@ +# lrt - most recently modified files, an `ls -alrt` built on sql-cli. +# +# Load it from your PowerShell profile: +# . C:\path\to\sql-cli\scripts\lrt.ps1 +# +# Usage: +# lrt # 20 newest files in the current directory +# lrt 50 src -r # 50 newest under src, recursive +# lrt -g '*.log' # only files matching a glob +# lrt -d # include directories +# lrt -style utf8 # any style from `sql-cli --list-table-styles` +# +# The default style is `plain` (no borders). It needs a sql-cli built with the +# ANSI width fix (after v1.85.4); on older builds coloured cells misalign in +# every style except `default`, so use `lrt -style default` there. +function lrt { + param( + [int]$n = 20, + [string]$path = '.', + [switch]$r, + [string]$g, + [switch]$d, + [string]$style = 'plain' + ) + $p = $path -replace "'", "''" + $opts = @() + if ($r) { $opts += 'RECURSIVE' } + if ($g) { $opts += "GLOB '$($g -replace "'", "''")'" } + $dirFilter = if ($d) { '' } else { 'AND is_dir = false' } + # recursive: show the path relative to the root, not the full \\?\C:\... path + $root = ('\\?\' + (Resolve-Path $path).ProviderPath.TrimEnd('\') + '\') -replace "'", "''" + $nameCol = if ($r) { "REPLACE(path, '$root', '')" } else { 'name' } + # The date functions work in UTC; shift to local time for display. This uses + # today's offset, so files from the other side of a DST change show an hour out. + $off = [int][TimeZoneInfo]::Local.GetUtcOffset((Get-Date)).TotalMinutes + + $sql = @" +WITH f AS (FILE PATH '$p' $($opts -join ' ')), +g AS ( + SELECT $nameCol AS name, ext, size, is_dir, + DATEADD('minute', $off, modified) AS lm, + DATEDIFF('second', modified, NOW()) AS secs + FROM f WHERE depth > 0 $dirFilter +) +SELECT + FORMAT_DATE(lm, '%Y-%m-%d') AS date, + FORMAT_DATE(lm, '%H:%M:%S') AS time, + CASE + WHEN secs < 60 THEN ANSI_COLOR('bright_green', secs || 's') + WHEN secs < 3600 THEN ANSI_COLOR('green', FLOOR(secs / 60) || 'm') + WHEN secs < 86400 THEN ANSI_COLOR('yellow', FLOOR(secs / 3600) || 'h') + WHEN secs < 604800 THEN FLOOR(secs / 86400) || 'd' + ELSE ANSI_COLOR('bright_black', FLOOR(secs / 86400) || 'd') + END AS age, + IIF(is_dir, '', FORMAT_BYTES(size)) AS size, + CASE + WHEN is_dir THEN ANSI_COLOR('bright_blue', name) + WHEN ext IN ('rs', 'py', 'sql', 'ps1', 'cs', 'lua') THEN ANSI_COLOR('cyan', name) + WHEN ext IN ('md', 'txt') THEN ANSI_COLOR('magenta', name) + WHEN ext IN ('log', 'csv', 'json') THEN ANSI_COLOR('yellow', name) + WHEN ext IN ('exe', 'dll', 'zip') THEN ANSI_COLOR('red', name) + ELSE name + END AS name +FROM g +ORDER BY secs ASC +LIMIT $n +"@ + # The "# Query completed" footer goes to stderr + sql-cli -q $sql -o table --table-style $style --max-col-width 0 2>$null +}