Skip to content

Commit fc82a4f

Browse files
authored
fix: more concrete example without character output spam (#32)
* fix: fix types in demo and provide more concrete example * update example
1 parent 06141ab commit fc82a4f

File tree

13 files changed

+220
-45
lines changed

13 files changed

+220
-45
lines changed

.mise.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ nodejs="20.12.1"
77
yarn="1.22.19"
88
shellcheck="0.9.0"
99
jq="1.7.1"
10+
usage = "latest"

.mise/tasks/dev

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
#!/usr/bin/env bash
2-
# mise description="Run dev server with watch mode"
2+
3+
#MISE description="Run dev server with watch mode"
4+
5+
#USAGE flag "-e --example <example>" "Specify example to run" default="main"
6+
7+
# verify that src/demo/${usage_example}.tsx exists
8+
if [ ! -f ./src/demo/"${usage_example}".tsx ]; then
9+
echo "Error: Example '${usage_example}' does not exist in src/demo/"
10+
11+
# list all files in src/demo
12+
echo "Available examples:"
13+
ls ./src/demo/*.tsx | xargs -n 1 basename | sed 's/\.tsx$//'
14+
15+
exit 1
16+
fi
317

418
tsx \
519
--watch \
6-
./src/demo/index.tsx
20+
./src/demo/"${usage_example}".tsx

.yarn/install-state.gz

2.15 KB
Binary file not shown.

README.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ https://github.com/zenobi-us/ink-mouse/assets/61225/658ad469-6438-4bff-8695-e2fe
1616

1717
## Usage
1818

19+
> [!NOTE]
20+
> Without `useInput`, escape sequence characters will being printed to the terminal during mouse movements.
21+
1922
```tsx
2023
import React, { useMemo, useRef, useState } from 'react';
2124
import type { ComponentProps } from 'react';
22-
import { Box, DOMElement, Text, render } from 'ink';
25+
import { Box, DOMElement, Text, render, useInput } from 'ink';
2326
import {
2427
MouseProvider,
2528
useOnMouseHover,
2629
useMousePosition,
27-
useElementPosition,
2830
useOnMouseClick,
31+
useMouse,
2932
} from '@zenobius/ink-mouse';
33+
import { useMap } from '@react-hookz/web';
3034

3135
function App() {
3236
return (
@@ -37,20 +41,34 @@ function App() {
3741
}
3842

3943
function MyComponent() {
40-
const mouse = useMousePosition();
44+
const mouse = useMouse();
45+
const mousePosition = useMousePosition();
46+
const map = useMap<'button1', number>() // Example of a simple state map
47+
48+
/**
49+
* Without this, your terminal will fill up with escape codes when you move the mouse.
50+
*/
51+
useInput((input, key) => {
52+
if (key.return) {
53+
mouse.toggle()
54+
}
55+
});
4156

4257
return (
43-
<Box>
44-
<Button label="Button 1" />
45-
<Text>
46-
{mouse.x}, {mouse.y}
47-
</Text>
58+
<Box gap={1} flexDirection='column' width={40} height={10} borderStyle="round" padding={1}>
59+
<Box gap={1}>
60+
<Button label="Button 1" onClick={() => map.set('button1', (map.get('button1') || 0) + 1)} />
61+
</Box>
62+
<Box flexDirection="column" gap={1}>
63+
<Text>{JSON.stringify(mousePosition)}</Text>
64+
<Text>Button 1 clicked: {map.get('button1') || 0} times</Text>
65+
</Box>
4866
</Box>
4967
);
5068
}
5169

5270
function Button({ label, onClick }: { label: string; onClick?: () => void }) {
53-
const ref = useRef<DOMElement>(null);
71+
const ref = useRef<DOMElement | null>(null);
5472

5573
const [hovering, setHovering] = useState(false);
5674
const [clicking, setClicking] = useState(false);
@@ -117,6 +135,10 @@ All project tasks are managed via Mise in `.mise/tasks/`, you can list them via
117135
- Elements positioned absolutely that occupy same space as other elements will mean they both recieve click and hover events.
118136
- Ink supports absolute positioning. I think z order is based on order rendered.
119137
- This means to simluate knowing the z order, we might need to register the order in which elements subscribe to events?
138+
- [ ] Support elements not rendered from 0,0
139+
- Currently the mouse position is tracked from the top left of the terminal (0,0).
140+
- If an element is rendered starting at (10,10) for example, the mouse position will not be accurate.
141+
- We need to track the offset of the element and adjust the mouse position accordingly.
120142
- [ ] Add tests.
121143
- testing a device may be difficult; but the implementation is sufficiently abstracted from the device that it should be possible to mock the device input stream.
122144
- [x] stdin event stream parsing

src/demo/index.tsx renamed to src/demo/main.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
useOnMouseClick,
1313
useElementPosition,
1414
useElementDimensions,
15-
useMouse
16-
} from '../../src/ink-mouse.ts';
15+
useMouse,
16+
useTerminalSize
17+
} from '../ink-mouse.ts';
1718

1819
function App() {
1920
return (
@@ -22,33 +23,7 @@ function App() {
2223
</MouseProvider>
2324
);
2425
}
25-
function useTerminalSize() {
26-
const out = useStdout();
27-
const [size, setSize] = useState(() => {
28-
return {
29-
width: out.stdout.columns,
30-
height: out.stdout.rows,
31-
};
32-
});
33-
34-
useEffect(() => {
35-
const handleTerminalResize = () => {
36-
setSize(() => ({
37-
width: out.stdout.columns,
38-
height: out.stdout.rows,
39-
}));
40-
};
4126

42-
process.stdout.on('resize', handleTerminalResize);
43-
process.stdout.on('SIGWINCH', handleTerminalResize);
44-
return () => {
45-
process.stdout.off('SIGWINCH', handleTerminalResize);
46-
process.stdout.off('resize', handleTerminalResize);
47-
};
48-
}, []);
49-
50-
return size;
51-
}
5227

5328
function View() {
5429
const map = useMap<'button1' | 'button2' | 'button3' | 'listitem1' | 'listitem2', number>()
@@ -111,6 +86,7 @@ function View() {
11186
</Box>
11287
</Box>
11388
</Button>
89+
11490
<Button onClick={() => {
11591
map.set('listitem2', (map.get('listitem2') || 0) + 1)
11692
}}>

src/demo/usage.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
import React, { useMemo, useRef, useState } from 'react';
3+
import type { ComponentProps } from 'react';
4+
import { Box, DOMElement, Text, render, useInput } from 'ink';
5+
import {
6+
MouseProvider,
7+
useOnMouseHover,
8+
useMousePosition,
9+
useOnMouseClick,
10+
useMouse,
11+
Fullscreen,
12+
} from '../ink-mouse';
13+
import { useMap } from '@react-hookz/web';
14+
15+
function App() {
16+
return (
17+
<MouseProvider>
18+
<Fullscreen>
19+
<MyComponent />
20+
</Fullscreen>
21+
</MouseProvider>
22+
);
23+
}
24+
25+
function MyComponent() {
26+
const mouse = useMouse();
27+
const mousePosition = useMousePosition();
28+
const map = useMap<'button1', number>() // Example of a simple state map
29+
30+
/**
31+
* Without this, your terminal will fill up with escape codes when you move the mouse.
32+
*/
33+
useInput((input, key) => {
34+
if (key.return) {
35+
mouse.toggle()
36+
}
37+
});
38+
39+
return (
40+
<Box gap={1} flexDirection='column' width={30} height={20} padding={5}>
41+
<Box>
42+
<Button label="Button 1" onClick={() => map.set('button1', (map.get('button1') || 0) + 1)} getProps={(state) => {
43+
if (state === 'clicking') {
44+
return { backgroundColor: 'blue', color: 'white' };
45+
}
46+
47+
if (state === 'hovering') {
48+
return { backgroundColor: 'yellow', };
49+
}
50+
51+
return {};
52+
}} />
53+
</Box>
54+
<Box flexDirection="column" gap={1}>
55+
<Text>{JSON.stringify(mousePosition)}</Text>
56+
<Text>Button 1 clicked: {map.get('button1') || 0} times</Text>
57+
</Box>
58+
</Box>
59+
);
60+
}
61+
62+
function Button(props: ComponentProps<typeof Box> & { label: string; onClick?: () => void, getProps?: (state: 'clicking' | 'hovering' | 'idle') => ComponentProps<typeof Box> }) {
63+
const ref = useRef<DOMElement | null>(null);
64+
65+
const [hovering, setHovering] = useState(false);
66+
const [clicking, setClicking] = useState(false);
67+
68+
useOnMouseClick(ref, (event) => {
69+
setClicking(event);
70+
if (event && typeof props.onClick === 'function') {
71+
props.onClick();
72+
}
73+
});
74+
useOnMouseHover(ref, setHovering);
75+
76+
const state = useMemo((): 'clicking' | 'hovering' | 'idle' => {
77+
if (clicking) {
78+
return 'clicking';
79+
}
80+
81+
if (hovering) {
82+
return 'hovering';
83+
}
84+
85+
return 'idle';
86+
}, [clicking, hovering]);
87+
88+
return (
89+
<Box
90+
gap={1}
91+
paddingX={1}
92+
ref={ref}
93+
{...props.getProps?.(state)}
94+
>
95+
<Text>{props.label}</Text>
96+
</Box>
97+
);
98+
}
99+
100+
render(<App />, {
101+
stdin: process.stdin,
102+
stdout: process.stdout,
103+
});

src/ink-mouse.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,16 @@ export { useOnMouseHover } from './ink-mouse/useOnMouseHover';
55
export { useOnMouseState } from './ink-mouse/useOnMouseState';
66
export { useMousePosition } from './ink-mouse/useMousePosition';
77
export { useElementPosition, useElementDimensions } from './ink-mouse/useElementPosition';
8-
export { useMouseAction } from './ink-mouse/useMouseAction';
8+
export { useMouseAction } from './ink-mouse/useMouseAction';
9+
export { useTerminalSize } from './ink-mouse/useTerminalSize';
10+
export { Fullscreen } from './ink-mouse/Fullscreen';
11+
export type { XtermMouseEvents } from './ink-mouse/createXtermMouseEvents';
12+
export type {
13+
MouseContextShape,
14+
MousePosition,
15+
MouseClickAction,
16+
MouseScrollAction,
17+
MouseDragAction,
18+
MouseAction,
19+
} from './ink-mouse/MouseContext';
20+
export type { XtermMouseState } from './ink-mouse/useXtermMouse';

src/ink-mouse/Fullscreen.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import { Box } from "ink";
3+
import type { PropsWithChildren } from "react";
4+
import { useTerminalSize } from "./useTerminalSize";
5+
6+
function Fullscreen(props: PropsWithChildren<{}>) {
7+
const terminal = useTerminalSize();
8+
return (
9+
<Box flexGrow={1} width={terminal.width} height={terminal.height}>
10+
{props.children}
11+
</Box>
12+
)
13+
}
14+
15+
export { Fullscreen };

src/ink-mouse/useElementPosition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useEffect, useState, type RefObject } from 'react';
77
* @param ref - The reference to the element.
88
* @returns The position of the element.
99
*/
10-
function useElementPosition(ref: RefObject<DOMElement>, deps: unknown[] = []) {
10+
function useElementPosition(ref: RefObject<DOMElement | null>, deps: unknown[] = []) {
1111
const [position, setPosition] = useState<{
1212
left: number;
1313
top: number;
@@ -27,7 +27,7 @@ function useElementPosition(ref: RefObject<DOMElement>, deps: unknown[] = []) {
2727
return position;
2828
}
2929

30-
function useElementDimensions(ref: RefObject<DOMElement>, deps: unknown[] = []) {
30+
function useElementDimensions(ref: RefObject<DOMElement | null>, deps: unknown[] = []) {
3131
const [dimensions, setDimensions] = useState<{
3232
width: number;
3333
height: number;

src/ink-mouse/useOnMouseClick.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isIntersecting } from './isIntersecting';
77
import { useElementDimensions, useElementPosition } from './useElementPosition';
88

99
function useOnMouseClick(
10-
ref: RefObject<DOMElement>,
10+
ref: RefObject<DOMElement | null>,
1111
onChange: (event: boolean) => void,
1212
) {
1313
const mouse = useMouse();

0 commit comments

Comments
 (0)