Skip to content

Commit c445f43

Browse files
feat: 第一个可以用的 ink 组件抽象 (#158)
1 parent 3ea64ee commit c445f43

645 files changed

Lines changed: 7255 additions & 1214 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"highlight.js": "^11.11.1",
132132
"https-proxy-agent": "^8.0.0",
133133
"ignore": "^7.0.5",
134+
"@anthropic/ink": "workspace:*",
134135
"image-processor-napi": "workspace:*",
135136
"indent-string": "^5.0.0",
136137
"jsonc-parser": "^3.3.1",

packages/@ant/ink/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@anthropic/ink",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"main": "./src/index.ts",
7+
"types": "./src/index.ts",
8+
"exports": {
9+
".": "./src/index.ts"
10+
},
11+
"dependencies": {
12+
"auto-bind": "^5.0.1",
13+
"bidi-js": "^1.0.3",
14+
"chalk": "^5.6.2",
15+
"cli-boxes": "^4.0.1",
16+
"emoji-regex": "^10.6.0",
17+
"figures": "^6.1.0",
18+
"get-east-asian-width": "^1.5.0",
19+
"indent-string": "^5.0.0",
20+
"lodash-es": "^4.17.23",
21+
"react": "^19.2.4",
22+
"react-reconciler": "^0.33.0",
23+
"signal-exit": "^4.1.0",
24+
"strip-ansi": "^7.2.0",
25+
"supports-hyperlinks": "^4.4.0",
26+
"type-fest": "^5.5.0",
27+
"usehooks-ts": "^3.1.1",
28+
"wrap-ansi": "^10.0.0"
29+
}
30+
}

src/ink/components/AlternateScreen.tsx renamed to packages/@ant/ink/src/components/AlternateScreen.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import React, {
33
useContext,
44
useInsertionEffect,
55
} from 'react'
6-
import instances from '../instances.js'
6+
import instances from '../core/instances.js'
77
import {
88
DISABLE_MOUSE_TRACKING,
99
ENABLE_MOUSE_TRACKING,
1010
ENTER_ALT_SCREEN,
1111
EXIT_ALT_SCREEN,
12-
} from '../termio/dec.js'
13-
import { TerminalWriteContext } from '../useTerminalNotification.js'
12+
} from '../core/termio/dec.js'
13+
import { TerminalWriteContext } from '../hooks/useTerminalNotification.js'
1414
import Box from './Box.js'
1515
import { TerminalSizeContext } from './TerminalSizeContext.js'
1616

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,70 @@
11
import React, { PureComponent, type ReactNode } from 'react'
2-
import { updateLastInteractionTime } from '../../bootstrap/state.js'
3-
import { logForDebugging } from '../../utils/debug.js'
4-
import { stopCapturingEarlyInput } from '../../utils/earlyInput.js'
5-
import { isEnvTruthy } from '../../utils/envUtils.js'
6-
import { isMouseClicksDisabled } from '../../utils/fullscreen.js'
7-
import { logError } from '../../utils/log.js'
8-
import { EventEmitter } from '../events/emitter.js'
9-
import { InputEvent } from '../events/input-event.js'
10-
import { TerminalFocusEvent } from '../events/terminal-focus-event.js'
2+
// Business-layer callbacks — replaced with inline defaults so this package
3+
// has zero dependencies on business code. The business layer can inject
4+
// implementations via AppCallbacks when needed.
5+
type AppCallbacks = {
6+
updateLastInteractionTime?: () => void
7+
stopCapturingEarlyInput?: () => void
8+
isMouseClicksDisabled?: () => boolean
9+
logError?: (error: unknown) => void
10+
logForDebugging?: (message: string, opts?: { level?: string }) => void
11+
}
12+
13+
/** Default no-op / safe-default implementations */
14+
const defaultCallbacks: Required<AppCallbacks> = {
15+
updateLastInteractionTime: () => {},
16+
stopCapturingEarlyInput: () => {},
17+
isMouseClicksDisabled: () => false,
18+
logError: (error: unknown) => console.error(error),
19+
logForDebugging: (_message: string, _opts?: { level?: string }) => {},
20+
}
21+
22+
/**
23+
* Override the default no-op callbacks. Call this from the business layer
24+
* (e.g. src/ink.tsx) before mounting <App>.
25+
*/
26+
export function setAppCallbacks(cb: AppCallbacks): void {
27+
Object.assign(defaultCallbacks, cb)
28+
}
29+
30+
function isEnvTruthy(value: string | undefined): boolean {
31+
return value === '1' || value === 'true'
32+
}
33+
import { EventEmitter } from '../core/events/emitter.js'
34+
import { InputEvent } from '../core/events/input-event.js'
35+
import { TerminalFocusEvent } from '../core/events/terminal-focus-event.js'
1136
import {
1237
INITIAL_STATE,
1338
type ParsedInput,
1439
type ParsedKey,
1540
type ParsedMouse,
1641
parseMultipleKeypresses,
17-
} from '../parse-keypress.js'
18-
import reconciler from '../reconciler.js'
42+
} from '../core/parse-keypress.js'
43+
import reconciler from '../core/reconciler.js'
1944
import {
2045
finishSelection,
2146
hasSelection,
2247
type SelectionState,
2348
startSelection,
24-
} from '../selection.js'
49+
} from '../core/selection.js'
2550
import {
2651
isXtermJs,
2752
setXtversionName,
2853
supportsExtendedKeys,
29-
} from '../terminal.js'
54+
} from '../core/terminal.js'
3055
import {
3156
getTerminalFocused,
3257
setTerminalFocused,
33-
} from '../terminal-focus-state.js'
34-
import { TerminalQuerier, xtversion } from '../terminal-querier.js'
58+
} from '../core/terminal-focus-state.js'
59+
import { TerminalQuerier, xtversion } from '../core/terminal-querier.js'
3560
import {
3661
DISABLE_KITTY_KEYBOARD,
3762
DISABLE_MODIFY_OTHER_KEYS,
3863
ENABLE_KITTY_KEYBOARD,
3964
ENABLE_MODIFY_OTHER_KEYS,
4065
FOCUS_IN,
4166
FOCUS_OUT,
42-
} from '../termio/csi.js'
67+
} from '../core/termio/csi.js'
4368
import {
4469
DBP,
4570
DFE,
@@ -48,7 +73,7 @@ import {
4873
EFE,
4974
HIDE_CURSOR,
5075
SHOW_CURSOR,
51-
} from '../termio/dec.js'
76+
} from '../core/termio/dec.js'
5277
import AppContext from './AppContext.js'
5378
import { ClockProvider } from './ClockContext.js'
5479
import CursorDeclarationContext, {
@@ -292,7 +317,7 @@ export default class App extends PureComponent<Props, State> {
292317
// Both use the same stdin 'readable' + read() pattern, so they can't
293318
// coexist -- our handler would drain stdin before Ink's can see it.
294319
// The buffered text is preserved for REPL.tsx via consumeEarlyInput().
295-
stopCapturingEarlyInput()
320+
defaultCallbacks.stopCapturingEarlyInput()
296321
stdin.ref()
297322
stdin.setRawMode(true)
298323
stdin.addListener('readable', this.handleReadable)
@@ -324,9 +349,9 @@ export default class App extends PureComponent<Props, State> {
324349
]).then(([r]) => {
325350
if (r) {
326351
setXtversionName(r.name)
327-
logForDebugging(`XTVERSION: terminal identified as "${r.name}"`)
352+
defaultCallbacks.logForDebugging(`XTVERSION: terminal identified as "${r.name}"`)
328353
} else {
329-
logForDebugging('XTVERSION: no reply (terminal ignored query)')
354+
defaultCallbacks.logForDebugging('XTVERSION: no reply (terminal ignored query)')
330355
}
331356
})
332357
})
@@ -436,7 +461,7 @@ export default class App extends PureComponent<Props, State> {
436461
// permanently wedge the stream: data stays buffered and 'readable'
437462
// never re-emits. Catching here ensures the stream stays healthy so
438463
// subsequent keystrokes are still delivered.
439-
logError(error)
464+
defaultCallbacks.logError(error)
440465

441466
// Re-attach the listener in case the exception detached it.
442467
// Bun may remove the listener after an error; without this,
@@ -446,7 +471,7 @@ export default class App extends PureComponent<Props, State> {
446471
this.rawModeEnabledCount > 0 &&
447472
!stdin.listeners('readable').includes(this.handleReadable)
448473
) {
449-
logForDebugging(
474+
defaultCallbacks.logForDebugging(
450475
'handleReadable: re-attaching stdin readable listener after error recovery',
451476
{ level: 'warn' },
452477
)
@@ -556,7 +581,7 @@ function processKeysInBatch(
556581
!((i.button & 0x20) !== 0 && (i.button & 0x03) === 3)),
557582
)
558583
) {
559-
updateLastInteractionTime()
584+
defaultCallbacks.updateLastInteractionTime()
560585
}
561586

562587
for (const item of items) {
@@ -625,7 +650,7 @@ function processKeysInBatch(
625650
export function handleMouseEvent(app: App, m: ParsedMouse): void {
626651
// Allow disabling click handling while keeping wheel scroll (which goes
627652
// through the keybinding system as 'wheelup'/'wheeldown', not here).
628-
if (isMouseClicksDisabled()) return
653+
if (defaultCallbacks.isMouseClicksDisabled()) return
629654

630655
const sel = app.props.selection
631656
// Terminal coords are 1-indexed; screen buffer is 0-indexed
File renamed without changes.
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { type PropsWithChildren, type Ref } from 'react'
22
import type { Except } from 'type-fest'
3-
import type { DOMElement } from '../dom.js'
4-
import type { ClickEvent } from '../events/click-event.js'
5-
import type { FocusEvent } from '../events/focus-event.js'
6-
import type { KeyboardEvent } from '../events/keyboard-event.js'
7-
import type { Styles } from '../styles.js'
8-
import * as warn from '../warn.js'
3+
import type { DOMElement } from '../core/dom.js'
4+
import type { ClickEvent } from '../core/events/click-event.js'
5+
import type { FocusEvent } from '../core/events/focus-event.js'
6+
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
7+
import type { Styles } from '../core/styles.js'
8+
import * as warn from '../core/warn.js'
99

1010
export type Props = Except<Styles, 'textWrap'> & {
1111
ref?: Ref<DOMElement>

src/ink/components/Button.tsx renamed to packages/@ant/ink/src/components/Button.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import React, {
66
useState,
77
} from 'react'
88
import type { Except } from 'type-fest'
9-
import type { DOMElement } from '../dom.js'
10-
import type { ClickEvent } from '../events/click-event.js'
11-
import type { FocusEvent } from '../events/focus-event.js'
12-
import type { KeyboardEvent } from '../events/keyboard-event.js'
13-
import type { Styles } from '../styles.js'
9+
import type { DOMElement } from '../core/dom.js'
10+
import type { ClickEvent } from '../core/events/click-event.js'
11+
import type { FocusEvent } from '../core/events/focus-event.js'
12+
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
13+
import type { Styles } from '../core/styles.js'
1414
import Box from './Box.js'
1515

1616
type ButtonState = {

src/ink/components/ClockContext.tsx renamed to packages/@ant/ink/src/components/ClockContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { createContext, useEffect, useState } from 'react'
2-
import { FRAME_INTERVAL_MS } from '../constants.js'
2+
import { FRAME_INTERVAL_MS } from '../core/constants.js'
33
import { useTerminalFocus } from '../hooks/use-terminal-focus.js'
44

55
export type Clock = {

src/ink/components/CursorDeclarationContext.ts renamed to packages/@ant/ink/src/components/CursorDeclarationContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext } from 'react'
2-
import type { DOMElement } from '../dom.js'
2+
import type { DOMElement } from '../core/dom.js'
33

44
export type CursorDeclaration = {
55
/** Display column (terminal cell width) within the declared node */

0 commit comments

Comments
 (0)