termcast 1.3.54 → 1.4.1
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/action-utils.d.ts.map +1 -1
- package/dist/action-utils.js +17 -132
- package/dist/action-utils.js.map +1 -1
- package/dist/apis/cache.d.ts +8 -30
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +9 -271
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/clipboard.d.ts +4 -2
- package/dist/apis/clipboard.d.ts.map +1 -1
- package/dist/apis/clipboard.js +18 -31
- package/dist/apis/clipboard.js.map +1 -1
- package/dist/apis/environment.d.ts.map +1 -1
- package/dist/apis/environment.js +14 -49
- package/dist/apis/environment.js.map +1 -1
- package/dist/apis/localstorage.d.ts +7 -12
- package/dist/apis/localstorage.d.ts.map +1 -1
- package/dist/apis/localstorage.js +7 -184
- package/dist/apis/localstorage.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +16 -15
- package/dist/app.js.map +1 -1
- package/dist/cli.js +7 -6
- package/dist/cli.js.map +1 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +13 -2
- package/dist/components/actions.js.map +1 -1
- package/dist/components/extension-preferences.d.ts.map +1 -1
- package/dist/components/extension-preferences.js +7 -8
- package/dist/components/extension-preferences.js.map +1 -1
- package/dist/components/form/file-autocomplete.js +2 -2
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +242 -14
- package/dist/components/list.js.map +1 -1
- package/dist/e2e-node.d.ts.map +1 -1
- package/dist/e2e-node.js +5 -4
- package/dist/e2e-node.js.map +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +5 -2
- package/dist/extensions/dev.js.map +1 -1
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +2 -1
- package/dist/globals.js.map +1 -1
- package/dist/internal/error-handler.d.ts.map +1 -1
- package/dist/internal/error-handler.js +21 -19
- package/dist/internal/error-handler.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +41 -1
- package/dist/internal/providers.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +40 -29
- package/dist/logger.js.map +1 -1
- package/dist/platform/browser/cache.d.ts +41 -0
- package/dist/platform/browser/cache.d.ts.map +1 -0
- package/dist/platform/browser/cache.js +262 -0
- package/dist/platform/browser/cache.js.map +1 -0
- package/dist/platform/browser/localstorage.d.ts +20 -0
- package/dist/platform/browser/localstorage.d.ts.map +1 -0
- package/dist/platform/browser/localstorage.js +102 -0
- package/dist/platform/browser/localstorage.js.map +1 -0
- package/dist/platform/browser/runtime.d.ts +51 -0
- package/dist/platform/browser/runtime.d.ts.map +1 -0
- package/dist/platform/browser/runtime.js +164 -0
- package/dist/platform/browser/runtime.js.map +1 -0
- package/dist/platform/bun/sqlite.d.ts +17 -0
- package/dist/platform/bun/sqlite.d.ts.map +1 -0
- package/dist/platform/bun/sqlite.js +6 -0
- package/dist/platform/bun/sqlite.js.map +1 -0
- package/dist/platform/node/cache.d.ts +35 -0
- package/dist/platform/node/cache.d.ts.map +1 -0
- package/dist/platform/node/cache.js +269 -0
- package/dist/platform/node/cache.js.map +1 -0
- package/dist/platform/node/localstorage.d.ts +17 -0
- package/dist/platform/node/localstorage.d.ts.map +1 -0
- package/dist/platform/node/localstorage.js +186 -0
- package/dist/platform/node/localstorage.js.map +1 -0
- package/dist/platform/node/runtime.d.ts +52 -0
- package/dist/platform/node/runtime.d.ts.map +1 -0
- package/dist/platform/node/runtime.js +230 -0
- package/dist/platform/node/runtime.js.map +1 -0
- package/dist/platform/node/sqlite.d.ts +27 -0
- package/dist/platform/node/sqlite.d.ts.map +1 -0
- package/dist/platform/node/sqlite.js +21 -0
- package/dist/platform/node/sqlite.js.map +1 -0
- package/dist/state.d.ts +5 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -28
- package/dist/state.js.map +1 -1
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +17 -22
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +42 -47
- package/dist/utils.js.map +1 -1
- package/dist/vim-mode.d.ts +40 -0
- package/dist/vim-mode.d.ts.map +1 -0
- package/dist/vim-mode.js +135 -0
- package/dist/vim-mode.js.map +1 -0
- package/fonts/Inconsolata.otf +0 -0
- package/fonts/SIL Open Font License.txt +41 -0
- package/package.json +62 -10
- package/src/action-utils.tsx +27 -124
- package/src/apis/cache.test.ts +1 -1
- package/src/apis/cache.tsx +9 -373
- package/src/apis/clipboard.tsx +29 -38
- package/src/apis/environment.tsx +25 -52
- package/src/apis/localstorage.tsx +8 -214
- package/src/app.tsx +16 -15
- package/src/cli.tsx +14 -15
- package/src/compile.vitest.tsx +2 -2
- package/src/components/actions.tsx +19 -1
- package/src/components/extension-preferences.tsx +7 -8
- package/src/components/form/file-autocomplete.tsx +2 -2
- package/src/components/list.tsx +279 -14
- package/src/e2e-node.tsx +7 -7
- package/src/examples/action-shortcut.vitest.tsx +2 -2
- package/src/examples/actions-context.vitest.tsx +1 -1
- package/src/examples/bar-graph-weekly.vitest.tsx +10 -36
- package/src/examples/detail-metadata-showcase.vitest.tsx +37 -42
- package/src/examples/form-basic.vitest.tsx +45 -41
- package/src/examples/github.vitest.tsx +4 -4
- package/src/examples/graph-bar-chart.vitest.tsx +13 -11
- package/src/examples/graph-polymarket.vitest.tsx +2 -2
- package/src/examples/graph-row.vitest.tsx +66 -66
- package/src/examples/graph-styles.vitest.tsx +12 -12
- package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -48
- package/src/examples/list-detail-metadata.vitest.tsx +5 -5
- package/src/examples/list-fetch-data.vitest.tsx +3 -3
- package/src/examples/list-item-accessories.vitest.tsx +2 -2
- package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
- package/src/examples/list-no-actions.vitest.tsx +2 -2
- package/src/examples/list-scrollbox.vitest.tsx +5 -5
- package/src/examples/list-spacing-mode.vitest.tsx +3 -3
- package/src/examples/list-with-detail.vitest.tsx +68 -68
- package/src/examples/list-with-dropdown.vitest.tsx +5 -5
- package/src/examples/list-with-sections.vitest.tsx +27 -27
- package/src/examples/simple-candle-chart.vitest.tsx +7 -7
- package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
- package/src/examples/simple-detail-table.vitest.tsx +8 -8
- package/src/examples/simple-graph.vitest.tsx +3 -3
- package/src/examples/simple-grid.vitest.tsx +14 -14
- package/src/examples/simple-heatmap.vitest.tsx +10 -10
- package/src/examples/simple-navigation.vitest.tsx +17 -17
- package/src/examples/simple-progress-bar.vitest.tsx +1 -1
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/swift-extension.vitest.tsx +2 -2
- package/src/examples/table-edge-cases.vitest.tsx +18 -18
- package/src/examples/toast-action.vitest.tsx +2 -2
- package/src/examples/toast-variations.vitest.tsx +5 -5
- package/src/extensions/dev.tsx +5 -2
- package/src/extensions/dev.vitest.tsx +3 -3
- package/src/globals.ts +2 -1
- package/src/internal/error-handler.tsx +19 -21
- package/src/internal/providers.tsx +39 -0
- package/src/logger.tsx +48 -41
- package/src/platform/browser/cache.ts +327 -0
- package/src/platform/browser/localstorage.ts +119 -0
- package/src/platform/browser/runtime.ts +209 -0
- package/src/platform/bun/sqlite.ts +19 -0
- package/src/platform/node/cache.ts +372 -0
- package/src/platform/node/localstorage.ts +214 -0
- package/src/platform/node/runtime.ts +264 -0
- package/src/platform/node/sqlite.ts +43 -0
- package/src/state.tsx +17 -28
- package/src/utils/file-system.ts +17 -22
- package/src/utils.test.tsx +1 -1
- package/src/utils.tsx +56 -47
- package/src/vim-mode.tsx +153 -0
- package/src/apis/sqlite.ts +0 -14
package/src/components/list.tsx
CHANGED
|
@@ -39,6 +39,8 @@ import { ActionPanel, matchesShortcut } from 'termcast/src/components/actions'
|
|
|
39
39
|
import { getInteractiveHoverBackground, useTheme } from 'termcast/src/theme'
|
|
40
40
|
import { Markdown } from 'termcast/src/components/markdown'
|
|
41
41
|
import { CommonProps } from 'termcast/src/utils'
|
|
42
|
+
import { getMatchingCommands, executeVimCommand } from 'termcast/src/vim-mode'
|
|
43
|
+
import { ThemePicker } from 'termcast/src/components/theme-picker'
|
|
42
44
|
|
|
43
45
|
export { Color }
|
|
44
46
|
|
|
@@ -84,6 +86,9 @@ function ListFooter(): any {
|
|
|
84
86
|
const dropdownFooterLabel = useStore((s) => s.dropdownFooterLabel)
|
|
85
87
|
const dropdownTooltip = useStore((s) => s.dropdownTooltip)
|
|
86
88
|
const hasToast = useStore((s) => s.toast !== null)
|
|
89
|
+
const inputMode = useStore((s) => s.inputMode)
|
|
90
|
+
const vimInputSubMode = useStore((s) => s.vimInputSubMode)
|
|
91
|
+
const vimCommandText = useStore((s) => s.vimCommandText)
|
|
87
92
|
const listContext = useContext(ListContext)
|
|
88
93
|
const isShowingDetail = listContext?.isShowingDetail ?? false
|
|
89
94
|
const hasDropdown = listContext?.hasDropdown ?? false
|
|
@@ -94,6 +99,23 @@ function ListFooter(): any {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
// When in command mode, show the command input in the footer
|
|
103
|
+
if (vimInputSubMode === 'command' && !hasToast) {
|
|
104
|
+
const matchingCommands = getMatchingCommands(vimCommandText)
|
|
105
|
+
return (
|
|
106
|
+
<Footer hidePoweredBy={isShowingDetail}>
|
|
107
|
+
<box style={{ flexDirection: 'row', gap: 2, flexShrink: 0, flexGrow: 1 }}>
|
|
108
|
+
<text flexShrink={0} fg={theme.text}>:{vimCommandText}</text>
|
|
109
|
+
<text flexShrink={0} fg={theme.textMuted}>
|
|
110
|
+
{matchingCommands.map((cmd) => cmd.name).join(' · ')}
|
|
111
|
+
</text>
|
|
112
|
+
</box>
|
|
113
|
+
</Footer>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isVim = inputMode === 'vim'
|
|
118
|
+
|
|
97
119
|
const content = hasToast ? null : (
|
|
98
120
|
<box style={{ flexDirection: 'row', gap: 3, flexShrink: 0 }}>
|
|
99
121
|
{firstActionTitle && (
|
|
@@ -110,10 +132,18 @@ function ListFooter(): any {
|
|
|
110
132
|
)}
|
|
111
133
|
<box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
|
|
112
134
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
113
|
-
↑↓
|
|
135
|
+
{isVim ? 'j/k' : '↑↓'}
|
|
114
136
|
</text>
|
|
115
137
|
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
116
138
|
</box>
|
|
139
|
+
{isVim && (
|
|
140
|
+
<box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
|
|
141
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
142
|
+
/
|
|
143
|
+
</text>
|
|
144
|
+
<text flexShrink={0} fg={theme.textMuted}>search</text>
|
|
145
|
+
</box>
|
|
146
|
+
)}
|
|
117
147
|
<Hoverable
|
|
118
148
|
onMouseDown={() => {
|
|
119
149
|
useStore.setState({ showActionsDialog: true })
|
|
@@ -134,6 +164,15 @@ function ListFooter(): any {
|
|
|
134
164
|
</text>
|
|
135
165
|
</Hoverable>
|
|
136
166
|
)}
|
|
167
|
+
{!isVim && (
|
|
168
|
+
<Hoverable
|
|
169
|
+
onMouseDown={() => {
|
|
170
|
+
executeVimCommand('vim')
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<text flexShrink={0} fg={theme.textMuted}>:vim</text>
|
|
174
|
+
</Hoverable>
|
|
175
|
+
)}
|
|
137
176
|
</box>
|
|
138
177
|
)
|
|
139
178
|
|
|
@@ -1367,18 +1406,237 @@ export const List: ListType = (props) => {
|
|
|
1367
1406
|
|
|
1368
1407
|
const inFocus = useIsInFocus()
|
|
1369
1408
|
const dialog = useDialog()
|
|
1409
|
+
const inputMode = useStore((s) => s.inputMode)
|
|
1410
|
+
const vimInputSubMode = useStore((s) => s.vimInputSubMode)
|
|
1411
|
+
|
|
1412
|
+
// Textarea is focused when:
|
|
1413
|
+
// - raycast mode default (always focused for type-to-search)
|
|
1414
|
+
// - vim mode search (user pressed /)
|
|
1415
|
+
// - NOT in command mode (command input lives in footer, not textarea)
|
|
1416
|
+
const searchBarFocused = vimInputSubMode !== 'command' && (inputMode === 'raycast' || vimInputSubMode === 'search')
|
|
1417
|
+
|
|
1418
|
+
// Timestamp-based gg sequence detection in vim mode.
|
|
1419
|
+
// If two 'g' presses happen within 500ms, jump to first item.
|
|
1420
|
+
// Using timestamps instead of setTimeout avoids cleanup on unmount.
|
|
1421
|
+
const lastGPressedAtRef = useRef(0)
|
|
1422
|
+
|
|
1423
|
+
// Helper: jump to first visible item
|
|
1424
|
+
const moveToFirst = () => {
|
|
1425
|
+
const items = Object.values(descendantsContext.map.current)
|
|
1426
|
+
.filter((item) => item.index !== -1 && item.props?.visible !== false)
|
|
1427
|
+
.sort((a, b) => a.index - b.index)
|
|
1428
|
+
if (items[0]) {
|
|
1429
|
+
flushSync(() => {
|
|
1430
|
+
setSelectedIndex(items[0].index)
|
|
1431
|
+
})
|
|
1432
|
+
persistSelectedIndexInCurrentNavigationItem(items[0].index)
|
|
1433
|
+
scrollToItemIfNeeded({ item: items[0], direction: -1 })
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Helper: jump to last visible item
|
|
1438
|
+
const moveToLast = () => {
|
|
1439
|
+
const items = Object.values(descendantsContext.map.current)
|
|
1440
|
+
.filter((item) => item.index !== -1 && item.props?.visible !== false)
|
|
1441
|
+
.sort((a, b) => a.index - b.index)
|
|
1442
|
+
const lastItem = items[items.length - 1]
|
|
1443
|
+
if (lastItem) {
|
|
1444
|
+
flushSync(() => {
|
|
1445
|
+
setSelectedIndex(lastItem.index)
|
|
1446
|
+
})
|
|
1447
|
+
persistSelectedIndexInCurrentNavigationItem(lastItem.index)
|
|
1448
|
+
scrollToItemIfNeeded({ item: lastItem, direction: 1 })
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Helper: move by N items (for half-page jumps)
|
|
1453
|
+
const moveByN = (n: number) => {
|
|
1454
|
+
const items = Object.values(descendantsContext.map.current)
|
|
1455
|
+
.filter((item) => item.index !== -1 && item.props?.visible !== false)
|
|
1456
|
+
.sort((a, b) => a.index - b.index)
|
|
1457
|
+
if (items.length === 0) return
|
|
1458
|
+
|
|
1459
|
+
const currentVisibleIndex = items.findIndex((item) => item.index === selectedIndex)
|
|
1460
|
+
if (currentVisibleIndex === -1) return
|
|
1461
|
+
|
|
1462
|
+
const nextVisibleIndex = Math.max(0, Math.min(items.length - 1, currentVisibleIndex + n))
|
|
1463
|
+
const nextItem = items[nextVisibleIndex]
|
|
1464
|
+
if (nextItem) {
|
|
1465
|
+
flushSync(() => {
|
|
1466
|
+
setSelectedIndex(nextItem.index)
|
|
1467
|
+
})
|
|
1468
|
+
persistSelectedIndexInCurrentNavigationItem(nextItem.index)
|
|
1469
|
+
scrollToItemIfNeeded({ item: nextItem, direction: n > 0 ? 1 : -1 })
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1370
1472
|
|
|
1371
1473
|
useKeyboard((evt) => {
|
|
1372
1474
|
if (!inFocus) return
|
|
1373
1475
|
|
|
1374
|
-
|
|
1476
|
+
const { inputMode, vimInputSubMode } = useStore.getState()
|
|
1477
|
+
|
|
1478
|
+
// ── Command mode: trap all keys ──
|
|
1479
|
+
// When ':' command input is active, all keystrokes are handled here.
|
|
1480
|
+
// Enter executes, Esc cancels, backspace deletes, printable chars append.
|
|
1481
|
+
// Auto-exit command mode if a toast appeared (toast needs its own key handlers).
|
|
1482
|
+
if (vimInputSubMode === 'command') {
|
|
1483
|
+
if (useStore.getState().toast) {
|
|
1484
|
+
useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
evt.stopPropagation()
|
|
1488
|
+
|
|
1489
|
+
if (evt.name === 'return') {
|
|
1490
|
+
const text = useStore.getState().vimCommandText
|
|
1491
|
+
const result = executeVimCommand(text)
|
|
1492
|
+
if (result === 'theme') {
|
|
1493
|
+
dialog.push({ element: <ThemePicker /> })
|
|
1494
|
+
} else if (result === 'actions') {
|
|
1495
|
+
useStore.setState({ showActionsDialog: true })
|
|
1496
|
+
} else if (result === 'filter') {
|
|
1497
|
+
if (searchBarAccessory && !isDropdownOpen) {
|
|
1498
|
+
openDropdown()
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
|
|
1502
|
+
return
|
|
1503
|
+
}
|
|
1504
|
+
if (evt.name === 'escape') {
|
|
1505
|
+
useStore.setState({ vimInputSubMode: 'default', vimCommandText: '' })
|
|
1506
|
+
return
|
|
1507
|
+
}
|
|
1508
|
+
if (evt.name === 'backspace') {
|
|
1509
|
+
const current = useStore.getState().vimCommandText
|
|
1510
|
+
if (current.length === 0) {
|
|
1511
|
+
// Backspace on empty exits command mode
|
|
1512
|
+
useStore.setState({ vimInputSubMode: 'default' })
|
|
1513
|
+
} else {
|
|
1514
|
+
useStore.setState({ vimCommandText: current.slice(0, -1) })
|
|
1515
|
+
}
|
|
1516
|
+
return
|
|
1517
|
+
}
|
|
1518
|
+
if (evt.name === 'tab') {
|
|
1519
|
+
// Tab-complete: fill in the first matching command
|
|
1520
|
+
const current = useStore.getState().vimCommandText
|
|
1521
|
+
const matches = getMatchingCommands(current)
|
|
1522
|
+
if (matches.length > 0 && matches[0]) {
|
|
1523
|
+
useStore.setState({ vimCommandText: matches[0].name })
|
|
1524
|
+
}
|
|
1525
|
+
return
|
|
1526
|
+
}
|
|
1527
|
+
// Append printable characters (single char, no modifiers except shift)
|
|
1528
|
+
if (evt.sequence.length === 1 && !evt.ctrl && !evt.meta) {
|
|
1529
|
+
useStore.setState({ vimCommandText: useStore.getState().vimCommandText + evt.sequence })
|
|
1530
|
+
}
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ── Vim search mode: Enter confirms, Esc clears and exits ──
|
|
1535
|
+
// Ctrl+K and Ctrl+P still work while searching (fall through to shared handlers below).
|
|
1536
|
+
if (inputMode === 'vim' && vimInputSubMode === 'search') {
|
|
1537
|
+
if (evt.name === 'return') {
|
|
1538
|
+
// Confirm search: keep search text, return to normal mode
|
|
1539
|
+
useStore.setState({ vimInputSubMode: 'default' })
|
|
1540
|
+
evt.stopPropagation()
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
if (evt.name === 'escape') {
|
|
1544
|
+
// Clear search and return to normal mode
|
|
1545
|
+
useStore.setState({ vimInputSubMode: 'default' })
|
|
1546
|
+
if (inputRef.current) {
|
|
1547
|
+
inputRef.current.clear()
|
|
1548
|
+
}
|
|
1549
|
+
handleSearchChange('')
|
|
1550
|
+
evt.stopPropagation()
|
|
1551
|
+
return
|
|
1552
|
+
}
|
|
1553
|
+
// Let Ctrl+K, Ctrl+P, and registered shortcuts fall through to shared handlers.
|
|
1554
|
+
// All other keys (text input) are handled by the focused textarea.
|
|
1555
|
+
const isSharedShortcut = (evt.ctrl || evt.super || evt.meta)
|
|
1556
|
+
if (!isSharedShortcut) return
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// ── Shared: Ctrl+P for dropdown ──
|
|
1375
1560
|
if (evt.ctrl && evt.name === 'p' && searchBarAccessory && !isDropdownOpen) {
|
|
1376
1561
|
openDropdown()
|
|
1377
1562
|
return
|
|
1378
1563
|
}
|
|
1379
1564
|
|
|
1380
|
-
//
|
|
1381
|
-
|
|
1565
|
+
// ── Shared: Ctrl+K / Cmd+K for actions dialog ──
|
|
1566
|
+
if (evt.name === 'k' && (evt.ctrl || evt.super)) {
|
|
1567
|
+
useStore.setState({ showActionsDialog: true })
|
|
1568
|
+
return
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// ── Vim mode keybindings (only in default sub-mode) ──
|
|
1572
|
+
// Checked before registered action shortcuts so extensions can't hijack
|
|
1573
|
+
// core vim motions like j/k with unmodified single-letter shortcuts.
|
|
1574
|
+
if (inputMode === 'vim' && vimInputSubMode === 'default') {
|
|
1575
|
+
// j/k for navigation
|
|
1576
|
+
if (evt.name === 'j' && !evt.ctrl && !evt.meta) {
|
|
1577
|
+
move(1)
|
|
1578
|
+
evt.stopPropagation()
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1581
|
+
if (evt.name === 'k' && !evt.ctrl && !evt.meta) {
|
|
1582
|
+
move(-1)
|
|
1583
|
+
evt.stopPropagation()
|
|
1584
|
+
return
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// G (shift+g) for last item
|
|
1588
|
+
if (evt.name === 'g' && evt.shift) {
|
|
1589
|
+
moveToLast()
|
|
1590
|
+
evt.stopPropagation()
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// g then g for first item (gg sequence).
|
|
1595
|
+
// Two 'g' presses within 500ms triggers jump to first item.
|
|
1596
|
+
if (evt.name === 'g' && !evt.shift && !evt.ctrl && !evt.meta) {
|
|
1597
|
+
const now = Date.now()
|
|
1598
|
+
if (now - lastGPressedAtRef.current < 500) {
|
|
1599
|
+
// Second g within window: jump to first
|
|
1600
|
+
lastGPressedAtRef.current = 0
|
|
1601
|
+
moveToFirst()
|
|
1602
|
+
} else {
|
|
1603
|
+
// First g: record timestamp, wait for second
|
|
1604
|
+
lastGPressedAtRef.current = now
|
|
1605
|
+
}
|
|
1606
|
+
evt.stopPropagation()
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Ctrl+d for half-page down, Ctrl+u for half-page up
|
|
1611
|
+
if (evt.ctrl && evt.name === 'd') {
|
|
1612
|
+
const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
|
|
1613
|
+
moveByN(Math.floor(viewportHeight / 2))
|
|
1614
|
+
evt.stopPropagation()
|
|
1615
|
+
return
|
|
1616
|
+
}
|
|
1617
|
+
if (evt.ctrl && evt.name === 'u') {
|
|
1618
|
+
const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
|
|
1619
|
+
moveByN(-Math.floor(viewportHeight / 2))
|
|
1620
|
+
evt.stopPropagation()
|
|
1621
|
+
return
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// / to enter search mode
|
|
1625
|
+
if (evt.sequence === '/' && !evt.ctrl && !evt.meta) {
|
|
1626
|
+
useStore.setState({ vimInputSubMode: 'search' })
|
|
1627
|
+
evt.stopPropagation()
|
|
1628
|
+
return
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// : to enter command mode
|
|
1632
|
+
if (evt.sequence === ':' && !evt.ctrl && !evt.meta) {
|
|
1633
|
+
useStore.setState({ vimInputSubMode: 'command', vimCommandText: '' })
|
|
1634
|
+
evt.stopPropagation()
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// ── Shared: registered action shortcuts ──
|
|
1382
1640
|
const registeredShortcuts = useStore.getState().registeredActionShortcuts
|
|
1383
1641
|
for (const { shortcut, execute } of registeredShortcuts) {
|
|
1384
1642
|
if (matchesShortcut(evt, shortcut)) {
|
|
@@ -1393,28 +1651,35 @@ export const List: ListType = (props) => {
|
|
|
1393
1651
|
.sort((a, b) => a.index - b.index)
|
|
1394
1652
|
const currentItem = items.find((item) => item.index === selectedIndex)
|
|
1395
1653
|
|
|
1396
|
-
//
|
|
1397
|
-
// Always open — built-in actions (Change Theme, etc.) are always available
|
|
1398
|
-
if (evt.name === 'k' && (evt.ctrl || evt.super)) {
|
|
1399
|
-
useStore.setState({ showActionsDialog: true })
|
|
1400
|
-
return
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1654
|
+
// ── Shared: arrow keys ──
|
|
1403
1655
|
if (evt.name === 'up') move(-1)
|
|
1404
1656
|
if (evt.name === 'down') move(1)
|
|
1405
|
-
|
|
1657
|
+
|
|
1658
|
+
// ── Shared: Enter to auto-execute first action ──
|
|
1406
1659
|
if (evt.name === 'return') {
|
|
1407
1660
|
if (!currentItem?.props) return
|
|
1408
|
-
|
|
1409
1661
|
if (currentItem.props.actions) {
|
|
1410
1662
|
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
1411
1663
|
}
|
|
1664
|
+
return
|
|
1412
1665
|
}
|
|
1413
1666
|
})
|
|
1414
1667
|
|
|
1415
1668
|
const handleSearchChange = (newValue: string) => {
|
|
1416
1669
|
if (!inFocus) return
|
|
1417
1670
|
|
|
1671
|
+
// Intercept ':' in raycast mode: when the search bar was empty and user typed ':',
|
|
1672
|
+
// enter command mode instead of searching. Clear the textarea back to empty.
|
|
1673
|
+
// Only triggers from truly empty state (not from editing "foo" to ":").
|
|
1674
|
+
const wasEmpty = searchText.length === 0
|
|
1675
|
+
if (inputMode === 'raycast' && wasEmpty && newValue === ':') {
|
|
1676
|
+
if (inputRef.current) {
|
|
1677
|
+
inputRef.current.clear()
|
|
1678
|
+
}
|
|
1679
|
+
useStore.setState({ vimInputSubMode: 'command', vimCommandText: '' })
|
|
1680
|
+
return
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1418
1683
|
// Always update internal state immediately so the textarea and filtering
|
|
1419
1684
|
// stay responsive even when throttle delays the parent callback
|
|
1420
1685
|
if (controlledSearchText === undefined) {
|
|
@@ -1506,7 +1771,7 @@ export const List: ListType = (props) => {
|
|
|
1506
1771
|
{ name: 'linefeed', action: 'submit' },
|
|
1507
1772
|
]}
|
|
1508
1773
|
placeholder={searchBarPlaceholder}
|
|
1509
|
-
focused={inFocus && !isDropdownOpen}
|
|
1774
|
+
focused={inFocus && !isDropdownOpen && searchBarFocused}
|
|
1510
1775
|
initialValue={searchText}
|
|
1511
1776
|
onContentChange={() => {
|
|
1512
1777
|
const value = inputRef.current?.plainText || ''
|
package/src/e2e-node.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as pty from 'node-pty'
|
|
1
|
+
import { spawn as zigSpawn } from 'zigpty'
|
|
4
2
|
import { Terminal } from '@xterm/headless'
|
|
5
3
|
import { SerializeAddon } from '@xterm/addon-serialize'
|
|
6
4
|
|
|
7
5
|
export class NodeTuiDriver {
|
|
8
|
-
private pty?:
|
|
6
|
+
private pty?: ReturnType<typeof zigSpawn>
|
|
9
7
|
private term: Terminal
|
|
10
8
|
private serialize: SerializeAddon
|
|
11
9
|
private cols: number
|
|
@@ -51,16 +49,18 @@ export class NodeTuiDriver {
|
|
|
51
49
|
TERM: 'xterm-truecolor',
|
|
52
50
|
COLORTERM: 'truecolor',
|
|
53
51
|
}
|
|
54
|
-
this.pty =
|
|
52
|
+
this.pty = zigSpawn(this.cmd, this.args, {
|
|
55
53
|
name: 'xterm-truecolor',
|
|
56
54
|
cols,
|
|
57
55
|
rows,
|
|
58
56
|
cwd,
|
|
59
|
-
env: envWithTerm as
|
|
57
|
+
env: envWithTerm as Record<string, string>,
|
|
60
58
|
})
|
|
61
59
|
|
|
62
60
|
this.pty.onData((data) => {
|
|
63
|
-
|
|
61
|
+
// zigpty onData can return string or Buffer
|
|
62
|
+
const str = typeof data === 'string' ? data : data.toString()
|
|
63
|
+
this.term.write(str)
|
|
64
64
|
clearTimeout(this.idleTimer)
|
|
65
65
|
this.idleTimer = setTimeout(() => {
|
|
66
66
|
const r = this.idleResolvers.splice(0)
|
|
@@ -57,7 +57,7 @@ test('ctrl+r shortcut should trigger Refresh action directly', async () => {
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
↵ refresh ↑↓ navigate ^k actions
|
|
60
|
+
↵ refresh ↑↓ navigate ^k actions :vim
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
|
|
@@ -97,11 +97,11 @@ test('action shortcut is displayed in action panel', async () => {
|
|
|
97
97
|
│ │
|
|
98
98
|
│ Settings │
|
|
99
99
|
│ Change Theme... │
|
|
100
|
+
│ Enable Vim Mode │
|
|
100
101
|
│ Toggle Console Logs │
|
|
101
102
|
│ │
|
|
102
103
|
│ │
|
|
103
104
|
│ │
|
|
104
|
-
│ │
|
|
105
105
|
│ ↵ select ↑↓ navigate │
|
|
106
106
|
│ │"
|
|
107
107
|
`)
|
|
@@ -74,44 +74,18 @@ test('many columns (20) clips with overflow hidden', async () => {
|
|
|
74
74
|
session.sendKey('down')
|
|
75
75
|
session.sendKey('down')
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
await session.text({
|
|
78
78
|
waitFor: (t) => t.includes('›Many Columns'),
|
|
79
79
|
timeout: 10000,
|
|
80
80
|
})
|
|
81
|
+
await session.waitIdle()
|
|
82
|
+
const text = await session.text()
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
> Search...
|
|
89
|
-
|
|
90
|
-
Weekly Traffic 3 channels across 6 d │ ███
|
|
91
|
-
Revenue by Region EMEA / APAC / Amer │ ███
|
|
92
|
-
Server Load CPU / Memory / IO │ ███ ███ ███ ███ ███
|
|
93
|
-
›Many Columns (20) Overflow test with │ ███ ███ ███ ███ ███ ███ ██
|
|
94
|
-
Many Series (8) Legend overflow test │ ███ ███ ███ ███ ███ ███ ███ ███ ██
|
|
95
|
-
Long Labels Labels wider than bar co │ ███ ███ ███ ███ ███ ███ ███ ███ ██
|
|
96
|
-
Week 1 vs Week 2 Two graphs in a Row │ ███ ███ ███ ███ ███ ███ ███ ███ ██
|
|
97
|
-
│ ███ ███ ███ ███ ███ ███ ███ ███ ██
|
|
98
|
-
│ ███ ███ ███ ███ ███ ███ ███ ███ ██
|
|
99
|
-
│ D1 D2 D3 D4 D5 D6 D7 D8 D9
|
|
100
|
-
↑↓ navigate ^k actions │ ■ A ■ B
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
`)
|
|
84
|
+
// Bar graph rendering has non-deterministic ANSI highlights, so use toContain checks
|
|
85
|
+
// instead of inline snapshot for the bars area
|
|
86
|
+
expect(text).toContain('›Many Columns')
|
|
87
|
+
expect(text).toContain('BarGraph Showcase')
|
|
88
|
+
expect(text).toContain('███')
|
|
115
89
|
|
|
116
90
|
// Some labels visible, overflow clips the rest
|
|
117
91
|
expect(text).toContain('D1')
|
|
@@ -149,7 +123,7 @@ test('many series (8) legend clips on one line', async () => {
|
|
|
149
123
|
│ ███ ███ ███ ███ ███ ███
|
|
150
124
|
│ ███ ███ ███ ███ ███ ███
|
|
151
125
|
│ Mon Tue Wed Thu Fri Sat
|
|
152
|
-
↑↓ navigate ^k actions
|
|
126
|
+
↑↓ navigate ^k actions :vim │ ■ Series 1 ■ Series 2 ■ Series 3 ■
|
|
153
127
|
|
|
154
128
|
|
|
155
129
|
|
|
@@ -202,7 +176,7 @@ test('long labels truncated by overflow hidden', async () => {
|
|
|
202
176
|
│ ███ ███ ███ ███ ███ ███
|
|
203
177
|
│ ███ ███ ███ ███ ███ ███
|
|
204
178
|
│ Mon Tue Wed Thu Fri Sat
|
|
205
|
-
↑↓ navigate ^k actions
|
|
179
|
+
↑↓ navigate ^k actions :vim │ ■ Views ■ Clicks
|
|
206
180
|
|
|
207
181
|
|
|
208
182
|
|