Skip to content

Commit 5c125d5

Browse files
authored
Add width layout modes (#36)
* wip * wip * wip * wip * wip * Refine width layout docs * wip
1 parent b4c81e9 commit 5c125d5

14 files changed

Lines changed: 172 additions & 99 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,27 @@
1-
# AGENTS
2-
31
## Project
42

53
- Small Zig CLI for rendering CSV as terminal tables.
6-
- Keep modules narrow: `args` parses CLI, `main` owns process flow, `render` owns output, `util` holds shared helpers.
7-
- Prefer simple value types and explicit ownership.
84

9-
## CLI
5+
## Important
106

11-
- `Args.init(alloc, argv)` returns structured control flow.
12-
- Use `Action` for early exits: `banner`, `fatal`, `help`, `version`.
13-
- Fatal CLI/setup errors should return owned `err_str`; `main` prints and picks the exit code.
7+
- Branch names: `^[a-z_]+$`
8+
- COMMIT: include all current changes by default
9+
- PR bodies: 1-2 bullets max, use --body-file, no backticks
1410

15-
## I/O
11+
## Zig Style
1612

17-
- Use `/dev/tty` only for terminal probing such as width or background detection.
18-
- Do not mix terminal probing with stdin table input.
1913
- Use `util.stdout` and `util.stderr` for shared buffered output.
20-
21-
## Memory
22-
2314
- Never return slices into stack buffers.
2415
- Allocate any string that must outlive the current scope.
16+
- In `src/*.zig`, add a one-line comment to each struct and function.
17+
- Keep imports sorted at the bottom of each file.
2518

2619
## Tests
2720

2821
- Prefer `just llm`. Run `just check` before commits and after larger refactors.
2922
- Keep tests deterministic. Force `--width 80` where layout matters.
30-
- Prefer table-driven tests and tiny helpers in `test_support` or the local test section when they reduce repetition.
31-
- This is partly for token reduction, colllapse if clarity stays good
23+
- Prefer table-driven tests and tiny helpers in `test_support`, reduce repetition.
24+
- This is partly for token reduction, collapse if clarity stays good
3225
- In `src/*.zig`, if a file has tests, add:
3326
`//`
3427
`// testing`
@@ -37,9 +30,5 @@
3730
## Style
3831

3932
- Keep files and APIs small and direct.
40-
- Prefer straightforward Zig control flow.
41-
- In `src/*.zig`, add a one-line comment to each struct and function.
42-
- Keep imports sorted at the bottom of each file.
43-
- Branch names should match `^[a-z_]+$`.
44-
- When the user says `commit`, commit all current changes by default, including unrelated local edits.
33+
- Prefer simple value types and explicit ownership.
4534
- With `gh pr create`, never use unescaped backticks; prefer `--body-file`.

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ $ zig build
4949
-n, --row-numbers Turn on row numbers
5050
-t, --title <string> Add a title to the table
5151
--border <border> Table border style (rounded|thin|double|...)
52+
-p, --pager Send output through $PAGER or less
5253
--peek Show csv shape, sample, and handy stats
5354
--zebra Turn on zebra stripes
5455
@@ -66,11 +67,10 @@ $ zig build
6667
--color <color> Turn color off and on (on|off|auto)
6768
--delimiter <char> Set CSV delim (can be any char or "tab")
6869
--digits <int> Digits after decimal for float columns
69-
-p, --pager Send output through $PAGER or less
7070
--table <table> Select the db table (for sqlite)
7171
--theme <theme> Select color theme (auto|dark|light)
7272
--vanilla Disable numeric formatting
73-
--width <int> Set max table width in chars
73+
--width <width> Set table width, or try (min|max)
7474
7575
--completion <shell> Print shell completion (bash|zsh)
7676
--help Get help
@@ -88,10 +88,12 @@ Tennis works fine with Unicode and emoji content. Calculating non-ASCII display
8888

8989
### Colors, Themes, Appearance
9090

91-
Tennis picks a color theme based on the color of your terminal. Color is on by default. It also honors `NO_COLOR=1`. See `--color` and `--theme`. Max terminal width is pulled from your terminal, or defaults to 80 if we can't figure it out. See `--width` if you need to override it or want something predictable for CI/tests.
91+
Tennis picks a color theme based on the color of your terminal. Color is on by default. It also honors `NO_COLOR=1`. Terminal width is pulled from your terminal, or defaults to 80 if we can't figure it out. See `--width` if you want to override layout.
9292

9393
Use `--border`, `--row-numbers`, `--title`, and `--zebra` for more bling. Tennis supports the same borders as `nushell`.
9494

95+
It's common to use `--pager` to send output through $PAGER or less.
96+
9597
<img src="./bling.png" width="60%">
9698

9799
### Data, Selection, Order
@@ -127,9 +129,8 @@ We love CSV tools and use them all the time! Here are a few that we rely on:
127129

128130
#### 0.5.0 (unreleased)
129131

130-
- detect and justify percent cols
131-
- idea: max col width and/or better layout controls, this is driving me nuts
132-
- idea: better output for "list of columns" on col arg errors
132+
- `--width min` to layout based on header width, or `--width max` to disable truncation
133+
- percent columns are justified (thanks @nkriege)
133134

134135
#### 0.4.0 (Apr '26)
135136

extra/_tennis

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ _tennis() {
77
'-t[Add a title to the table]:string:' \
88
'--title[Add a title to the table]:string:' \
99
'--border[Table border style (rounded|thin|double|...)]:border:(ascii_rounded basic basic_compact compact compact_double dots double heavy light markdown none psql reinforced restructured rounded single thin with_love)' \
10+
'-p[Send output through $PAGER or less]' \
11+
'--pager[Send output through $PAGER or less]' \
1012
'--peek[Show csv shape, sample, and handy stats]' \
1113
'--zebra[Turn on zebra stripes]' \
1214
'--deselect[De-select comma-separated headers]:headers:' \
@@ -22,12 +24,10 @@ _tennis() {
2224
'--color[Turn color off and on (on|off|auto)]:color:(auto off on)' \
2325
'--delimiter[Set CSV delim (can be any char or "tab")]:char:(tab , \; \|)' \
2426
'--digits[Digits after decimal for float columns]:int:(1 2 3 4 5 6)' \
25-
'-p[Send output through $PAGER or less]' \
26-
'--pager[Send output through $PAGER or less]' \
2727
'--table[Select the db table (for sqlite)]:table:' \
2828
'--theme[Select color theme (auto|dark|light)]:theme:(auto dark light)' \
2929
'--vanilla[Disable numeric formatting]' \
30-
'--width[Set max table width in chars]:int:' \
30+
'--width[Set table width, or try (min|max)]:width:(min max)' \
3131
'--completion[Print shell completion (bash|zsh)]:shell:(bash zsh)' \
3232
'--help[Get help]' \
3333
'--version[Show version number and exit]' \

extra/tennis.1

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
.nh
66
.ad l
77
.\" Begin generated content:
8-
.TH "tennis" "1" "2026-04-07"
8+
.TH "tennis" "1" "2026-04-08"
99
.P
1010
.SH NAME
1111
.P
@@ -62,6 +62,12 @@ Show the shape, some sample rows, and compact per-column stats.\&
6262
Show row numbers.\&
6363
.P
6464
.RE
65+
\fB-p\fR, \fB--pager\fR
66+
.RS 4
67+
Send output through \fB$PAGER\fR or \fBless\fR.\& If \fBLESS\fR is unset, it defaults to
68+
\fBFRX\fR.\&
69+
.P
70+
.RE
6571
\fB-t\fR, \fB--title\fR <string>
6672
.RS 4
6773
Add a title above the table.\&
@@ -141,12 +147,6 @@ Set the number of digits after the decimal for float columns.\& Valid values
141147
\fB1\fR through \fB6\fR, the default is \fB3\fR.\&
142148
.P
143149
.RE
144-
\fB--pager\fR
145-
.RS 4
146-
Send output through \fB$PAGER\fR or \fBless\fR.\& If \fBLESS\fR is unset, it defaults to
147-
\fBFRX\fR.\&
148-
.P
149-
.RE
150150
\fB--theme\fR <theme>
151151
.RS 4
152152
Select the color theme.\& Valid values: \fBauto\fR, \fBdark\fR, \fBlight\fR.\& The default
@@ -158,10 +158,15 @@ is \fBauto\fR.\&
158158
Disable type inference and numeric formatting.\&
159159
.P
160160
.RE
161-
\fB--width\fR <int>
161+
\fB--width\fR <width>
162+
.P
162163
.RS 4
163-
Set the maximum table width in characters.\& By default, \fBtennis\fR uses the
164-
current terminal width or \fB80\fR if it can'\&t be detected.\&
164+
Set the table width.\& If you specify an integer, it will be used as the
165+
max table width and tennis will truncate to fit if necessary.\& By default,
166+
\fBtennis\fR uses the current terminal width or \fB80\fR if it can'\&t be detected.\&
167+
.P
168+
Also supports two special values - \fBmin\fR to layout based on header
169+
width, \fBmax\fR to disable truncation entirely.\&
165170
.P
166171
.RE
167172
.SH BORDERS

extra/tennis.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ _tennis() {
1818
--digits) COMPREPLY=($(compgen -W "1 2 3 4 5 6" -- "${cur}")) ; return ;;
1919
--table) COMPREPLY=() ; return ;;
2020
--theme) COMPREPLY=($(compgen -W "auto dark light" -- "${cur}")) ; return ;;
21-
--width) COMPREPLY=() ; return ;;
21+
--width) COMPREPLY=($(compgen -W "min max" -- "${cur}")) ; return ;;
2222
--completion) COMPREPLY=($(compgen -W "bash zsh" -- "${cur}")) ; return ;;
2323
esac
2424

2525
if [[ "${cur}" == -* ]]; then
26-
COMPREPLY=($(compgen -W "-n --row-numbers -t --title --border --peek --zebra --deselect --select --sort -r --reverse --shuffle --shuf --head --tail --filter --color --delimiter --digits -p --pager --table --theme --vanilla --width --completion --help --version " -- "${cur}"))
26+
COMPREPLY=($(compgen -W "-n --row-numbers -t --title --border -p --pager --peek --zebra --deselect --select --sort -r --reverse --shuffle --shuf --head --tail --filter --color --delimiter --digits --table --theme --vanilla --width --completion --help --version " -- "${cur}"))
2727
else
2828
_filedir csv
2929
[[ ${#COMPREPLY[@]} -eq 0 ]] && _filedir

extra/tennis.scd

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ $ tennis diamonds.csv
4747
*-n*, *--row-numbers*
4848
Show row numbers.
4949

50+
*-p*, *--pager*
51+
Send output through *$PAGER* or *less*. If *LESS* is unset, it defaults to
52+
*FRX*.
53+
5054
*-t*, *--title* <string>
5155
Add a title above the table.
5256

@@ -98,20 +102,21 @@ $ tennis diamonds.csv
98102
Set the number of digits after the decimal for float columns. Valid values
99103
*1* through *6*, the default is *3*.
100104

101-
*--pager*
102-
Send output through *$PAGER* or *less*. If *LESS* is unset, it defaults to
103-
*FRX*.
104-
105105
*--theme* <theme>
106106
Select the color theme. Valid values: *auto*, *dark*, *light*. The default
107107
is *auto*.
108108

109109
*--vanilla*
110110
Disable type inference and numeric formatting.
111111

112-
*--width* <int>
113-
Set the maximum table width in characters. By default, *tennis* uses the
114-
current terminal width or *80* if it can't be detected.
112+
*--width* <width>
113+
114+
Set the table width. If you specify an integer, it will be used as the
115+
max table width and tennis will truncate to fit if necessary. By default,
116+
*tennis* uses the current terminal width or *80* if it can't be detected.
117+
118+
Also supports two special values - *min* to layout based on header
119+
width, *max* to disable truncation entirely.
115120

116121
# BORDERS
117122

src/args.zig

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub const Args = struct {
1212
\\ -n, --row-numbers Turn on row numbers
1313
\\ -t, --title <string> Add a title to the table
1414
\\ --border <border> Table border style (rounded|thin|double|...)
15+
\\ -p, --pager Send output through $PAGER or less
1516
\\ --peek Show csv shape, sample, and handy stats
1617
\\ --zebra Turn on zebra stripes
1718
\\
@@ -29,11 +30,10 @@ pub const Args = struct {
2930
\\ --color <color> Turn color off and on (on|off|auto)
3031
\\ --delimiter <char> Set CSV delim (can be any char or "tab")
3132
\\ --digits <int> Digits after decimal for float columns
32-
\\ -p, --pager Send output through $PAGER or less
3333
\\ --table <table> Select the db table (for sqlite)
3434
\\ --theme <theme> Select color theme (auto|dark|light)
3535
\\ --vanilla Disable numeric formatting
36-
\\ --width <int> Set max table width in chars
36+
\\ --width <width> Set table width, or try (min|max)
3737
\\
3838
\\ --completion <shell> Print shell completion (bash|zsh)
3939
\\ --help Get help
@@ -59,7 +59,7 @@ pub const Args = struct {
5959
\\ --tail <INT>
6060
\\ --theme <THEME>
6161
\\ --vanilla
62-
\\ --width <INT>
62+
\\ --width <WIDTH>
6363
\\-d, --delimiter <CHAR>
6464
\\-h, --help
6565
\\-n, --row-numbers
@@ -80,6 +80,7 @@ pub const Args = struct {
8080
.SHELL = clap.parsers.enumeration(types.CompletionShell),
8181
.STRING = clap.parsers.string,
8282
.THEME = clap.parsers.enumeration(types.Theme),
83+
.WIDTH = parseWidth,
8384
};
8485

8586
// Parse a delimiter argument into one ASCII byte.
@@ -90,6 +91,15 @@ pub const Args = struct {
9091
return error.InvalidArgument;
9192
}
9293

94+
// Parse one width mode argument.
95+
fn parseWidth(input: []const u8) error{InvalidArgument}!types.Width {
96+
if (std.mem.eql(u8, input, "min")) return .min;
97+
if (std.mem.eql(u8, input, "max")) return .max;
98+
const value = std.fmt.parseInt(usize, input, 10) catch return error.InvalidArgument;
99+
if (value == 0) return .auto;
100+
return .{ .chars = value };
101+
}
102+
93103
// Parse argv into one top-level main event.
94104
pub fn init(alloc: std.mem.Allocator, argv: []const []const u8) !MainEvent {
95105
var diagnostics: clap.Diagnostic = .{};
@@ -262,7 +272,7 @@ test "parse option config case" {
262272
try testing.expectEqual(types.Theme.light, out.run.theme);
263273
try testing.expectEqualStrings("foo", out.run.title);
264274
try testing.expect(out.run.vanilla);
265-
try testing.expectEqual(80, out.run.width);
275+
try testing.expectEqual(types.Width{ .chars = 80 }, out.run.width);
266276
try testing.expect(out.run.row_numbers);
267277
try testing.expectEqualStrings("-", out.run.filename.?);
268278
}
@@ -288,6 +298,9 @@ test "parse option event cases" {
288298
.{ .argv = &.{ "--shuf", "-" } },
289299
.{ .argv = &.{ "--sort", "score,name", "-" } },
290300
.{ .argv = &.{ "--table", "players", "-" } },
301+
.{ .argv = &.{ "--width", "min", "-" } },
302+
.{ .argv = &.{ "--width", "max", "-" } },
303+
.{ .argv = &.{ "--width", "80", "-" } },
291304
.{ .argv = &.{ "--completion", "zsh" }, .event = .{ .completion = .zsh } },
292305
.{ .argv = &.{"--help"}, .event = .help },
293306
.{ .argv = &.{"--version"}, .event = .version },
@@ -306,6 +319,9 @@ test "parse option event cases" {
306319
try testing.expect(parsed.run.shuffle);
307320
}
308321
if (std.mem.eql(u8, tc.argv[0], "--table")) try testing.expectEqualStrings("players", parsed.run.table);
322+
if (std.mem.eql(u8, tc.argv[0], "--width") and std.mem.eql(u8, tc.argv[1], "min")) try testing.expectEqual(types.Width.min, parsed.run.width);
323+
if (std.mem.eql(u8, tc.argv[0], "--width") and std.mem.eql(u8, tc.argv[1], "max")) try testing.expectEqual(types.Width.max, parsed.run.width);
324+
if (std.mem.eql(u8, tc.argv[0], "--width") and std.mem.eql(u8, tc.argv[1], "80")) try testing.expectEqual(types.Width{ .chars = 80 }, parsed.run.width);
309325
if (std.mem.eql(u8, tc.argv[0], "--zebra")) try testing.expect(parsed.run.zebra);
310326
if (tc.event) |event| try testing.expectEqual(event, parsed);
311327
}
@@ -344,6 +360,7 @@ test "parse reject cases" {
344360
.{ .argv = &.{ "--head", "1", "--tail", "1" }, .err = error.InvalidHeadTail },
345361
.{ .argv = &.{ "--head", "0" }, .err = error.InvalidHeadValue },
346362
.{ .argv = &.{ "--tail", "0" }, .err = error.InvalidTailValue },
363+
.{ .argv = &.{ "--width", "bogus" }, .err = error.InvalidArgument },
347364
};
348365

349366
for (cases) |tc| try expectParseError(tc.err, tc.argv);

src/completion.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ fn valuesFor(shell: types.CompletionShell, long: []const u8) ?[]const u8 {
188188
}
189189
if (std.mem.eql(u8, long, "--digits")) return "1 2 3 4 5 6";
190190
if (std.mem.eql(u8, long, "--theme")) return enumValues(types.Theme);
191+
if (std.mem.eql(u8, long, "--width")) return "min max";
191192
return null;
192193
}
193194

0 commit comments

Comments
 (0)