Files
claude-code/packages/@ant/ink/docs/07-user-input.md
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

5.8 KiB

Chapter 7: User Input

useInput

The primary hook for handling keyboard input.

import { useInput } from '@anthropic/ink'

function MyComponent() {
  useInput((input, key, event) => {
    if (input === 'q') {
      // 'q' key pressed
    }
    if (key.leftArrow) {
      // Left arrow
    }
    if (key.ctrl && input === 'c') {
      // Ctrl+C (only if exitOnCtrlC is false)
    }
    if (key.meta && input === 'b') {
      // Alt+B (Option+B on Mac)
    }
    if (key.shift && input === 'Tab') {
      // Shift+Tab
    }
  })

  return <Text>Press keys...</Text>
}

Signature

function useInput(
  handler: (input: string, key: Key, event: InputEvent) => void,
  options?: { isActive?: boolean }
): void

Parameters

  • input (string) -- The character entered. Empty string for non-printable keys (arrows, function keys). For paste events, the entire pasted text.
  • key (Key) -- Parsed key metadata (see below)
  • event (InputEvent) -- Raw event with stopImmediatePropagation()

Options

Option Type Default Description
isActive boolean true Enable/disable input handling

Key Object

type Key = {
  upArrow: boolean
  downArrow: boolean
  leftArrow: boolean
  rightArrow: boolean
  pageDown: boolean
  pageUp: boolean
  wheelUp: boolean       // Mouse wheel in alt-screen
  wheelDown: boolean      // Mouse wheel in alt-screen
  home: boolean
  end: boolean
  return: boolean
  escape: boolean
  ctrl: boolean
  shift: boolean
  fn: boolean
  tab: boolean
  backspace: boolean
  delete: boolean
  meta: boolean           // Alt / Option
  super: boolean          // Cmd (macOS) / Win key
}

Event Propagation

Multiple useInput handlers form a chain. Call event.stopImmediatePropagation() to prevent downstream handlers from receiving the event:

useInput((input, key, event) => {
  if (input === 'j') {
    // Consumed by this handler
    event.stopImmediatePropagation()
  }
  // Other handlers won't see 'j'
})

useInput((input, key) => {
  // This won't fire for 'j'
})

Raw Mode

useInput automatically enables raw mode on stdin when active. Raw mode is reference-counted -- it stays enabled as long as any hook has isActive: true.

In raw mode:

  • Keystrokes don't echo
  • Ctrl+C is not sent as signal (app must handle it)
  • Line buffering is disabled

InputEvent

class InputEvent extends Event {
  readonly input: string
  readonly key: Key
  readonly keypress: ParsedKey  // Raw parsed keypress data
}

KeyboardEvent

DOM-like keyboard event dispatched to focused elements:

class KeyboardEvent extends Event {
  readonly key: Key
}

Used with Box's onKeyDown and onKeyDownCapture props:

<Box
  tabIndex={0}
  autoFocus
  onKeyDown={(event) => {
    if (event.key.return) {
      handleSubmit()
    }
  }}
>
  <Text>Press Enter to submit</Text>
</Box>

Key Parsing

Ink supports multiple keyboard protocols:

Standard Escape Sequences

  • Arrow keys, function keys, Home/End, Page Up/Down
  • Ctrl+letter combinations
  • Shift, Alt, Meta modifiers

Kitty Keyboard Protocol (CSI u)

Extended key reporting with full modifier support:

  • Distinguishes Ctrl+Shift+A from Ctrl+A
  • Reports Super (Cmd/Win) key
  • Sends key release events

xterm modifyOtherKeys

Alternative extended key reporting for xterm-compatible terminals.

Application Keypad Mode

Numpad keys mapped to their digit characters.

Paste Detection

When Bracketed Paste mode is enabled (DECSET 2004), pasted text is delivered as a single InputEvent with the full text in input. This distinguishes paste from rapid typing:

useInput((input, key, event) => {
  if (event.keypress.paste) {
    // User pasted text -- handle as a batch
    handlePaste(input)
  } else {
    // Regular keypress
    handleKey(input, key)
  }
})

Mouse Events (Alt-Screen Only)

In alternate screen mode, mouse events are parsed and dispatched:

Click Events

<Box
  onClick={(event) => {
    console.log(`Clicked at (${event.x}, ${event.y})`)
    event.stopImmediatePropagation()
  }}
>
  <Text>Click me</Text>
</Box>

Hover Events

<Box
  onMouseEnter={() => setHovered(true)}
  onMouseLeave={() => setHovered(false)}
>
  <Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
</Box>

Hover events use mouseenter/mouseleave semantics (no bubbling between children).

Wheel Events

Mouse wheel events arrive as Key.wheelUp/Key.wheelDown:

useInput((input, key) => {
  if (key.wheelUp) scrollUp()
  if (key.wheelDown) scrollDown()
})

useStdin

Lower-level access to the stdin stream.

import { useStdin } from '@anthropic/ink'

const {
  stdin,                    // Raw stdin stream
  setRawMode,               // (enabled: boolean) => void
  isRawModeSupported,       // boolean
  internal_exitOnCtrlC,     // boolean
  internal_eventEmitter,    // EventEmitter | undefined
  internal_querier,         // Terminal querier
} = useStdin()

Prefer useInput for keyboard handling. useStdin is for advanced use cases like terminal querying or custom event handling.

Button Component

Interactive button that responds to keyboard and mouse:

import { Button } from '@anthropic/ink'

<Button onAction={() => handleClick()} tabIndex={0} autoFocus>
  {(state) => (
    <Text bold={state.focused} color={state.focused ? 'claude' : 'text'}>
      {state.focused ? '> Click Me' : '  Click Me'}
    </Text>
  )}
</Button>

Button receives a render prop with state:

type ButtonState = {
  focused: boolean    // Has keyboard focus
  hovered: boolean    // Mouse is over it (alt-screen)
  active: boolean     // True for 100ms after activation (flash effect)
}

Activation triggers: Enter key, Space key, or mouse click.