usecomputer 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ /// Aligned table renderer for CLI output.
2
+ /// Renders rows of string columns with padding to align them visually.
3
+ const std = @import("std");
4
+
5
+ pub const Alignment = enum { left, right };
6
+
7
+ pub const Column = struct {
8
+ header: []const u8,
9
+ alignment: Alignment = .left,
10
+ };
11
+
12
+ /// Render rows into aligned, space-separated lines.
13
+ /// Each row is a slice of cell strings matching the columns in order.
14
+ /// Returns a slice of lines allocated with the given allocator.
15
+ pub fn render(
16
+ allocator: std.mem.Allocator,
17
+ columns: []const Column,
18
+ rows: []const []const []const u8,
19
+ ) ![]const []const u8 {
20
+ if (columns.len == 0 or rows.len == 0) return &.{};
21
+
22
+ // Compute max width per column (header vs data)
23
+ const widths = try allocator.alloc(usize, columns.len);
24
+ defer allocator.free(widths);
25
+
26
+ for (columns, 0..) |col, i| {
27
+ widths[i] = col.header.len;
28
+ }
29
+ for (rows) |row| {
30
+ for (row, 0..) |cell, i| {
31
+ if (i < widths.len and cell.len > widths[i]) {
32
+ widths[i] = cell.len;
33
+ }
34
+ }
35
+ }
36
+
37
+ var lines = std.ArrayListUnmanaged([]const u8).empty;
38
+ errdefer {
39
+ for (lines.items) |line| allocator.free(line);
40
+ lines.deinit(allocator);
41
+ }
42
+
43
+ // Header line
44
+ const header_line = try formatRow(allocator, columns, &.{}, widths, true);
45
+ try lines.append(allocator, header_line);
46
+
47
+ // Data lines
48
+ for (rows) |row| {
49
+ const line = try formatRow(allocator, columns, row, widths, false);
50
+ try lines.append(allocator, line);
51
+ }
52
+
53
+ return lines.toOwnedSlice(allocator);
54
+ }
55
+
56
+ fn formatRow(
57
+ allocator: std.mem.Allocator,
58
+ columns: []const Column,
59
+ cells: []const []const u8,
60
+ widths: []const usize,
61
+ is_header: bool,
62
+ ) ![]const u8 {
63
+ var buf = std.ArrayListUnmanaged(u8).empty;
64
+ errdefer buf.deinit(allocator);
65
+
66
+ for (columns, 0..) |col, i| {
67
+ if (i > 0) try buf.appendSlice(allocator, " ");
68
+
69
+ const text = if (is_header) col.header else if (i < cells.len) cells[i] else "";
70
+ const w = widths[i];
71
+ const pad = if (w > text.len) w - text.len else 0;
72
+
73
+ if (col.alignment == .right and !is_header) {
74
+ try buf.appendNTimes(allocator, ' ', pad);
75
+ try buf.appendSlice(allocator, text);
76
+ } else {
77
+ try buf.appendSlice(allocator, text);
78
+ // Don't pad the last column
79
+ if (i < columns.len - 1) {
80
+ try buf.appendNTimes(allocator, ' ', pad);
81
+ }
82
+ }
83
+ }
84
+
85
+ return buf.toOwnedSlice(allocator);
86
+ }
87
+
88
+ // ─── Tests ───
89
+
90
+ test "renders aligned columns with headers" {
91
+ const allocator = std.testing.allocator;
92
+ const columns = &[_]Column{
93
+ .{ .header = "desktop" },
94
+ .{ .header = "primary" },
95
+ .{ .header = "size", .alignment = .right },
96
+ .{ .header = "name" },
97
+ };
98
+ const rows = &[_][]const []const u8{
99
+ &.{ "#0", "yes", "1720x1440", "Display 1" },
100
+ &.{ "#1", "no", "800x600", "External" },
101
+ };
102
+
103
+ const lines = try render(allocator, columns, rows);
104
+ defer {
105
+ for (lines) |line| allocator.free(line);
106
+ allocator.free(lines);
107
+ }
108
+
109
+ try std.testing.expectEqual(3, lines.len);
110
+ try std.testing.expectEqualStrings("desktop primary size name", lines[0]);
111
+ try std.testing.expectEqualStrings("#0 yes 1720x1440 Display 1", lines[1]);
112
+ try std.testing.expectEqualStrings("#1 no 800x600 External", lines[2]);
113
+ }
114
+
115
+ test "renders right-aligned numeric columns" {
116
+ const allocator = std.testing.allocator;
117
+ const columns = &[_]Column{
118
+ .{ .header = "id", .alignment = .right },
119
+ .{ .header = "app" },
120
+ .{ .header = "size", .alignment = .right },
121
+ .{ .header = "title" },
122
+ };
123
+ const rows = &[_][]const []const u8{
124
+ &.{ "42", "Zed", "1720x1440", "main.zig" },
125
+ &.{ "1337", "Safari", "800x600", "Google" },
126
+ };
127
+
128
+ const lines = try render(allocator, columns, rows);
129
+ defer {
130
+ for (lines) |line| allocator.free(line);
131
+ allocator.free(lines);
132
+ }
133
+
134
+ try std.testing.expectEqual(3, lines.len);
135
+ try std.testing.expectEqualStrings("id app size title", lines[0]);
136
+ try std.testing.expectEqualStrings(" 42 Zed 1720x1440 main.zig", lines[1]);
137
+ try std.testing.expectEqualStrings("1337 Safari 800x600 Google", lines[2]);
138
+ }
139
+
140
+ test "empty rows returns empty" {
141
+ const allocator = std.testing.allocator;
142
+ const columns = &[_]Column{
143
+ .{ .header = "a" },
144
+ };
145
+ const empty: []const []const []const u8 = &.{};
146
+ const lines = try render(allocator, columns, empty);
147
+ try std.testing.expectEqual(0, lines.len);
148
+ }
149
+
150
+ test "single column no trailing spaces" {
151
+ const allocator = std.testing.allocator;
152
+ const columns = &[_]Column{
153
+ .{ .header = "name" },
154
+ };
155
+ const rows = &[_][]const []const u8{
156
+ &.{"hello"},
157
+ &.{"hi"},
158
+ };
159
+
160
+ const lines = try render(allocator, columns, rows);
161
+ defer {
162
+ for (lines) |line| allocator.free(line);
163
+ allocator.free(lines);
164
+ }
165
+
166
+ try std.testing.expectEqual(3, lines.len);
167
+ try std.testing.expectEqualStrings("name", lines[0]);
168
+ try std.testing.expectEqualStrings("hello", lines[1]);
169
+ try std.testing.expectEqualStrings("hi", lines[2]);
170
+ }
package/bin.js DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCli } from './dist/cli.js'
3
-
4
- runCli()
Binary file
@@ -1,61 +0,0 @@
1
- // Parser tests for goke CLI options and flags.
2
-
3
- import { describe, expect, test } from 'vitest'
4
- import { createCli } from './cli.js'
5
-
6
- describe('usecomputer cli parsing', () => {
7
- test('parses click options with typed defaults', () => {
8
- const cli = createCli()
9
- const parsed = cli.parse(['node', 'usecomputer', 'click', '100,200', '--count', '2'], { run: false })
10
- expect(parsed.args[0]).toBe('100,200')
11
- expect(parsed.options.count).toBe(2)
12
- expect(parsed.options.button).toBe('left')
13
- })
14
-
15
- test('parses screenshot options', () => {
16
- const cli = createCli()
17
- const parsed = cli.parse(['node', 'usecomputer', 'screenshot', './shot.png', '--display', '2', '--region', '0,0,120,80'], {
18
- run: false,
19
- })
20
- expect(parsed.args[0]).toBe('./shot.png')
21
- expect(parsed.options.display).toBe(2)
22
- expect(parsed.options.region).toBe('0,0,120,80')
23
- })
24
-
25
- test('parses coord-map option for click and mouse move', () => {
26
- const clickCli = createCli()
27
- const clickParsed = clickCli.parse(['node', 'usecomputer', 'click', '-x', '100', '-y', '200', '--coord-map', '0,0,1600,900,1568,882'], {
28
- run: false,
29
- })
30
-
31
- const moveCli = createCli()
32
- const moveParsed = moveCli.parse(['node', 'usecomputer', 'mouse', 'move', '-x', '100', '-y', '200', '--coord-map', '0,0,1600,900,1568,882'], {
33
- run: false,
34
- })
35
-
36
- expect(clickParsed.options.coordMap).toBe('0,0,1600,900,1568,882')
37
- expect(moveParsed.options.coordMap).toBe('0,0,1600,900,1568,882')
38
- })
39
-
40
- test('parses debug-point options', () => {
41
- const cli = createCli()
42
- const parsed = cli.parse([
43
- 'node',
44
- 'usecomputer',
45
- 'debug-point',
46
- '-x',
47
- '210',
48
- '-y',
49
- '560',
50
- '--coord-map',
51
- '0,0,1720,1440,1568,1313',
52
- '--output',
53
- './tmp/debug-point.png',
54
- ], { run: false })
55
-
56
- expect(parsed.options.coordMap).toBe('0,0,1720,1440,1568,1313')
57
- expect(parsed.options.output).toBe('./tmp/debug-point.png')
58
- expect(parsed.options.x).toBe(210)
59
- expect(parsed.options.y).toBe(560)
60
- })
61
- })