Skip to content

Commit 7db21ad

Browse files
committed
feat(tui): add comprehensive terminal UI property support
Add extensive TUI styling, layout, and positioning capabilities using iocraft native bindings. **TypeScript Interface Enhancements:** - Text styling: TextWeight, TextAlign, TextWrap types with JSDoc - Layout: DisplayType, PositionType, OverflowType types - Border: BorderEdges interface for selective rendering - BoxProps: 17 new properties (display, position, inset, size constraints, alignContent, overflow, borderEdges, rowGap, columnGap) - TextProps: align, wrap properties for text layout control - Comprehensive JSDoc documentation with usage examples **Property Mappings:** - Text(): align, weight, wrap, dimColor, strikethrough - Box(): all 17 new layout properties with snake_case conversion - Overflow shorthand handling (sets both X and Y) - BorderEdges object mapping to border_edges **Test Coverage (54 new tests):** - Text properties: weight, dimColor, strikethrough, align, wrap (11 tests) - Flex layout: flexBasis, flexWrap, alignContent, rowGap, columnGap (8 tests) - Overflow: overflow, overflowX, overflowY with shorthand (6 tests) - Positioning: display, position, top/right/bottom/left, inset (10 tests) - Dimensions: minWidth, maxWidth, minHeight, maxHeight (6 tests) - Border edges: selective rendering configurations (3 tests) - Complex scenarios: nested layouts, absolute positioning, styled text (5 tests) - Integration: renderToString validation (5 tests) All 60 tests passing (54 new + 6 existing renderer tests). **Documentation:** - CHANGELOG updated with all new features - JSDoc examples for every property type - Property-level comments on all interfaces **Binary Updates:** - Templates updated with new type definitions - Loader enhanced with pnpm virtual store compatibility Enables sophisticated terminal interfaces with professional styling, responsive layouts, absolute positioning, dimension constraints, and fine-grained overflow control.
1 parent d0e103a commit 7db21ad

23 files changed

+1373
-353
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Comprehensive TUI styling and layout properties for terminal interfaces:
12+
- Text styling: weight (normal, bold, light), dimColor for faded appearance, strikethrough decoration
13+
- Text layout: align (left, center, right), wrap (wrap, nowrap) for content control
14+
- Flex layout: flexBasis for initial sizing, flexWrap for multi-line layouts, alignContent for line distribution
15+
- Advanced positioning: display (flex, none), position (relative, absolute) with inset controls (top, right, bottom, left)
16+
- Dimension constraints: minWidth, maxWidth, minHeight, maxHeight for responsive layouts
17+
- Overflow control: overflow, overflowX, overflowY for content that exceeds container bounds
18+
- Border customization: borderEdges for selective border rendering (top, right, bottom, left)
19+
- Layout spacing: rowGap and columnGap for fine-grained flex item spacing
20+
921
### Changed
1022

1123
- Updated to @socketsecurity/socket-patch@1.2.0.

packages/cli/src/commands/analytics/AnalyticsRenderer.mts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,46 @@
55
* This is a proof-of-concept implementation for the hybrid Ink/iocraft approach.
66
*/
77

8+
import { getDefaultLogger } from '@socketsecurity/lib/logger'
9+
810
import { Box, Text, print } from '../../utils/terminal/iocraft.mts'
911

1012
import type { FormattedData } from './output-analytics.mts'
1113

14+
const logger = getDefaultLogger()
15+
1216
/**
1317
* Render analytics data using iocraft.
1418
*
1519
* Non-interactive version - displays data and exits immediately.
16-
* Press 'q' to quit will be added when InteractiveRenderer is implemented.
1720
*/
18-
export async function displayAnalyticsWithIocraft(
19-
data: FormattedData,
20-
): Promise<void> {
21-
const tree = Box({
21+
export function displayAnalyticsWithIocraft(data: FormattedData): void {
22+
try {
23+
// Check if data is empty
24+
const hasTopAlerts = Object.keys(data.top_five_alert_types).length > 0
25+
const hasCriticalAlerts = Object.keys(data.total_critical_alerts).length > 0
26+
const hasHighAlerts = Object.keys(data.total_high_alerts).length > 0
27+
28+
if (!hasTopAlerts && !hasCriticalAlerts && !hasHighAlerts) {
29+
const emptyTree = Box({
30+
children: [
31+
Text({
32+
children: 'No analytics data available for this period.',
33+
color: 'yellow',
34+
}),
35+
],
36+
})
37+
print(emptyTree)
38+
return
39+
}
40+
41+
const tree = Box({
2242
children: [
2343
Box({
2444
children: [
2545
Text({
2646
bold: true,
27-
children: '📊 Socket Analytics',
47+
children: 'Socket Analytics',
2848
color: 'cyan',
2949
}),
3050
],
@@ -38,7 +58,7 @@ export async function displayAnalyticsWithIocraft(
3858
children: [Text({ bold: true, children: 'Top 5 Alert Types:' })],
3959
marginBottom: 1,
4060
}),
41-
...Object.entries(data.top_five_alert_types).map(([type, count]) =>
61+
...Object.entries(data.top_five_alert_types).map(({ 0: type, 1: count }) =>
4262
Box({
4363
children: [Text({ children: ` ${type}: ${count}` })],
4464
}),
@@ -59,7 +79,7 @@ export async function displayAnalyticsWithIocraft(
5979
],
6080
marginBottom: 1,
6181
}),
62-
...Object.entries(data.total_critical_alerts).map(([date, count]) =>
82+
...Object.entries(data.total_critical_alerts).map(({ 0: date, 1: count }) =>
6383
Box({
6484
children: [Text({ children: ` ${date}: ${count}` })],
6585
}),
@@ -80,7 +100,7 @@ export async function displayAnalyticsWithIocraft(
80100
],
81101
marginBottom: 1,
82102
}),
83-
...Object.entries(data.total_high_alerts).map(([date, count]) =>
103+
...Object.entries(data.total_high_alerts).map(({ 0: date, 1: count }) =>
84104
Box({
85105
children: [Text({ children: ` ${date}: ${count}` })],
86106
}),
@@ -95,5 +115,13 @@ export async function displayAnalyticsWithIocraft(
95115
flexDirection: 'column',
96116
})
97117

98-
print(tree)
118+
print(tree)
119+
} catch (e) {
120+
process.exitCode = 1
121+
logger.error('Error rendering analytics:', e instanceof Error ? e.message : String(e))
122+
logger.warn('Falling back to plain text output')
123+
logger.log(`Top 5 Alert Types: ${Object.keys(data.top_five_alert_types).length} types`)
124+
logger.log(`Critical Alerts: ${Object.keys(data.total_critical_alerts).length} dates`)
125+
logger.log(`High Alerts: ${Object.keys(data.total_high_alerts).length} dates`)
126+
}
99127
}

packages/cli/src/commands/analytics/output-analytics.mts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { failMsgWithBadge } from '../../utils/error/fail-msg-with-badge.mts'
77
import { mdTableStringNumber } from '../../utils/output/markdown.mts'
88
import { serializeResultJson } from '../../utils/output/result-json.mjs'
99
import { fileLink } from '../../utils/terminal/link.mts'
10+
import { displayAnalyticsWithIocraft } from './AnalyticsRenderer.mts'
1011

1112
import type { CResult, OutputKind } from '../../types.mts'
1213
import type { SocketSdkSuccessResult } from '@socketsecurity/sdk'
14+
1315
const logger = getDefaultLogger()
1416

1517
const METRICS = [
@@ -118,10 +120,7 @@ export async function outputAnalytics(
118120
}
119121
} else {
120122
// Use iocraft for TUI rendering.
121-
const { displayAnalyticsWithIocraft } = await import(
122-
'./AnalyticsRenderer.mts'
123-
)
124-
await displayAnalyticsWithIocraft(fdata)
123+
displayAnalyticsWithIocraft(fdata)
125124
}
126125
}
127126

packages/cli/src/commands/analytics/test-analytics-renderer.mts

Lines changed: 0 additions & 50 deletions
This file was deleted.

packages/cli/src/commands/audit-log/AuditLogRenderer.mts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
* Non-interactive renderer for audit log data using iocraft native bindings.
55
*/
66

7+
import { getDefaultLogger } from '@socketsecurity/lib/logger'
8+
79
import { Box, Text, print } from '../../utils/terminal/iocraft.mts'
810

11+
const logger = getDefaultLogger()
12+
913
export interface AuditLogEntry {
1014
created_at: string
1115
event_id: string
@@ -43,20 +47,23 @@ export function displayAuditLogWithIocraft({
4347
orgSlug,
4448
results,
4549
}: AuditLogRendererProps): void {
46-
if (!results.length) {
47-
const tree = Box({
48-
children: [
49-
Text({
50-
children: 'No audit log entries found.',
51-
color: 'yellow',
52-
}),
53-
],
54-
})
55-
print(tree)
56-
return
57-
}
50+
try {
51+
if (!results.length) {
52+
const tree = Box({
53+
children: [
54+
Text({
55+
children: 'No audit log entries found.',
56+
color: 'yellow',
57+
}),
58+
],
59+
})
60+
print(tree)
61+
return
62+
}
5863

59-
const tree = Box({
64+
const firstEntry = results[0]!
65+
66+
const tree = Box({
6067
children: [
6168
Box({
6269
children: [
@@ -91,10 +98,10 @@ export function displayAuditLogWithIocraft({
9198
children: [
9299
Text({
93100
children: [
94-
entry.event_id.slice(0, 18).padEnd(20),
95-
entry.formatted_created_at.padEnd(25),
96-
entry.type.padEnd(30),
97-
entry.user_email.padEnd(30),
101+
(entry.event_id || '').slice(0, 18).padEnd(20),
102+
(entry.formatted_created_at || '').padEnd(25),
103+
(entry.type || '').padEnd(30),
104+
(entry.user_email || '').padEnd(30),
98105
].join(' '),
99106
}),
100107
],
@@ -123,7 +130,7 @@ export function displayAuditLogWithIocraft({
123130
Box({
124131
children: [
125132
Text({
126-
children: formatEntry(results[0]!),
133+
children: formatEntry(firstEntry),
127134
}),
128135
],
129136
}),
@@ -136,5 +143,18 @@ export function displayAuditLogWithIocraft({
136143
flexDirection: 'column',
137144
})
138145

139-
print(tree)
146+
print(tree)
147+
} catch (e) {
148+
process.exitCode = 1
149+
logger.error('Error rendering audit log:', e instanceof Error ? e.message : String(e))
150+
logger.warn('Falling back to plain text output')
151+
logger.log(`Organization: ${orgSlug}`)
152+
logger.log(`Entries: ${results.length}`)
153+
results.slice(0, 10).forEach((entry, i) => {
154+
logger.log(`[${i + 1}] ${entry.event_id || 'N/A'} - ${entry.type || 'N/A'} - ${entry.formatted_created_at || 'N/A'}`)
155+
})
156+
if (results.length > 10) {
157+
logger.log(`... and ${results.length - 10} more entries`)
158+
}
159+
}
140160
}

packages/cli/src/commands/audit-log/output-audit-log.mts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { VITEST } from '../../env/vitest.mts'
1111
import { failMsgWithBadge } from '../../utils/error/fail-msg-with-badge.mts'
1212
import { mdTable } from '../../utils/output/markdown.mts'
1313
import { serializeResultJson } from '../../utils/output/result-json.mjs'
14+
import { displayAuditLogWithIocraft } from './AuditLogRenderer.mts'
1415

1516
import type { CResult, OutputKind } from '../../types.mts'
1617
import type { SocketSdkSuccessResult } from '@socketsecurity/sdk'
@@ -67,7 +68,7 @@ export async function outputAuditLog(
6768
return
6869
}
6970

70-
await outputWithIocraft(result.data, orgSlug)
71+
outputWithIocraft(result.data, orgSlug)
7172
}
7273

7374
export async function outputAsJson(
@@ -172,14 +173,10 @@ ${table}
172173
/**
173174
* Display audit log using iocraft.
174175
*/
175-
async function outputWithIocraft(
176+
function outputWithIocraft(
176177
data: SocketSdkSuccessResult<'getAuditLogEvents'>['data'],
177178
orgSlug: string,
178-
): Promise<void> {
179-
const { displayAuditLogWithIocraft } = await import(
180-
'./AuditLogRenderer.mts'
181-
)
182-
179+
): void {
183180
displayAuditLogWithIocraft({
184181
orgSlug,
185182
results: data.results.map((entry: AuditLogEvent) => ({

packages/cli/src/commands/audit-log/test-audit-log-renderer.mts

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)