termcast 1.3.50 → 1.3.52
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.
- package/dist/apis/environment.d.ts +1 -0
- package/dist/apis/environment.d.ts.map +1 -1
- package/dist/apis/environment.js +5 -0
- package/dist/apis/environment.js.map +1 -1
- package/dist/app.d.ts +33 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +1130 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts +4 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +8 -5
- package/dist/components/actions.js.map +1 -1
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +21 -18
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +3 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts +6 -0
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +15 -6
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/checkbox.d.ts.map +1 -1
- package/dist/components/form/checkbox.js +1 -13
- package/dist/components/form/checkbox.js.map +1 -1
- package/dist/components/form/date-picker.js +2 -2
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/description.js +1 -1
- package/dist/components/form/description.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +19 -3
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +22 -4
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/index.d.ts +3 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +7 -5
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/password-field.js +3 -3
- package/dist/components/form/password-field.js.map +1 -1
- package/dist/components/form/text-area.d.ts.map +1 -1
- package/dist/components/form/text-area.js +29 -6
- package/dist/components/form/text-area.js.map +1 -1
- package/dist/components/form/text-field.js +3 -3
- package/dist/components/form/text-field.js.map +1 -1
- package/dist/components/graph.d.ts.map +1 -1
- package/dist/components/graph.js +21 -25
- package/dist/components/graph.js.map +1 -1
- package/dist/components/heatmap.d.ts +80 -0
- package/dist/components/heatmap.d.ts.map +1 -0
- package/dist/components/heatmap.js +424 -0
- package/dist/components/heatmap.js.map +1 -0
- package/dist/components/list.d.ts +2 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +91 -58
- package/dist/components/list.js.map +1 -1
- package/dist/components/markdown.d.ts +7 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +19 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/metadata.d.ts.map +1 -1
- package/dist/components/metadata.js +4 -1
- package/dist/components/metadata.js.map +1 -1
- package/dist/components/progress-bar.d.ts +37 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +34 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/table.d.ts +3 -2
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +78 -63
- package/dist/components/table.js.map +1 -1
- package/dist/diagram-parser.d.ts +17 -3
- package/dist/diagram-parser.d.ts.map +1 -1
- package/dist/diagram-parser.js +17 -3
- package/dist/diagram-parser.js.map +1 -1
- package/dist/examples/list-slot.d.ts +2 -0
- package/dist/examples/list-slot.d.ts.map +1 -0
- package/dist/examples/list-slot.js +14 -0
- package/dist/examples/list-slot.js.map +1 -0
- package/dist/examples/list-with-dropdown.js +2 -4
- package/dist/examples/list-with-dropdown.js.map +1 -1
- package/dist/examples/simple-heatmap.d.ts +2 -0
- package/dist/examples/simple-heatmap.d.ts.map +1 -0
- package/dist/examples/simple-heatmap.js +37 -0
- package/dist/examples/simple-heatmap.js.map +1 -0
- package/dist/examples/simple-progress-bar.d.ts +2 -0
- package/dist/examples/simple-progress-bar.d.ts.map +1 -0
- package/dist/examples/simple-progress-bar.js +36 -0
- package/dist/examples/simple-progress-bar.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +5 -4
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +7 -2
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +42 -4
- package/dist/internal/providers.js.map +1 -1
- package/dist/logger.js +6 -1
- package/dist/logger.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +31 -2
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +23 -1
- package/dist/theme.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/apis/environment.tsx +6 -0
- package/src/app.tsx +1492 -0
- package/src/assets/default-app-icon.png +0 -0
- package/src/cli.tsx +105 -0
- package/src/compile.tsx +5 -2
- package/src/components/actions.tsx +9 -6
- package/src/components/detail.tsx +33 -23
- package/src/components/dropdown.tsx +3 -2
- package/src/components/footer.tsx +40 -7
- package/src/components/form/checkbox.tsx +2 -17
- package/src/components/form/date-picker.tsx +2 -2
- package/src/components/form/description.tsx +1 -1
- package/src/components/form/dropdown.tsx +22 -3
- package/src/components/form/file-picker.tsx +33 -10
- package/src/components/form/index.tsx +11 -7
- package/src/components/form/password-field.tsx +3 -3
- package/src/components/form/text-area.tsx +31 -6
- package/src/components/form/text-field.tsx +3 -3
- package/src/components/graph.tsx +21 -24
- package/src/components/heatmap.tsx +602 -0
- package/src/components/list.tsx +147 -78
- package/src/components/markdown.tsx +30 -0
- package/src/components/metadata.tsx +9 -2
- package/src/components/progress-bar.tsx +112 -0
- package/src/components/table.tsx +88 -71
- package/src/diagram-parser.tsx +17 -3
- package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
- package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
- package/src/examples/form-basic.vitest.tsx +117 -16
- package/src/examples/graph-bar-chart.vitest.tsx +7 -7
- package/src/examples/graph-row.vitest.tsx +45 -45
- package/src/examples/graph-styles.vitest.tsx +19 -19
- package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
- package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
- package/src/examples/list-dropdown-default.vitest.tsx +78 -58
- package/src/examples/list-slot.tsx +38 -0
- package/src/examples/list-with-detail.vitest.tsx +8 -8
- package/src/examples/list-with-dropdown.tsx +2 -2
- package/src/examples/list-with-dropdown.vitest.tsx +16 -16
- package/src/examples/list-with-sections.vitest.tsx +45 -32
- package/src/examples/simple-detail-table.vitest.tsx +2 -2
- package/src/examples/simple-file-picker.vitest.tsx +1 -1
- package/src/examples/simple-grid.vitest.tsx +27 -53
- package/src/examples/simple-heatmap.tsx +63 -0
- package/src/examples/simple-heatmap.vitest.tsx +88 -0
- package/src/examples/simple-progress-bar.tsx +82 -0
- package/src/examples/simple-progress-bar.vitest.tsx +72 -0
- package/src/examples/table-edge-cases.vitest.tsx +1 -1
- package/src/index.tsx +19 -0
- package/src/internal/date-picker-widget.tsx +23 -12
- package/src/internal/navigation.tsx +7 -2
- package/src/internal/providers.tsx +48 -3
- package/src/logger.tsx +6 -1
- package/src/state.tsx +38 -2
- package/src/theme.tsx +26 -2
- package/src/utils.tsx +6 -1
|
@@ -522,30 +522,27 @@ test('grid mouse interaction', async () => {
|
|
|
522
522
|
Simple Grid Example ────────────────────────────────────────────
|
|
523
523
|
|
|
524
524
|
> Search items...
|
|
525
|
-
╭────────────────────────────────────────────────────────────────╮
|
|
526
|
-
│ │
|
|
527
|
-
│ Actions esc │
|
|
528
|
-
│ │
|
|
529
|
-
│ > Search actions... │
|
|
530
|
-
│ │
|
|
531
|
-
│ ›Show Details │
|
|
532
|
-
│ Copy Emoji ⌃C │
|
|
533
|
-
│ │
|
|
534
|
-
│ Settings │
|
|
535
|
-
│ Change Theme... │
|
|
536
|
-
│ Toggle Console Logs │
|
|
537
|
-
│ │
|
|
538
|
-
│ │
|
|
539
|
-
│ │
|
|
540
|
-
│ │
|
|
541
|
-
│ │
|
|
542
|
-
│ ↵ select ↑↓ navigate │
|
|
543
|
-
│ │
|
|
544
|
-
╰────────────────────────────────────────────────────────────────╯"
|
|
545
|
-
`)
|
|
546
525
|
|
|
547
|
-
|
|
548
|
-
|
|
526
|
+
|
|
527
|
+
Animals
|
|
528
|
+
🐕 Dog
|
|
529
|
+
🐱 Cat
|
|
530
|
+
🐰 Rabbit
|
|
531
|
+
|
|
532
|
+
Others
|
|
533
|
+
🏠 House
|
|
534
|
+
🚗 Car
|
|
535
|
+
🚀 Rocket
|
|
536
|
+
›⭐ Star
|
|
537
|
+
🌙 Moon
|
|
538
|
+
☀ Sun
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
↵ show details ↑↓ navigate ^k actions
|
|
543
|
+
|
|
544
|
+
"
|
|
545
|
+
`)
|
|
549
546
|
|
|
550
547
|
// Navigate back up to make Apple visible.
|
|
551
548
|
// Grid is implemented via List, which uses edge-triggered pagination.
|
|
@@ -562,36 +559,13 @@ test('grid mouse interaction', async () => {
|
|
|
562
559
|
timeout: 5000,
|
|
563
560
|
})
|
|
564
561
|
|
|
565
|
-
// Click on "Apple"
|
|
562
|
+
// Click on "Apple" - it's already selected (first item after scrolling up),
|
|
563
|
+
// so clicking executes the first action (Show Details) which logs to console.
|
|
564
|
+
// Verify no actions dialog opened (unlike old behavior).
|
|
566
565
|
await session.click('Apple', { first: true })
|
|
567
566
|
|
|
568
567
|
const afterClickAppleSnapshot = await session.text()
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
Simple Grid Example ────────────────────────────────────────────
|
|
574
|
-
|
|
575
|
-
> Search items...
|
|
576
|
-
╭────────────────────────────────────────────────────────────────╮
|
|
577
|
-
│ │
|
|
578
|
-
│ Actions esc │
|
|
579
|
-
│ │
|
|
580
|
-
│ > Search actions... │
|
|
581
|
-
│ │
|
|
582
|
-
│ ›Show Details │
|
|
583
|
-
│ Copy Emoji ⌃C │
|
|
584
|
-
│ │
|
|
585
|
-
│ Settings │
|
|
586
|
-
│ Change Theme... │
|
|
587
|
-
│ Toggle Console Logs │
|
|
588
|
-
│ │
|
|
589
|
-
│ │
|
|
590
|
-
│ │
|
|
591
|
-
│ │
|
|
592
|
-
│ │
|
|
593
|
-
│ ↵ select ↑↓ navigate │
|
|
594
|
-
│ │
|
|
595
|
-
╰────────────────────────────────────────────────────────────────╯"
|
|
596
|
-
`)
|
|
597
|
-
}, 10000)
|
|
568
|
+
// Apple should still be selected, no actions dialog visible
|
|
569
|
+
expect(afterClickAppleSnapshot).toContain('›🍎 Apple')
|
|
570
|
+
expect(afterClickAppleSnapshot).not.toContain('Actions')
|
|
571
|
+
}, 30000)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Example: CalendarHeatmap component showcase with various color combinations.
|
|
2
|
+
// Shows month splits, width-based truncation, and different color palettes.
|
|
3
|
+
// Demonstrates Markdown component interleaved with Heatmaps for descriptions.
|
|
4
|
+
|
|
5
|
+
import { CalendarHeatmap, Color, Detail, Markdown } from 'termcast'
|
|
6
|
+
import type { CalendarHeatmapData } from 'termcast'
|
|
7
|
+
import { renderWithProviders } from '../utils'
|
|
8
|
+
|
|
9
|
+
function createRangeData(start: Date, dayCount: number, offset: number): CalendarHeatmapData[] {
|
|
10
|
+
return Array.from({ length: dayCount }, (_, index) => {
|
|
11
|
+
const date = new Date(start)
|
|
12
|
+
date.setDate(start.getDate() + index)
|
|
13
|
+
|
|
14
|
+
const dayOfWeek = date.getDay()
|
|
15
|
+
const weekendPenalty = dayOfWeek === 0 || dayOfWeek === 6 ? 0.35 : 1
|
|
16
|
+
const wave = (Math.sin((index + offset) / 6) + 1) / 2
|
|
17
|
+
const value = Math.round((1 + wave * 7) * weekendPenalty)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
date,
|
|
21
|
+
value,
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const summerData = createRangeData(new Date(2025, 5, 1), 110, 0)
|
|
27
|
+
const winterData = createRangeData(new Date(2026, 0, 5), 45, 31)
|
|
28
|
+
const longHistoryData = createRangeData(new Date(2021, 0, 1), 1800, 13)
|
|
29
|
+
const recentData = createRangeData(new Date(2025, 9, 1), 150, 7)
|
|
30
|
+
const shortBurst = createRangeData(new Date(2025, 8, 1), 180, 3)
|
|
31
|
+
const journalData = [...summerData, ...winterData]
|
|
32
|
+
|
|
33
|
+
function SimpleHeatmap() {
|
|
34
|
+
return (
|
|
35
|
+
<Detail
|
|
36
|
+
markdown={[
|
|
37
|
+
'# Calendar Heatmap Color Showcase',
|
|
38
|
+
'',
|
|
39
|
+
'Each heatmap demonstrates a different color combination.',
|
|
40
|
+
'Data has a late-fall gap to show that empty weeks are skipped.',
|
|
41
|
+
'Last heatmap renders multi-year data to verify width truncation.',
|
|
42
|
+
].join('\n')}
|
|
43
|
+
metadata={
|
|
44
|
+
<Detail.Metadata>
|
|
45
|
+
<Markdown content="**Long history** — 5 years of daily data in purple. Months that don't fit the terminal width are truncated from the left." />
|
|
46
|
+
<CalendarHeatmap data={longHistoryData} color={Color.Purple} />
|
|
47
|
+
<Markdown content="**Journal** — summer + winter entries in green, with a fall gap between the two ranges." />
|
|
48
|
+
<CalendarHeatmap data={journalData} color={Color.Green} />
|
|
49
|
+
<Markdown content="**Recent activity** — last 150 days in red, showing the sine-wave pattern clearly." />
|
|
50
|
+
<CalendarHeatmap data={recentData} color={Color.Red} />
|
|
51
|
+
<Markdown content="**Short burst** — 180 days in blue on a purple empty background." />
|
|
52
|
+
<CalendarHeatmap data={shortBurst} color={Color.Blue} emptyColor={Color.Purple} />
|
|
53
|
+
<Markdown content="**Warm tones** — orange cells on magenta empty, same journal data." />
|
|
54
|
+
<CalendarHeatmap data={journalData} color={Color.Orange} emptyColor={Color.Magenta} />
|
|
55
|
+
<Markdown content="**Yellow on blue** — high-contrast palette for the recent data set." />
|
|
56
|
+
<CalendarHeatmap data={recentData} color={Color.Yellow} emptyColor={Color.Blue} />
|
|
57
|
+
</Detail.Metadata>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderWithProviders(<SimpleHeatmap />)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// E2E tests for CalendarHeatmap with normal and overflow data ranges.
|
|
2
|
+
// Verifies month truncation does not overflow terminal width.
|
|
3
|
+
|
|
4
|
+
import { test, expect, afterEach, beforeEach } from 'vitest'
|
|
5
|
+
import { launchTerminal, Session } from 'tuistory/src'
|
|
6
|
+
|
|
7
|
+
let session: Session
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
session = await launchTerminal({
|
|
11
|
+
command: 'bun',
|
|
12
|
+
args: ['src/examples/simple-heatmap.tsx'],
|
|
13
|
+
cols: 88,
|
|
14
|
+
rows: 50,
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
session?.close()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('renders calendar heatmaps with various color combinations', async () => {
|
|
23
|
+
const text = await session.text({
|
|
24
|
+
waitFor: (text) => {
|
|
25
|
+
return text.includes('Calendar Heatmap Color Showcase') && text.includes('Less')
|
|
26
|
+
},
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(text).toMatchInlineSnapshot(`
|
|
31
|
+
"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
█
|
|
35
|
+
|
|
36
|
+
Calendar Heatmap Color Showcase
|
|
37
|
+
|
|
38
|
+
Each heatmap demonstrates a different color combination.
|
|
39
|
+
Data has a late-fall gap to show that empty weeks are skipped.
|
|
40
|
+
Last heatmap renders multi-year data to verify width truncation.
|
|
41
|
+
|
|
42
|
+
Long history — 5 years of daily data in purple. Months that don't fit the
|
|
43
|
+
terminal width are truncated from the left.
|
|
44
|
+
|
|
45
|
+
May Jun Jul Aug Sep Oct Nov
|
|
46
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
|
47
|
+
■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ Mon
|
|
48
|
+
■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼
|
|
49
|
+
■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ Wed
|
|
50
|
+
■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■
|
|
51
|
+
■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ Fri
|
|
52
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
|
53
|
+
Less ◼ ◼ ■ ■ More
|
|
54
|
+
|
|
55
|
+
Journal — summer + winter entries in green, with a fall gap between the two
|
|
56
|
+
ranges.
|
|
57
|
+
|
|
58
|
+
Jun Jul Aug Sep Jan Feb
|
|
59
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
|
60
|
+
■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ Mon
|
|
61
|
+
■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼
|
|
62
|
+
■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ Wed
|
|
63
|
+
■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ◼ ■ ■ ■ ◼ ◼
|
|
64
|
+
■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ Fri
|
|
65
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
|
66
|
+
Less ◼ ◼ ■ ■ More
|
|
67
|
+
|
|
68
|
+
Recent activity — last 150 days in red, showing the sine-wave pattern clearly.
|
|
69
|
+
|
|
70
|
+
Se Oct Nov Dec Jan Feb
|
|
71
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
|
72
|
+
■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ Mon
|
|
73
|
+
■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■
|
|
74
|
+
■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ Wed
|
|
75
|
+
■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■
|
|
76
|
+
■ ■ ◼ ◼ ◼ ■ ■ ◼ ◼ ◼ ■ ■ ■ ◼ ◼ ■ ■ ■ ◼ ◼ ◼ ■ Fri
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
esc go back ^k actions powered by termcast.app
|
|
80
|
+
|
|
81
|
+
"
|
|
82
|
+
`)
|
|
83
|
+
|
|
84
|
+
const maxLineLength = text.split('\n').reduce((maxLength, line) => {
|
|
85
|
+
return Math.max(maxLength, line.length)
|
|
86
|
+
}, 0)
|
|
87
|
+
expect(maxLineLength).toBeLessThanOrEqual(88)
|
|
88
|
+
}, 30000)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Example: ProgressBar rendered inside List.Item.Detail.Metadata.
|
|
2
|
+
// Shows usage-style rows with title, bar+percentage in one line, and reset labels.
|
|
3
|
+
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { List, ProgressBar } from 'termcast'
|
|
6
|
+
import { renderWithProviders } from '../utils'
|
|
7
|
+
|
|
8
|
+
interface UsageItem {
|
|
9
|
+
title: string
|
|
10
|
+
subtitle: string
|
|
11
|
+
sessionUsage: number
|
|
12
|
+
sessionReset: string
|
|
13
|
+
weekUsage: number
|
|
14
|
+
weekReset: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const usageItems: UsageItem[] = [
|
|
18
|
+
{
|
|
19
|
+
title: 'OpenAI account',
|
|
20
|
+
subtitle: 'default workspace',
|
|
21
|
+
sessionUsage: 37,
|
|
22
|
+
sessionReset: 'Resets 9pm (Asia/Bangkok)',
|
|
23
|
+
weekUsage: 7,
|
|
24
|
+
weekReset: 'Resets Feb 27, 1pm (Asia/Bangkok)',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: 'Anthropic account',
|
|
28
|
+
subtitle: 'research workspace',
|
|
29
|
+
sessionUsage: 82,
|
|
30
|
+
sessionReset: 'Resets 11pm (Europe/Rome)',
|
|
31
|
+
weekUsage: 46,
|
|
32
|
+
weekReset: 'Resets Mar 1, 9am (Europe/Rome)',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: 'Google account',
|
|
36
|
+
subtitle: 'sandbox workspace',
|
|
37
|
+
sessionUsage: 15,
|
|
38
|
+
sessionReset: 'Resets 6pm (America/New_York)',
|
|
39
|
+
weekUsage: 24,
|
|
40
|
+
weekReset: 'Resets Mar 3, 8am (America/New_York)',
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function SimpleProgressBar() {
|
|
45
|
+
return (
|
|
46
|
+
<List navigationTitle="ProgressBar Metadata" isShowingDetail={true}>
|
|
47
|
+
{usageItems.map((item) => {
|
|
48
|
+
return (
|
|
49
|
+
<List.Item
|
|
50
|
+
key={item.title}
|
|
51
|
+
title={item.title}
|
|
52
|
+
subtitle={item.subtitle}
|
|
53
|
+
detail={
|
|
54
|
+
<List.Item.Detail
|
|
55
|
+
metadata={
|
|
56
|
+
<List.Item.Detail.Metadata>
|
|
57
|
+
<ProgressBar
|
|
58
|
+
title="Current session"
|
|
59
|
+
value={item.sessionUsage}
|
|
60
|
+
percentageSuffix="used"
|
|
61
|
+
label={item.sessionReset}
|
|
62
|
+
/>
|
|
63
|
+
<ProgressBar
|
|
64
|
+
title="Current week (all models)"
|
|
65
|
+
value={item.weekUsage}
|
|
66
|
+
percentageSuffix="used"
|
|
67
|
+
label={item.weekReset}
|
|
68
|
+
barCharacter="▁"
|
|
69
|
+
trackCharacter="▁"
|
|
70
|
+
/>
|
|
71
|
+
</List.Item.Detail.Metadata>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</List>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
renderWithProviders(<SimpleProgressBar />)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// E2E tests for ProgressBar example.
|
|
2
|
+
// Verifies ProgressBar in List.Item.Detail.Metadata updates per selected item.
|
|
3
|
+
|
|
4
|
+
import { test, expect, afterEach, beforeEach } from 'vitest'
|
|
5
|
+
import { launchTerminal, Session } from 'tuistory/src'
|
|
6
|
+
|
|
7
|
+
let session: Session
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
session = await launchTerminal({
|
|
11
|
+
command: 'bun',
|
|
12
|
+
args: ['src/examples/simple-progress-bar.tsx'],
|
|
13
|
+
cols: 80,
|
|
14
|
+
rows: 24,
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
session?.close()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('progress bars render in detail metadata and update on selection', async () => {
|
|
23
|
+
const initial = await session.text({
|
|
24
|
+
waitFor: (text) => {
|
|
25
|
+
return text.includes('OpenAI account') && text.includes('37% used')
|
|
26
|
+
},
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(initial).toMatchInlineSnapshot(`
|
|
31
|
+
"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ProgressBar Metadata ─────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
> Search...
|
|
37
|
+
|
|
38
|
+
›OpenAI account default workspace │ Current session
|
|
39
|
+
Anthropic account research workspace │ █████████░░░░░░░░░░░░░░░░ 37% used
|
|
40
|
+
Google account sandbox workspace │ Resets 9pm (Asia/Bangkok)
|
|
41
|
+
│
|
|
42
|
+
│ Current week (all models)
|
|
43
|
+
│ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 7% used
|
|
44
|
+
│ Resets Feb 27, 1pm (Asia/Bangkok)
|
|
45
|
+
│
|
|
46
|
+
↑↓ navigate ^k actions │
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
"
|
|
56
|
+
`)
|
|
57
|
+
|
|
58
|
+
expect(initial).toContain('37% used')
|
|
59
|
+
expect(initial).toContain('7% used')
|
|
60
|
+
|
|
61
|
+
await session.press('down')
|
|
62
|
+
|
|
63
|
+
const second = await session.text({
|
|
64
|
+
waitFor: (text) => {
|
|
65
|
+
return text.includes('›Anthropic account') && text.includes('82% used')
|
|
66
|
+
},
|
|
67
|
+
timeout: 10000,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(second).toContain('46% used')
|
|
71
|
+
expect(second).toContain('Europe/Rome')
|
|
72
|
+
}, 30000)
|
|
@@ -86,6 +86,7 @@ test('inline formatting table renders all rows', async () => {
|
|
|
86
86
|
Done.
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
esc go back ^k actions powered by termcast.app
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
|
|
@@ -106,7 +107,6 @@ test('inline formatting table renders all rows', async () => {
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
esc go back ^k actions powered by termcast.app
|
|
110
110
|
|
|
111
111
|
"
|
|
112
112
|
`)
|
package/src/index.tsx
CHANGED
|
@@ -45,6 +45,10 @@ export type {
|
|
|
45
45
|
ActionPanelSectionProps,
|
|
46
46
|
} from 'termcast/src/components/actions'
|
|
47
47
|
|
|
48
|
+
// Core UI Components - Markdown
|
|
49
|
+
export { Markdown } from 'termcast/src/components/markdown'
|
|
50
|
+
export type { MarkdownProps } from 'termcast/src/components/markdown'
|
|
51
|
+
|
|
48
52
|
// Core UI Components - Detail
|
|
49
53
|
export { Detail } from 'termcast/src/components/detail'
|
|
50
54
|
export type {
|
|
@@ -82,6 +86,21 @@ export type {
|
|
|
82
86
|
BarGraphSeriesProps,
|
|
83
87
|
} from 'termcast/src/components/bar-graph'
|
|
84
88
|
|
|
89
|
+
// Core UI Components - CalendarHeatmap
|
|
90
|
+
export { CalendarHeatmap, Heatmap } from 'termcast/src/components/heatmap'
|
|
91
|
+
export type {
|
|
92
|
+
CalendarHeatmapProps,
|
|
93
|
+
CalendarHeatmapData,
|
|
94
|
+
CalendarHeatmapCellChar,
|
|
95
|
+
HeatmapProps,
|
|
96
|
+
HeatmapData,
|
|
97
|
+
HeatmapCellChar,
|
|
98
|
+
} from 'termcast/src/components/heatmap'
|
|
99
|
+
|
|
100
|
+
// Core UI Components - ProgressBar
|
|
101
|
+
export { ProgressBar } from 'termcast/src/components/progress-bar'
|
|
102
|
+
export type { ProgressBarProps } from 'termcast/src/components/progress-bar'
|
|
103
|
+
|
|
85
104
|
// Form Components
|
|
86
105
|
import {
|
|
87
106
|
Form as FormComponent,
|
|
@@ -377,7 +377,6 @@ export function DatePickerWidget({
|
|
|
377
377
|
enableColors && focus === 'year' ? Theme.primary : undefined,
|
|
378
378
|
marginBottom: 0,
|
|
379
379
|
}}
|
|
380
|
-
onMouseDown={() => setFocus('year')}
|
|
381
380
|
>
|
|
382
381
|
<box
|
|
383
382
|
style={{
|
|
@@ -387,11 +386,17 @@ export function DatePickerWidget({
|
|
|
387
386
|
width: headerWidth,
|
|
388
387
|
}}
|
|
389
388
|
>
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
<box onMouseDown={() => { changeYear(-1); setFocus('year') }}>
|
|
390
|
+
<text fg={focus === 'year' ? Theme.text : Theme.textMuted}>←</text>
|
|
391
|
+
</box>
|
|
392
|
+
<box onMouseDown={() => { setFocus('year') }}>
|
|
393
|
+
<text fg={focus === 'year' ? Theme.text : Theme.textMuted}>
|
|
394
|
+
{String(y)}
|
|
395
|
+
</text>
|
|
396
|
+
</box>
|
|
397
|
+
<box onMouseDown={() => { changeYear(+1); setFocus('year') }}>
|
|
398
|
+
<text fg={focus === 'year' ? Theme.text : Theme.textMuted}>→</text>
|
|
399
|
+
</box>
|
|
395
400
|
</box>
|
|
396
401
|
</box>
|
|
397
402
|
|
|
@@ -404,7 +409,6 @@ export function DatePickerWidget({
|
|
|
404
409
|
enableColors && focus === 'month' ? Theme.primary : undefined,
|
|
405
410
|
marginBottom: 1,
|
|
406
411
|
}}
|
|
407
|
-
onMouseDown={() => setFocus('month')}
|
|
408
412
|
>
|
|
409
413
|
<box
|
|
410
414
|
style={{
|
|
@@ -414,11 +418,17 @@ export function DatePickerWidget({
|
|
|
414
418
|
width: headerWidth,
|
|
415
419
|
}}
|
|
416
420
|
>
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
<box onMouseDown={() => { changeMonth(-1); setFocus('month') }}>
|
|
422
|
+
<text fg={focus === 'month' ? Theme.text : Theme.textMuted}>←</text>
|
|
423
|
+
</box>
|
|
424
|
+
<box onMouseDown={() => { setFocus('month') }}>
|
|
425
|
+
<text fg={focus === 'month' ? Theme.text : Theme.textMuted}>
|
|
426
|
+
{MONTHS[m]}
|
|
427
|
+
</text>
|
|
428
|
+
</box>
|
|
429
|
+
<box onMouseDown={() => { changeMonth(+1); setFocus('month') }}>
|
|
430
|
+
<text fg={focus === 'month' ? Theme.text : Theme.textMuted}>→</text>
|
|
431
|
+
</box>
|
|
422
432
|
</box>
|
|
423
433
|
</box>
|
|
424
434
|
|
|
@@ -485,6 +495,7 @@ export function DatePickerWidget({
|
|
|
485
495
|
setSelected(d)
|
|
486
496
|
setFocus('grid')
|
|
487
497
|
ensureVisibleFor(d)
|
|
498
|
+
onChange?.(d)
|
|
488
499
|
}}
|
|
489
500
|
>
|
|
490
501
|
<text
|
|
@@ -13,6 +13,7 @@ import { CommonProps } from 'termcast/src/utils'
|
|
|
13
13
|
import { useStore, type NavigationStackItem } from 'termcast/src/state'
|
|
14
14
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
15
15
|
import { logger } from '../logger'
|
|
16
|
+
import { isAppMode } from '../apis/environment'
|
|
16
17
|
|
|
17
18
|
interface Navigation {
|
|
18
19
|
push: (element: ReactNode, onPop?: () => void) => void
|
|
@@ -172,8 +173,12 @@ export function NavigationProvider(props: NavigationProviderProps): any {
|
|
|
172
173
|
activeSearchInputRef.setText('')
|
|
173
174
|
return
|
|
174
175
|
}
|
|
175
|
-
// At root with no dialogs and no search text - exit the CLI
|
|
176
|
-
|
|
176
|
+
// At root with no dialogs and no search text - exit the CLI.
|
|
177
|
+
// In app mode (standalone desktop app), ESC at root is a no-op
|
|
178
|
+
// since destroying the renderer would kill the entire application.
|
|
179
|
+
if (!isAppMode()) {
|
|
180
|
+
renderer.destroy()
|
|
181
|
+
}
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
})
|
|
@@ -86,6 +86,7 @@ class ErrorBoundaryClass extends Component<
|
|
|
86
86
|
constructor(props: { children: ReactNode }) {
|
|
87
87
|
super(props)
|
|
88
88
|
this.state = { hasError: false, error: null }
|
|
89
|
+
this.reset = this.reset.bind(this)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
@@ -100,22 +101,45 @@ class ErrorBoundaryClass extends Component<
|
|
|
100
101
|
})
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
reset(): void {
|
|
105
|
+
// Clear navigation and dialog stacks so the app returns to the root view
|
|
106
|
+
// instead of re-rendering the same crashed component
|
|
107
|
+
useStore.setState({
|
|
108
|
+
navigationStack: [],
|
|
109
|
+
dialogStack: [],
|
|
110
|
+
toast: null,
|
|
111
|
+
toastWithPrimaryAction: false,
|
|
112
|
+
showActionsDialog: false,
|
|
113
|
+
})
|
|
114
|
+
this.setState({ hasError: false, error: null })
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
render(): any {
|
|
104
118
|
if (this.state.hasError) {
|
|
105
|
-
return <ErrorDisplay error={this.state.error} />
|
|
119
|
+
return <ErrorDisplay error={this.state.error} onRetry={this.reset} />
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
return this.props.children
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
function ErrorDisplay({ error }: { error: Error | null }): any {
|
|
126
|
+
function ErrorDisplay({ error, onRetry }: { error: Error | null; onRetry: () => void }): any {
|
|
113
127
|
const theme = useTheme()
|
|
128
|
+
|
|
129
|
+
useKeyboard((evt) => {
|
|
130
|
+
if (evt.name === 'return') {
|
|
131
|
+
onRetry()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
114
135
|
return (
|
|
115
|
-
<box padding={2}>
|
|
136
|
+
<box padding={2} flexDirection="column" gap={1}>
|
|
116
137
|
<text fg={theme.error} wrapMode='none'>
|
|
117
138
|
{error?.stack}
|
|
118
139
|
</text>
|
|
140
|
+
<text fg={theme.textMuted}>
|
|
141
|
+
Press Enter to retry
|
|
142
|
+
</text>
|
|
119
143
|
</box>
|
|
120
144
|
)
|
|
121
145
|
}
|
|
@@ -125,6 +149,7 @@ const ErrorBoundary = ErrorBoundaryClass as any
|
|
|
125
149
|
export function TermcastProvider(props: ProvidersProps): any {
|
|
126
150
|
const theme = useTheme()
|
|
127
151
|
const renderer = useRenderer()
|
|
152
|
+
|
|
128
153
|
useKeyboard((key) => {
|
|
129
154
|
if (!renderer) return
|
|
130
155
|
if (key.ctrl && key.name === 'd') {
|
|
@@ -136,6 +161,26 @@ export function TermcastProvider(props: ProvidersProps): any {
|
|
|
136
161
|
}
|
|
137
162
|
})
|
|
138
163
|
|
|
164
|
+
// Cmd+C (super+c): if there's an active selection, copy it to clipboard and clear.
|
|
165
|
+
// Otherwise let the key propagate to the TUI for other handlers.
|
|
166
|
+
// In standalone apps, WezTerm forwards Cmd+C via SendKey so it arrives as super modifier.
|
|
167
|
+
useKeyboard((key) => {
|
|
168
|
+
if (!renderer) return
|
|
169
|
+
if (key.super && key.name === 'c') {
|
|
170
|
+
if (renderer.hasSelection) {
|
|
171
|
+
const selection = renderer.getSelection()
|
|
172
|
+
if (selection) {
|
|
173
|
+
const text = selection.getSelectedText()
|
|
174
|
+
if (text) {
|
|
175
|
+
Clipboard.copy(text)
|
|
176
|
+
renderer.clearSelection()
|
|
177
|
+
key.stopPropagation()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
139
184
|
return (
|
|
140
185
|
<ErrorBoundary>
|
|
141
186
|
<Suspense fallback={<LoadingFallback />}>
|
package/src/logger.tsx
CHANGED
|
@@ -66,7 +66,12 @@ process.on('uncaughtException', (error: Error) => {
|
|
|
66
66
|
} else {
|
|
67
67
|
logger.error('Uncaught Exception:', serialize(error))
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// In app mode, don't exit on uncaught exceptions — the error boundary
|
|
70
|
+
// will catch React errors, and crashing the whole app is worse than
|
|
71
|
+
// a broken screen the user can recover from.
|
|
72
|
+
if (process.env.TERMCAST_APP_MODE !== '1') {
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
70
75
|
})
|
|
71
76
|
|
|
72
77
|
process.on('unhandledRejection', async (reason: any, promise: Promise<any>) => {
|