@skilly-hand/skilly-hand 0.18.0 → 0.20.0

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.
@@ -24,6 +24,19 @@ function normalizeBooleanEnv(value) {
24
24
  return true;
25
25
  }
26
26
 
27
+ function detectViewportWidth({ stdout = process.stdout, env = process.env } = {}) {
28
+ if (stdout?.isTTY && Number.isInteger(stdout.columns) && stdout.columns > 0) {
29
+ return stdout.columns;
30
+ }
31
+
32
+ const envColumns = Number.parseInt(String(env?.COLUMNS ?? ""), 10);
33
+ if (Number.isInteger(envColumns) && envColumns > 0) {
34
+ return envColumns;
35
+ }
36
+
37
+ return 80;
38
+ }
39
+
27
40
  // Kept as exported API (tests import these directly)
28
41
  export function detectColorSupport({ env = process.env, stream = process.stdout } = {}) {
29
42
  const noColor = normalizeBooleanEnv(env.NO_COLOR);
@@ -78,6 +91,7 @@ export function createTerminalRenderer({
78
91
  const colorLevel = detectColorLevel({ env, stream: stdout });
79
92
  const colorEnabled = colorLevel > 0;
80
93
  const unicodeEnabled = detectUnicodeSupport({ env, stream: stdout, platform });
94
+ const viewportWidth = detectViewportWidth({ stdout, env });
81
95
 
82
96
  const theme = createTheme(colorLevel);
83
97
  const layout = createLayout(theme, unicodeEnabled);
@@ -109,6 +123,7 @@ export function createTerminalRenderer({
109
123
  colorEnabled,
110
124
  colorLevel,
111
125
  unicodeEnabled,
126
+ viewportWidth,
112
127
  symbols,
113
128
  style,
114
129
  theme,
@@ -134,11 +149,7 @@ export function createTerminalRenderer({
134
149
  },
135
150
 
136
151
  table(columns, rows) {
137
- // Use bordered table when unicode is on; fall back to plain
138
- if (unicodeEnabled) {
139
- return layout.borderedTable(columns, rows);
140
- }
141
- return renderPlainTable(columns, rows);
152
+ return layout.borderedTable(columns, rows, { viewportWidth });
142
153
  },
143
154
 
144
155
  status(level, message, detail = "") {
@@ -30,6 +30,68 @@ function truncate(value, maxWidth) {
30
30
  return clean.slice(0, maxWidth - 1) + "…";
31
31
  }
32
32
 
33
+ function summarizeDenseValue(value) {
34
+ const raw = String(value);
35
+ const parts = raw.split(",").map((part) => part.trim()).filter(Boolean);
36
+ if (parts.length < 4) return { display: raw, detail: null };
37
+ return {
38
+ display: `${parts.slice(0, 3).join(", ")} +${parts.length - 3} more`,
39
+ detail: raw
40
+ };
41
+ }
42
+
43
+ function fitWidths(preferred, minimums, target) {
44
+ const widths = [...preferred];
45
+ let current = widths.reduce((sum, width) => sum + width, 0);
46
+ if (current <= target) return widths;
47
+
48
+ while (current > target) {
49
+ let idx = -1;
50
+ let widest = 0;
51
+
52
+ for (let i = 0; i < widths.length; i += 1) {
53
+ if (widths[i] > minimums[i] && widths[i] > widest) {
54
+ widest = widths[i];
55
+ idx = i;
56
+ }
57
+ }
58
+
59
+ if (idx === -1) return null;
60
+ widths[idx] -= 1;
61
+ current -= 1;
62
+ }
63
+
64
+ return widths;
65
+ }
66
+
67
+ function nextWrapChunk(text, width) {
68
+ if (text.length <= width) return [text, ""];
69
+ const split = text.lastIndexOf(" ", width);
70
+ if (split <= 0) return [text.slice(0, width), text.slice(width)];
71
+ return [text.slice(0, split), text.slice(split + 1)];
72
+ }
73
+
74
+ function wrapPrefixedText(text, maxWidth, firstPrefix, continuationPrefix) {
75
+ const lines = [];
76
+ let remaining = String(text).trim();
77
+ let first = true;
78
+
79
+ while (remaining.length > 0) {
80
+ const prefix = first ? firstPrefix : continuationPrefix;
81
+ const available = Math.max(8, maxWidth - prefix.length);
82
+ const [chunk, rest] = nextWrapChunk(remaining, available);
83
+ lines.push(prefix + chunk);
84
+ remaining = rest.trimStart();
85
+ first = false;
86
+ }
87
+
88
+ if (lines.length === 0) {
89
+ return [firstPrefix.trimEnd()];
90
+ }
91
+
92
+ return lines;
93
+ }
94
+
33
95
  // Box character sets
34
96
  const BOX = {
35
97
  // double-line outer (for banner)
@@ -140,27 +202,29 @@ export function createLayout(theme, unicodeEnabled) {
140
202
  function borderedTable(columns, rows, opts = {}) {
141
203
  if (!columns || columns.length === 0) return "";
142
204
  const maxColW = opts.maxColWidth || 36;
143
-
205
+ const viewportWidth = Math.max(40, Number(opts.viewportWidth) || 80);
144
206
  const headers = columns.map((c) => String(c.header));
207
+ const detailMarker = unicodeEnabled ? "↳ " : "-> ";
208
+ const minWidths = headers.map((header) => Math.max(6, Math.min(visLen(header), 14)));
209
+
145
210
  const matrix = rows.map((row) =>
146
- columns.map((c) => truncate(String(row[c.key] ?? ""), maxColW))
211
+ columns.map((c) => {
212
+ const summarized = summarizeDenseValue(String(row[c.key] ?? ""));
213
+ return {
214
+ display: summarized.display,
215
+ detail: summarized.detail
216
+ };
217
+ })
147
218
  );
148
219
 
149
- // Compute column widths
150
- const widths = headers.map((h, i) => {
151
- const contentMax = matrix.reduce((m, row) => Math.max(m, visLen(row[i])), 0);
152
- return Math.max(visLen(h), contentMax, 3);
220
+ const preferredWidths = headers.map((h, i) => {
221
+ const contentMax = matrix.reduce((max, row) => Math.max(max, visLen(row[i].display)), 0);
222
+ return Math.min(Math.max(visLen(h), contentMax, 3), maxColW);
153
223
  });
154
224
 
155
- if (!unicodeEnabled) {
156
- // Plain fallback original table style
157
- const headerLine = headers.map((h, i) => padEnd(h, widths[i])).join(" ");
158
- const sepLine = widths.map((w) => "-".repeat(w)).join(" ");
159
- const bodyLines = matrix.map((row) =>
160
- row.map((cell, i) => padEnd(cell, widths[i])).join(" ")
161
- );
162
- return [headerLine, sepLine, ...bodyLines].join("\n");
163
- }
225
+ const borderedOverhead = (3 * headers.length) + 1;
226
+ const compactOverhead = 3 * Math.max(headers.length - 1, 0);
227
+ const preferredSum = preferredWidths.reduce((sum, width) => sum + width, 0);
164
228
 
165
229
  const s = box.s;
166
230
 
@@ -174,33 +238,120 @@ export function createLayout(theme, unicodeEnabled) {
174
238
  return leftC + inner + rightC;
175
239
  }
176
240
 
177
- const topBorder = makeDivider(widths, s.tl, s.mt, s.tr, s.h);
178
- const headerRow = makeRow(headers.map((h, i) => theme.bold(h)), widths, theme.primary(s.v), theme.primary(s.v), theme.primary(s.v));
179
- const headerSep = makeDivider(widths, s.dl, s.dc, s.dr, s.dh);
180
- const bodyRows = matrix.map((row) =>
181
- makeRow(
182
- row.map((cell, i) => {
183
- // color first column (usually ID) with accent
184
- return i === 0 ? theme.accent(cell) : cell;
185
- }),
186
- widths, theme.primary(s.v), theme.primary(s.v), theme.primary(s.v)
187
- )
188
- );
189
- const bottomBorder = makeDivider(widths, s.bl, s.mb, s.br, s.h);
190
-
191
- // Apply primary color to border chars
192
- const colorBorder = (line) => {
193
- // Replace non-letter/space chars with colored versions
194
- return theme.primary(line);
195
- };
196
-
197
- return [
198
- colorBorder(topBorder),
199
- headerRow,
200
- colorBorder(headerSep),
201
- ...bodyRows,
202
- colorBorder(bottomBorder),
203
- ].join("\n");
241
+ function buildCompactTable(widths) {
242
+ const compactRows = [];
243
+ const formattedHeaders = headers.map((header, idx) => truncate(header, widths[idx]));
244
+ const headerLine = formattedHeaders.map((header, idx) => padEnd(header, widths[idx])).join(" | ");
245
+ const sepLine = widths.map((width) => "-".repeat(width)).join("-+-");
246
+
247
+ compactRows.push(headerLine);
248
+ compactRows.push(sepLine);
249
+
250
+ for (let rowIdx = 0; rowIdx < matrix.length; rowIdx += 1) {
251
+ const row = matrix[rowIdx];
252
+ const cells = row.map((cell, colIdx) => truncate(cell.display, widths[colIdx]));
253
+ compactRows.push(cells.map((cell, colIdx) => padEnd(cell, widths[colIdx])).join(" | "));
254
+
255
+ const details = [];
256
+ for (let colIdx = 0; colIdx < row.length; colIdx += 1) {
257
+ if (!row[colIdx].detail) continue;
258
+ details.push(...wrapPrefixedText(
259
+ `${headers[colIdx]}: ${row[colIdx].detail}`,
260
+ viewportWidth,
261
+ `${detailMarker}`,
262
+ " "
263
+ ));
264
+ }
265
+ compactRows.push(...details.map((line) => theme.muted(line)));
266
+ }
267
+
268
+ return compactRows.join("\n");
269
+ }
270
+
271
+ function buildCardList() {
272
+ const cardRows = [];
273
+ for (let rowIdx = 0; rowIdx < matrix.length; rowIdx += 1) {
274
+ const row = matrix[rowIdx];
275
+ for (let colIdx = 0; colIdx < row.length; colIdx += 1) {
276
+ const header = headers[colIdx];
277
+ const value = row[colIdx].display;
278
+ const isFirst = colIdx === 0;
279
+ const prefix = isFirst ? `${detailMarker}${header}: ` : ` ${header}: `;
280
+ const continuation = isFirst ? " " : " ";
281
+ const lineSet = wrapPrefixedText(value, viewportWidth, prefix, continuation);
282
+ if (isFirst) {
283
+ cardRows.push(...lineSet.map((line, idx) => idx === 0 ? theme.accent(line) : line));
284
+ } else {
285
+ cardRows.push(...lineSet);
286
+ }
287
+
288
+ if (row[colIdx].detail) {
289
+ cardRows.push(...wrapPrefixedText(
290
+ row[colIdx].detail,
291
+ viewportWidth,
292
+ ` ${header} (full): `,
293
+ " "
294
+ ).map((line) => theme.muted(line)));
295
+ }
296
+ }
297
+
298
+ if (rowIdx < matrix.length - 1) {
299
+ cardRows.push(theme.muted("-".repeat(Math.min(32, viewportWidth))));
300
+ }
301
+ }
302
+ return cardRows.join("\n");
303
+ }
304
+
305
+ const canUseBordered = unicodeEnabled && (preferredSum + borderedOverhead <= viewportWidth);
306
+ if (canUseBordered) {
307
+ const widths = preferredWidths;
308
+ const topBorder = makeDivider(widths, s.tl, s.mt, s.tr, s.h);
309
+ const headerRow = makeRow(headers.map((h) => theme.bold(h)), widths, theme.primary(s.v), theme.primary(s.v), theme.primary(s.v));
310
+ const headerSep = makeDivider(widths, s.dl, s.dc, s.dr, s.dh);
311
+ const tableWidth = visLen(topBorder);
312
+ const detailContentWidth = Math.max(8, tableWidth - 4);
313
+
314
+ const bodyRows = [];
315
+ for (const row of matrix) {
316
+ bodyRows.push(makeRow(
317
+ row.map((cell, i) => i === 0 ? theme.accent(truncate(cell.display, widths[i])) : truncate(cell.display, widths[i])),
318
+ widths,
319
+ theme.primary(s.v),
320
+ theme.primary(s.v),
321
+ theme.primary(s.v)
322
+ ));
323
+
324
+ for (let colIdx = 0; colIdx < row.length; colIdx += 1) {
325
+ if (!row[colIdx].detail) continue;
326
+ const wrapped = wrapPrefixedText(
327
+ `${headers[colIdx]}: ${row[colIdx].detail}`,
328
+ detailContentWidth,
329
+ detailMarker,
330
+ " "
331
+ );
332
+ for (const line of wrapped) {
333
+ bodyRows.push(
334
+ theme.primary(s.v) +
335
+ " " +
336
+ padEnd(theme.muted(line), detailContentWidth) +
337
+ " " +
338
+ theme.primary(s.v)
339
+ );
340
+ }
341
+ }
342
+ }
343
+
344
+ const bottomBorder = makeDivider(widths, s.bl, s.mb, s.br, s.h);
345
+ return [theme.primary(topBorder), headerRow, theme.primary(headerSep), ...bodyRows, theme.primary(bottomBorder)].join("\n");
346
+ }
347
+
348
+ const compactWidthTarget = viewportWidth - compactOverhead;
349
+ const fittedCompactWidths = fitWidths(preferredWidths, minWidths, compactWidthTarget);
350
+ if (fittedCompactWidths) {
351
+ return buildCompactTable(fittedCompactWidths);
352
+ }
353
+
354
+ return buildCardList();
204
355
  }
205
356
 
206
357
  // ── Detection grid ────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/detectors",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }