chenbhao Claude Opus 4.6 commited on
Commit
f73174f
·
1 Parent(s): 7e3630c

feat: add useStdout and useIsScreenReaderEnabled hooks for ink-picture

Browse files

- Add StdoutContext.Provider and AccessibilityContext.Provider to App render tree
- Export useStdout/useIsScreenReaderEnabled from src/ink entry point
- Pass writeToStdout from Ink class to App for raw escape sequence output
- Create LocalPicture.test.tsx visual test for local image display

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

src/ink-picture/__tests__/LocalImageShow.test.ts DELETED
File without changes
src/ink-picture/__tests__/LocalPicture.test.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Visual test: display a local image using ink-picture + Ink.
4
+ *
5
+ * Usage:
6
+ * bun run src/ink-picture/__tests__/LocalPicture.test.tsx
7
+ */
8
+
9
+ import React, { useEffect } from 'react'
10
+ import { render, Box, useApp } from 'ink'
11
+ import Image, { InkPictureProvider } from '../index.ts'
12
+
13
+ const IMAGE_PATH = '/home/yuki/Pictures/Wallpapers/3god.jpg'
14
+
15
+ const cols = process.stdout.columns ?? 80
16
+ const rows = process.stdout.rows ?? 40
17
+ const imgWidth = Math.floor(cols * 0.6)
18
+ const imgHeight = Math.floor(rows * 0.4)
19
+
20
+ function App() {
21
+ const { exit } = useApp()
22
+
23
+ useEffect(() => {
24
+ const timer = setTimeout(() => exit(), 3000)
25
+ return () => clearTimeout(timer)
26
+ }, [exit])
27
+
28
+ return (
29
+ <Box flexDirection="column">
30
+ <InkPictureProvider>
31
+ <Image src={IMAGE_PATH} width={imgWidth} height={imgHeight} alt="3god" />
32
+ </InkPictureProvider>
33
+ </Box>
34
+ )
35
+ }
36
+
37
+ const { waitUntilExit } = render(<App />)
38
+ await waitUntilExit()
src/ink.ts CHANGED
@@ -76,6 +76,8 @@ export { default as useInput } from './ink/hooks/use-input.js'
76
  export { useAnimationTimer, useInterval } from './ink/hooks/use-interval.js'
77
  export { useSelection } from './ink/hooks/use-selection.js'
78
  export { default as useStdin } from './ink/hooks/use-stdin.js'
 
 
79
  export { useTabStatus } from './ink/hooks/use-tab-status.js'
80
  export { useTerminalFocus } from './ink/hooks/use-terminal-focus.js'
81
  export { useTerminalTitle } from './ink/hooks/use-terminal-title.js'
 
76
  export { useAnimationTimer, useInterval } from './ink/hooks/use-interval.js'
77
  export { useSelection } from './ink/hooks/use-selection.js'
78
  export { default as useStdin } from './ink/hooks/use-stdin.js'
79
+ export { default as useStdout } from './ink/hooks/use-stdout.js'
80
+ export { default as useIsScreenReaderEnabled } from './ink/hooks/use-is-screen-reader-enabled.js'
81
  export { useTabStatus } from './ink/hooks/use-tab-status.js'
82
  export { useTerminalFocus } from './ink/hooks/use-terminal-focus.js'
83
  export { useTerminalTitle } from './ink/hooks/use-terminal-title.js'
src/ink/components/AccessibilityContext.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext } from 'react'
2
+
3
+ type AccessibilityContextValue = {
4
+ readonly isScreenReaderEnabled: boolean
5
+ }
6
+
7
+ const AccessibilityContext = createContext<AccessibilityContextValue>({
8
+ isScreenReaderEnabled: false,
9
+ })
10
+
11
+ AccessibilityContext.displayName = 'AccessibilityContext'
12
+
13
+ export default AccessibilityContext
14
+
15
+ export function useAccessibility(): AccessibilityContextValue {
16
+ return useContext(AccessibilityContext)
17
+ }
src/ink/components/App.tsx CHANGED
@@ -21,6 +21,8 @@ import { ClockProvider } from './ClockContext.js';
21
  import CursorDeclarationContext, { type CursorDeclarationSetter } from './CursorDeclarationContext.js';
22
  import ErrorOverview from './ErrorOverview.js';
23
  import StdinContext from './StdinContext.js';
 
 
24
  import { TerminalFocusProvider } from './TerminalFocusContext.js';
25
  import { TerminalSizeContext } from './TerminalSizeContext.js';
26
 
@@ -38,6 +40,7 @@ type Props = {
38
  readonly stdin: NodeJS.ReadStream;
39
  readonly stdout: NodeJS.WriteStream;
40
  readonly stderr: NodeJS.WriteStream;
 
41
  readonly exitOnCtrlC: boolean;
42
  readonly onExit: (error?: Error) => void;
43
  readonly terminalColumns: number;
@@ -170,7 +173,11 @@ export default class App extends PureComponent<Props, State> {
170
  <TerminalFocusProvider>
171
  <ClockProvider>
172
  <CursorDeclarationContext.Provider value={this.props.onCursorDeclaration ?? (() => {})}>
173
- {this.state.error ? <ErrorOverview error={this.state.error as Error} /> : this.props.children}
 
 
 
 
174
  </CursorDeclarationContext.Provider>
175
  </ClockProvider>
176
  </TerminalFocusProvider>
 
21
  import CursorDeclarationContext, { type CursorDeclarationSetter } from './CursorDeclarationContext.js';
22
  import ErrorOverview from './ErrorOverview.js';
23
  import StdinContext from './StdinContext.js';
24
+ import StdoutContext from './StdoutContext.js';
25
+ import AccessibilityContext from './AccessibilityContext.js';
26
  import { TerminalFocusProvider } from './TerminalFocusContext.js';
27
  import { TerminalSizeContext } from './TerminalSizeContext.js';
28
 
 
40
  readonly stdin: NodeJS.ReadStream;
41
  readonly stdout: NodeJS.WriteStream;
42
  readonly stderr: NodeJS.WriteStream;
43
+ readonly writeToStdout: (data: string) => void;
44
  readonly exitOnCtrlC: boolean;
45
  readonly onExit: (error?: Error) => void;
46
  readonly terminalColumns: number;
 
173
  <TerminalFocusProvider>
174
  <ClockProvider>
175
  <CursorDeclarationContext.Provider value={this.props.onCursorDeclaration ?? (() => {})}>
176
+ <StdoutContext.Provider value={{ stdout: this.props.stdout, write: this.props.writeToStdout }}>
177
+ <AccessibilityContext.Provider value={{ isScreenReaderEnabled: false }}>
178
+ {this.state.error ? <ErrorOverview error={this.state.error as Error} /> : this.props.children}
179
+ </AccessibilityContext.Provider>
180
+ </StdoutContext.Provider>
181
  </CursorDeclarationContext.Provider>
182
  </ClockProvider>
183
  </TerminalFocusProvider>
src/ink/components/StdoutContext.tsx CHANGED
@@ -2,6 +2,11 @@ import { createContext, useContext } from 'react'
2
 
3
  export type Props = {
4
  readonly stdout: NodeJS.WriteStream & { readonly rows: number; readonly columns: number }
 
 
 
 
 
5
  readonly write: (data: string) => void
6
  }
7
 
@@ -12,4 +17,8 @@ const StdoutContext = createContext<Props>({
12
 
13
  StdoutContext.displayName = 'StdoutContext'
14
 
15
- export default StdoutContext
 
 
 
 
 
2
 
3
  export type Props = {
4
  readonly stdout: NodeJS.WriteStream & { readonly rows: number; readonly columns: number }
5
+ /**
6
+ * Write any string to stdout while preserving Ink's output.
7
+ * Useful for emitting raw escape sequences (e.g. image protocols)
8
+ * outside of Ink's normal render loop.
9
+ */
10
  readonly write: (data: string) => void
11
  }
12
 
 
17
 
18
  StdoutContext.displayName = 'StdoutContext'
19
 
20
+ export default StdoutContext
21
+
22
+ export function useStdout(): Props {
23
+ return useContext(StdoutContext)
24
+ }
src/ink/hooks/use-is-screen-reader-enabled.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useContext } from 'react'
2
+ import { useAccessibility } from '../components/AccessibilityContext.js'
3
+
4
+ /**
5
+ * Hook that returns whether a screen reader is enabled.
6
+ * Useful when components need to render different output for screen readers.
7
+ */
8
+ export default function useIsScreenReaderEnabled(): boolean {
9
+ const { isScreenReaderEnabled } = useAccessibility()
10
+ return isScreenReaderEnabled
11
+ }
src/ink/hooks/use-stdout.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useContext } from 'react'
2
+ import StdoutContext from '../components/StdoutContext.js'
3
+
4
+ /**
5
+ * A React hook that returns the stdout stream where Ink renders your app,
6
+ * plus a `write` function for emitting raw data to stdout outside of Ink's
7
+ * normal render loop (e.g. for inline image protocols).
8
+ */
9
+ export default function useStdout() {
10
+ return useContext(StdoutContext)
11
+ }
src/ink/ink.tsx CHANGED
@@ -1441,7 +1441,7 @@ export default class Ink {
1441
  };
1442
  render(node: ReactNode): void {
1443
  this.currentNode = node;
1444
- const tree = <App stdin={this.options.stdin} stdout={this.options.stdout} stderr={this.options.stderr} exitOnCtrlC={this.options.exitOnCtrlC} onExit={this.unmount} terminalColumns={this.terminalColumns} terminalRows={this.terminalRows} selection={this.selection} onSelectionChange={this.notifySelectionChange} onClickAt={this.dispatchClick} onHoverAt={this.dispatchHover} getHyperlinkAt={this.getHyperlinkAt} onOpenHyperlink={this.openHyperlink} onMultiClick={this.handleMultiClick} onSelectionDrag={this.handleSelectionDrag} onStdinResume={this.reassertTerminalModes} onCursorDeclaration={this.setCursorDeclaration} dispatchKeyboardEvent={this.dispatchKeyboardEvent}>
1445
  <TerminalWriteProvider value={this.writeRaw}>
1446
  {node}
1447
  </TerminalWriteProvider>
 
1441
  };
1442
  render(node: ReactNode): void {
1443
  this.currentNode = node;
1444
+ const tree = <App stdin={this.options.stdin} stdout={this.options.stdout} stderr={this.options.stderr} writeToStdout={this.writeRaw} exitOnCtrlC={this.options.exitOnCtrlC} onExit={this.unmount} terminalColumns={this.terminalColumns} terminalRows={this.terminalRows} selection={this.selection} onSelectionChange={this.notifySelectionChange} onClickAt={this.dispatchClick} onHoverAt={this.dispatchHover} getHyperlinkAt={this.getHyperlinkAt} onOpenHyperlink={this.openHyperlink} onMultiClick={this.handleMultiClick} onSelectionDrag={this.handleSelectionDrag} onStdinResume={this.reassertTerminalModes} onCursorDeclaration={this.setCursorDeclaration} dispatchKeyboardEvent={this.dispatchKeyboardEvent}>
1445
  <TerminalWriteProvider value={this.writeRaw}>
1446
  {node}
1447
  </TerminalWriteProvider>