opencode-vim 0.0.23 → 0.0.24

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.
Files changed (38) hide show
  1. package/README.md +11 -0
  2. package/dist/tui.js +3326 -0
  3. package/package.json +6 -6
  4. package/src/clipboard.tsx +0 -50
  5. package/src/modules/snippets/index.tsx +0 -17
  6. package/src/modules/snippets/loader.ts +0 -147
  7. package/src/modules/snippets/search.ts +0 -118
  8. package/src/modules/snippets/skill-loader.ts +0 -81
  9. package/src/modules/snippets/state.ts +0 -40
  10. package/src/modules/snippets/trigger.ts +0 -68
  11. package/src/modules/snippets/types.ts +0 -50
  12. package/src/modules/snippets/view.tsx +0 -674
  13. package/src/modules/vim/actions.ts +0 -82
  14. package/src/modules/vim/config.ts +0 -137
  15. package/src/modules/vim/edit.test.ts +0 -51
  16. package/src/modules/vim/edit.ts +0 -33
  17. package/src/modules/vim/graphemes.ts +0 -38
  18. package/src/modules/vim/index.tsx +0 -200
  19. package/src/modules/vim/keys.ts +0 -38
  20. package/src/modules/vim/log.ts +0 -37
  21. package/src/modules/vim/map.test.ts +0 -45
  22. package/src/modules/vim/map.ts +0 -90
  23. package/src/modules/vim/state.test.ts +0 -18
  24. package/src/modules/vim/state.tsx +0 -46
  25. package/src/modules/vim/view.tsx +0 -130
  26. package/src/modules/vim/vimee.test.ts +0 -211
  27. package/src/modules/vim/vimee.ts +0 -1211
  28. package/src/plugin.tsx +0 -116
  29. package/src/prompt/host.tsx +0 -38
  30. package/src/prompt/modules.ts +0 -16
  31. package/src/prompt/root.tsx +0 -69
  32. package/src/prompt/types.ts +0 -42
  33. package/src/session.tsx +0 -291
  34. package/src/transcript-items.tsx +0 -168
  35. package/src/transcript.tsx +0 -179
  36. package/src/update.ts +0 -120
  37. package/tui.tsx +0 -340
  38. package/view.tsx +0 -29
package/src/plugin.tsx DELETED
@@ -1,116 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
- import { createSignal } from "solid-js"
4
- import { PromptRoot } from "./prompt/root"
5
- import type { PromptModule } from "./prompt/types"
6
- import { createVimModule } from "./modules/vim"
7
- import { createSnippetsModule } from "./modules/snippets"
8
- import type { SnippetController } from "./modules/snippets/types"
9
- import { checkAutoUpdate } from "./update"
10
-
11
- const tui: TuiPlugin = async (api, options, meta) => {
12
- const vimEnabledKey = `${meta.id}:enabled`
13
- const [vimEnabled, setVimEnabled] = createSignal(api.kv.get(vimEnabledKey, true) === true)
14
-
15
- api.keymap.registerLayer({
16
- commands: [
17
- {
18
- namespace: "palette",
19
- name: "opencode-vim.toggle",
20
- title: "Toggle Vim Mode",
21
- desc: "Enable or disable Vim key handling",
22
- category: "Vim",
23
- slashName: "vim",
24
- run() {
25
- const next = !vimEnabled()
26
- setVimEnabled(next)
27
- api.kv.set(vimEnabledKey, next)
28
- api.ui.toast({ variant: "info", message: `Vim mode ${next ? "enabled" : "disabled"}` })
29
- },
30
- },
31
- ],
32
- })
33
-
34
- if (readAutoUpdate(options)) {
35
- let timer: Timer | undefined
36
- api.lifecycle.onDispose(() => {
37
- if (timer) clearTimeout(timer)
38
- })
39
-
40
- void checkAutoUpdate(meta, api.lifecycle.signal).then((result) => {
41
- if (!result.updated) return
42
-
43
- timer = setTimeout(() => {
44
- api.ui.toast({ variant: "info", message: "Updating opencode-vim. Restart OpenCode to finish." })
45
- }, 5000)
46
- }).catch(() => {})
47
- }
48
-
49
- const moduleCache = new Map<string, PromptModule[]>()
50
- const createModules = (key: string) => {
51
- const cached = moduleCache.get(key)
52
- if (cached) return cached
53
-
54
- const modules: PromptModule[] = []
55
- const snippets: SnippetController = {}
56
- if (hasSnippetsPlugin(api)) modules.push(createSnippetsModule(snippets))
57
- modules.push(createVimModule(options, vimEnabled, snippets))
58
- moduleCache.set(key, modules)
59
- return modules
60
- }
61
-
62
- api.slots.register({
63
- order: 50,
64
- slots: {
65
- home_prompt(ctx, props) {
66
- return <PromptRoot api={api} slot={ctx} kind="home" workspaceID={props.workspace_id} ref={props.ref} modules={createModules(`home:${props.workspace_id ?? ""}`)} />
67
- },
68
- session_prompt(ctx, props) {
69
- return (
70
- <PromptRoot
71
- api={api}
72
- slot={ctx}
73
- kind="session"
74
- sessionID={props.session_id}
75
- visible={props.visible}
76
- disabled={props.disabled}
77
- onSubmit={props.on_submit}
78
- ref={props.ref}
79
- modules={createModules(`session:${props.session_id}`)}
80
- />
81
- )
82
- },
83
- },
84
- })
85
- }
86
-
87
- function readAutoUpdate(options: unknown) {
88
- if (!options || typeof options !== "object") return true
89
- const value = (options as { autoUpdate?: unknown }).autoUpdate
90
- return typeof value === "boolean" ? value : true
91
- }
92
-
93
- function hasSnippetsPlugin(api: TuiPluginApi) {
94
- return configHasSnippets(api.state.config.plugin) || configHasSnippets(api.tuiConfig.plugin) || api.plugins.list().some(pluginHasSnippets)
95
- }
96
-
97
- function configHasSnippets(plugins: unknown) {
98
- if (!Array.isArray(plugins)) return false
99
- return plugins.some((plugin) => specHasSnippets(Array.isArray(plugin) ? plugin[0] : plugin))
100
- }
101
-
102
- function pluginHasSnippets(plugin: ReturnType<TuiPluginApi["plugins"]["list"]>[number]) {
103
- return plugin.enabled && (specHasSnippets(plugin.id) || specHasSnippets(plugin.spec))
104
- }
105
-
106
- function specHasSnippets(spec: unknown) {
107
- if (typeof spec !== "string") return false
108
- return spec === "opencode-snippets" || spec.startsWith("opencode-snippets@") || spec.includes("/opencode-snippets")
109
- }
110
-
111
- const plugin: TuiPluginModule & { id: string } = {
112
- id: "local.opencode-vim",
113
- tui,
114
- }
115
-
116
- export default plugin
@@ -1,38 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPromptRef } from "@opencode-ai/plugin/tui"
3
- import type { JSX } from "@opentui/solid"
4
- import type { PromptContext, PromptModule } from "./types"
5
- import { submitModules } from "./modules"
6
-
7
- type HostPromptProps = {
8
- ctx: PromptContext
9
- modules: PromptModule[]
10
- ref?: (ref: TuiPromptRef | undefined) => void
11
- right?: JSX.Element
12
- }
13
-
14
- export function HostPrompt(props: HostPromptProps) {
15
- const api = props.ctx.api
16
-
17
- const setRef = (ref: TuiPromptRef | undefined) => {
18
- props.ctx.setPromptRef(ref)
19
- props.ref?.(ref)
20
- }
21
-
22
- const onSubmit = () => {
23
- if (submitModules(props.modules, props.ctx)) return
24
- props.ctx.submitHost()
25
- }
26
-
27
- return (
28
- <api.ui.Prompt
29
- sessionID={props.ctx.sessionID}
30
- workspaceID={props.ctx.workspaceID}
31
- visible={props.ctx.visible}
32
- disabled={props.ctx.disabled}
33
- onSubmit={onSubmit}
34
- ref={setRef}
35
- right={props.right}
36
- />
37
- )
38
- }
@@ -1,16 +0,0 @@
1
- import type { PromptContext, PromptModule } from "./types"
2
-
3
- export function sortModules(modules: PromptModule[]) {
4
- return [...modules].sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
5
- }
6
-
7
- export function submitModules(modules: PromptModule[], ctx: PromptContext) {
8
- for (const module of modules) {
9
- if (module.onSubmit?.(ctx) === true) return true
10
- }
11
- return false
12
- }
13
-
14
- export function notifyPromptRef(modules: PromptModule[], ref: ReturnType<PromptContext["prompt"]>, ctx: PromptContext) {
15
- for (const module of modules) module.onPromptRef?.(ref, ctx)
16
- }
@@ -1,69 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import { onCleanup } from "solid-js"
3
- import type { JSX } from "@opentui/solid"
4
- import { HostPrompt } from "./host"
5
- import { notifyPromptRef, sortModules } from "./modules"
6
- import type { PromptContext, PromptRootProps } from "./types"
7
-
8
- export function PromptRoot(props: PromptRootProps) {
9
- const modules = sortModules(props.modules)
10
- const api = props.api
11
- let current: ReturnType<PromptContext["prompt"]>
12
- const ctx: PromptContext = {
13
- api,
14
- slot: props.slot,
15
- kind: props.kind,
16
- get sessionID() {
17
- return props.sessionID
18
- },
19
- get workspaceID() {
20
- return props.workspaceID
21
- },
22
- get visible() {
23
- return props.visible
24
- },
25
- get disabled() {
26
- return props.disabled
27
- },
28
- prompt: () => current,
29
- setPromptRef(ref) {
30
- current = ref
31
- notifyPromptRef(modules, ref, ctx)
32
- },
33
- submitHost: () => props.onSubmit?.(),
34
- requestRender: () => api.renderer.requestRender(),
35
- }
36
-
37
- const cleanups = modules.flatMap((module) => {
38
- const cleanup = module.setup?.(ctx)
39
- return typeof cleanup === "function" ? [cleanup] : []
40
- })
41
- onCleanup(() => {
42
- for (const cleanup of cleanups) cleanup()
43
- })
44
-
45
- const moduleAbove = modules.map((module) => module.renderAbove?.(ctx)).filter(Boolean) as JSX.Element[]
46
- const moduleBelow = modules.map((module) => module.renderBelow?.(ctx)).filter(Boolean) as JSX.Element[]
47
- const moduleRight = modules.map((module) => module.renderRight?.(ctx)).filter(Boolean) as JSX.Element[]
48
- const hostRight = renderHostRight(props)
49
-
50
- return (
51
- <box>
52
- {moduleAbove}
53
- <HostPrompt ctx={ctx} modules={modules} ref={props.ref} right={<RightItems items={[...moduleRight, hostRight]} />} />
54
- {moduleBelow}
55
- </box>
56
- )
57
- }
58
-
59
- function renderHostRight(props: PromptRootProps) {
60
- const Slot = props.api.ui.Slot
61
- if (props.kind === "home") return <Slot name="home_prompt_right" workspace_id={props.workspaceID} />
62
- return <Slot name="session_prompt_right" session_id={props.sessionID ?? ""} />
63
- }
64
-
65
- function RightItems(props: { items: JSX.Element[] }) {
66
- const items = props.items.filter(Boolean)
67
- if (items.length === 0) return undefined
68
- return <box flexDirection="row">{items}</box>
69
- }
@@ -1,42 +0,0 @@
1
- import type { JSX } from "@opentui/solid"
2
- import type { TuiPluginApi, TuiPromptRef, TuiSlotContext } from "@opencode-ai/plugin/tui"
3
-
4
- export type PromptKind = "home" | "session"
5
-
6
- export type PromptContext = {
7
- api: TuiPluginApi
8
- slot: TuiSlotContext
9
- kind: PromptKind
10
- sessionID?: string
11
- workspaceID?: string
12
- visible?: boolean
13
- disabled?: boolean
14
- prompt: () => TuiPromptRef | undefined
15
- setPromptRef: (ref: TuiPromptRef | undefined) => void
16
- submitHost: () => void
17
- requestRender: () => void
18
- }
19
-
20
- export type PromptModule = {
21
- id: string
22
- order?: number
23
- setup?: (ctx: PromptContext) => void | (() => void)
24
- onPromptRef?: (ref: TuiPromptRef | undefined, ctx: PromptContext) => void
25
- onSubmit?: (ctx: PromptContext) => boolean | void
26
- renderAbove?: (ctx: PromptContext) => JSX.Element
27
- renderBelow?: (ctx: PromptContext) => JSX.Element
28
- renderRight?: (ctx: PromptContext) => JSX.Element
29
- }
30
-
31
- export type PromptRootProps = {
32
- api: TuiPluginApi
33
- slot: TuiSlotContext
34
- kind: PromptKind
35
- sessionID?: string
36
- workspaceID?: string
37
- visible?: boolean
38
- disabled?: boolean
39
- onSubmit?: () => void
40
- ref?: (ref: TuiPromptRef | undefined) => void
41
- modules: PromptModule[]
42
- }
package/src/session.tsx DELETED
@@ -1,291 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { Context } from "@opencode/plugin/tui/context"
3
- import { type KeyEvent, type TextareaRenderable } from "@opentui/core"
4
- import { useTerminalDimensions } from "@opentui/solid"
5
- import { createEffect, createSignal, onCleanup, onMount, untrack } from "solid-js"
6
- import type { PromptContext } from "./modules/vim/actions"
7
- import type { VimConfig } from "./modules/vim/config"
8
- import { keyNotation } from "./modules/vim/keys"
9
- import { displayWidth } from "./modules/vim/map"
10
- import { createVimState } from "./modules/vim/state"
11
- import { createVimeeAdapter, YANK_FLASH_MS } from "./modules/vim/vimee"
12
- import { createTranscriptSelection } from "./transcript"
13
- import type { TranscriptItem } from "./transcript-items"
14
- import type { VimClipboard } from "./clipboard"
15
-
16
- export const SESSION_MODE = "opencode-vim.session"
17
-
18
- export function createSessionMode(context: Context, config: VimConfig, clipboard: VimClipboard) {
19
- const [active, setActive] = createSignal(false)
20
- const [reading, setReading] = createSignal<TranscriptItem>()
21
- const [notice, setNotice] = createSignal("")
22
- const positions = new Map<string, number>()
23
- let sessionID = ""
24
- let selectedID: string | undefined
25
- let prompt: Context["renderer"]["currentFocusedEditor"] = null
26
- let transcript: ReturnType<typeof createTranscriptSelection> | undefined
27
- let popMode: (() => void) | undefined
28
- let prefix = ""
29
- let count = ""
30
- let disposed = false
31
- let noticeTimer: ReturnType<typeof setTimeout> | undefined
32
- let yankTimer: ReturnType<typeof setTimeout> | undefined
33
- let yankID: string | undefined
34
-
35
- function selected() {
36
- return transcript?.get(selectedID)
37
- }
38
-
39
- function clearYankFlash() {
40
- if (yankTimer) clearTimeout(yankTimer)
41
- yankTimer = undefined
42
- if (!yankID) return
43
- yankID = undefined
44
- context.renderer.requestRender()
45
- }
46
-
47
- function close() {
48
- if (!active()) return
49
- clearYankFlash()
50
- const wasReading = Boolean(reading())
51
- if (wasReading) context.ui.dialog.clear()
52
- setActive(false)
53
- context.renderer.removePostProcessFn(draw)
54
- popMode?.()
55
- popMode = undefined
56
- transcript = undefined
57
- const savedPrompt = prompt
58
- const savedSessionID = sessionID
59
- function restorePrompt() {
60
- const route = context.ui.router.current()
61
- if (!active() && context.keymap.mode.current() === "base" && route.type === "session"
62
- && route.sessionID === savedSessionID && savedPrompt && !savedPrompt.isDestroyed) savedPrompt.focus()
63
- }
64
- restorePrompt()
65
- // Restore after the dialog's deferred refocus, or it steals focus back.
66
- if (wasReading) setTimeout(restorePrompt, 1)
67
- prompt = null
68
- context.renderer.requestRender()
69
- }
70
-
71
- function enter() {
72
- const route = context.ui.router.current()
73
- if (route.type !== "session") return false
74
- sessionID = route.sessionID
75
- prompt = context.renderer.currentFocusedEditor
76
- prefix = count = ""
77
- positions.clear()
78
- setNotice("")
79
- popMode = context.keymap.mode.push(SESSION_MODE)
80
- transcript = createTranscriptSelection(context, sessionID, (id) => {
81
- selectedID = id
82
- })
83
- setActive(true)
84
- prompt?.blur()
85
- transcript.focus()
86
- transcript.latest()
87
- context.renderer.addPostProcessFn(draw)
88
- context.renderer.requestRender()
89
- return true
90
- }
91
-
92
- const draw: Parameters<Context["renderer"]["addPostProcessFn"]>[0] = (buffer) => {
93
- if (active() && !reading() && context.keymap.mode.current() === SESSION_MODE) transcript?.draw(buffer, yankID)
94
- }
95
-
96
- async function copy(text: string) {
97
- const copied = await clipboard.write(text)
98
- if (disposed) return
99
- if (noticeTimer) clearTimeout(noticeTimer)
100
- setNotice(copied ? "Copied" : "Yanked · clipboard unavailable")
101
- noticeTimer = setTimeout(() => setNotice(""), 1500)
102
- }
103
-
104
- function openMessage(message: TranscriptItem) {
105
- transcript?.focus()
106
- setReading(message)
107
- context.ui.dialog.show(() => (
108
- <MessageReader context={context} config={config} message={message} offset={positions.get(message.id) ?? 0}
109
- copy={copy} notice={notice} remember={(offset) => positions.set(message.id, offset)}
110
- back={() => context.ui.dialog.clear()} close={close} />
111
- ), () => {
112
- setReading(undefined)
113
- setNotice("")
114
- })
115
- }
116
-
117
- const onKey = (event: KeyEvent) => {
118
- if (!active() || reading()) return
119
- const key = readerKey(context, event, config.sessionKey)
120
- if (!key) return
121
- event.preventDefault()
122
- event.stopPropagation()
123
- clearYankFlash()
124
- setNotice("")
125
- if (key === config.sessionKey || key === "<Esc>" || key === "<C-[>") { close(); return }
126
- if (/^[0-9]$/.test(key) && (count || key !== "0")) { count = (count + key).slice(0, 6); return }
127
- const amount = Number(count) || 1
128
- count = ""
129
- const previous = prefix
130
- prefix = ""
131
- let command = pageCommand(key)
132
- if (key === "j" || key === "<Down>") { transcript?.move("next", amount); return }
133
- else if (key === "k" || key === "<Up>") { transcript?.move("previous", amount); return }
134
- else if (key === "G") { transcript?.latest(); return }
135
- else if (key === "g" && previous === "g") command = "session.first"
136
- else if (key === "y" && previous === "y") {
137
- const message = selected()
138
- if (message) {
139
- yankID = message.id
140
- yankTimer = setTimeout(clearYankFlash, YANK_FLASH_MS)
141
- context.renderer.requestRender()
142
- void copy(message.text)
143
- }
144
- } else if (key === "g" || key === "y") prefix = key
145
- else if (key === "<CR>") {
146
- transcript?.sync()
147
- if (transcript?.toggle()) return
148
- const message = selected()
149
- if (message) openMessage(message)
150
- }
151
- if (command) {
152
- transcript?.followViewport()
153
- for (let index = 0; index < amount; index++) context.keymap.dispatch(command)
154
- }
155
- }
156
-
157
- createEffect(() => {
158
- const route = context.ui.router.current()
159
- if (active() && (route.type !== "session" || route.sessionID !== sessionID)) untrack(close)
160
- })
161
- onMount(() => context.renderer.keyInput.prependListener("keypress", onKey))
162
- onCleanup(() => {
163
- disposed = true
164
- close()
165
- if (noticeTimer) clearTimeout(noticeTimer)
166
- context.renderer.keyInput.off("keypress", onKey)
167
- })
168
-
169
- return {
170
- active, enter, close,
171
- Status() {
172
- return <text fg={context.theme.text.feedback.info.base}>
173
- {notice() || `SESSION · Enter open · yy copy · ${config.sessionKey} prompt`}
174
- </text>
175
- },
176
- }
177
- }
178
-
179
- function MessageReader(props: {
180
- context: Context
181
- config: VimConfig
182
- message: TranscriptItem
183
- offset: number
184
- copy: (text: string) => void
185
- notice: () => string
186
- remember: (offset: number) => void
187
- back: () => void
188
- close: () => void
189
- }) {
190
- const context = props.context
191
- context.ui.dialog.set({ size: "large", centered: true })
192
- const dimensions = useTerminalDimensions()
193
- const theme = () => context.theme.surface("dialog")
194
- const background = () => theme().background.raised.high
195
- const state = createVimState("normal")
196
- const [line, setLine] = createSignal(1)
197
- let input!: TextareaRenderable
198
- const adapter = createVimeeAdapter(state, props.config, () => {}, { readOnly: true, onYank: props.copy })
199
- const editorContext: PromptContext = {
200
- api: {
201
- renderer: { get currentFocusedRenderable() { return input } },
202
- keymap: { dispatchCommand: () => ({ ok: false }) },
203
- theme: { get current() { return {
204
- warning: theme().text.feedback.warning.base,
205
- info: theme().text.feedback.info.base,
206
- background: background(),
207
- } } },
208
- },
209
- prompt: () => ({
210
- get current() { return { input: input.plainText, mode: "normal", parts: [] } },
211
- set() {}, submit() {}, blur() {},
212
- }),
213
- requestRender: () => context.renderer.requestRender(),
214
- }
215
- const onKey = (event: KeyEvent) => {
216
- if (context.renderer.currentFocusedEditor !== input) return
217
- const key = readerKey(context, event, props.config.sessionKey, "modal")
218
- if (!key) return
219
- event.preventDefault()
220
- event.stopPropagation()
221
- if (key === props.config.sessionKey && state.mode() === "normal" && !adapter.isPending()) { props.close(); return }
222
- // The host's Ctrl+C clears the editor unless we intercept it.
223
- if (key === "<C-c>") { props.back(); return }
224
- if ((key === "<Esc>" || key === "<C-[>") && state.mode() === "normal" && !adapter.isPending()) {
225
- props.back()
226
- return
227
- }
228
- if (pageCommand(key)) {
229
- const down = key === "<C-d>" || key === "<C-f>" || key === "<PageDown>"
230
- const half = key === "<C-d>" || key === "<C-u>"
231
- const rows = Math.max(1, Math.floor(input.height / (half ? 2 : 1)))
232
- for (let row = 0; row < rows; row++) adapter.handle({ ...event, ctrl: false } as KeyEvent, down ? "j" : "k", editorContext)
233
- } else adapter.handle(event, key, editorContext)
234
- setLine(input.logicalCursor.row + 1)
235
- }
236
- onMount(() => {
237
- input.cursorOffset = Math.min(props.offset, displayWidth(input.plainText))
238
- adapter.attach(editorContext)
239
- setLine(input.logicalCursor.row + 1)
240
- input.focus()
241
- context.renderer.keyInput.prependListener("keypress", onKey)
242
- })
243
- onCleanup(() => {
244
- props.remember(input.cursorOffset)
245
- context.renderer.keyInput.off("keypress", onKey)
246
- adapter.cleanup()
247
- })
248
-
249
- function modeLabel() {
250
- if (state.mode() === "visual") return "VISUAL"
251
- if (state.mode() === "visual-line") return "VISUAL LINE"
252
- return "MESSAGE"
253
- }
254
- return (
255
- <box id="vim-message-reader" gap={1}>
256
- <box paddingLeft={2} paddingRight={2} flexDirection="row" justifyContent="space-between">
257
- <text fg={theme().text.base}><b>{props.message.author}</b></text>
258
- <text id="vim-message-close" fg={theme().text.muted} onMouseUp={props.back}>esc</text>
259
- </box>
260
- <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} backgroundColor={background()}>
261
- <textarea id="vim-session-message" ref={(value: TextareaRenderable) => {
262
- input = value
263
- input.handleKeyPress = () => true
264
- input.handlePaste = () => {}
265
- }} initialValue={props.message.text} minHeight={1} maxHeight={Math.max(1, Math.min(20, dimensions().height - 10))}
266
- wrapMode="word" showCursor cursorStyle={props.config.cursorStyles.normal} textColor={theme().text.base}
267
- backgroundColor={background()} focusedBackgroundColor={background()} focusedTextColor={theme().text.base} />
268
- </box>
269
- <box paddingLeft={2} paddingRight={2} paddingBottom={1} flexDirection="row" flexWrap="wrap" columnGap={3}>
270
- <text fg={theme().text.muted}>{modeLabel()} · {line()}/{input?.lineCount ?? 1}</text>
271
- <text fg={theme().text.muted}>
272
- {props.notice() || (state.mode() !== "normal" ? "y copy · Esc cancel" : `v select · V lines · ${props.config.sessionKey} prompt`)}
273
- </text>
274
- </box>
275
- </box>
276
- )
277
- }
278
-
279
- function readerKey(context: Context, event: KeyEvent, sessionKey: string, mode = SESSION_MODE) {
280
- if (event.defaultPrevented || context.keymap.mode.current() !== mode) return
281
- if (context.keymap.pending().length || event.super || event.meta) return
282
- const key = keyNotation(event)
283
- if (key && (key === sessionKey || !event.ctrl || key === "<C-[>" || pageCommand(key) || (mode === "modal" && key === "<C-c>"))) return key
284
- }
285
-
286
- function pageCommand(key: string) {
287
- if (key === "<C-d>") return "session.half.page.down"
288
- if (key === "<C-u>") return "session.half.page.up"
289
- if (key === "<C-f>" || key === "<PageDown>") return "session.page.down"
290
- if (key === "<C-b>" || key === "<PageUp>") return "session.page.up"
291
- }