@tooee/search 0.1.14

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.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@tooee/search",
3
+ "version": "0.1.14",
4
+ "description": "Search hook, utilities, and SearchBar component for Tooee",
5
+ "keywords": [
6
+ "cli",
7
+ "opentui",
8
+ "search",
9
+ "terminal",
10
+ "tui"
11
+ ],
12
+ "homepage": "https://github.com/gingerhendrix/tooee",
13
+ "bugs": "https://github.com/gingerhendrix/tooee/issues",
14
+ "license": "MIT",
15
+ "author": "Gareth Andrew",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/gingerhendrix/tooee.git",
19
+ "directory": "packages/search"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "@tooee/source": "./src/index.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ }
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@tooee/commands": "0.1.14",
39
+ "@tooee/themes": "0.1.14"
40
+ },
41
+ "devDependencies": {
42
+ "@opentui/core": "^0.1.86",
43
+ "@opentui/react": "^0.1.86",
44
+ "@types/bun": "^1.3.10",
45
+ "@types/react": "^19.2.14",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "peerDependencies": {
49
+ "@opentui/core": "^0.1.86",
50
+ "@opentui/react": "^0.1.86",
51
+ "react": "^18.0.0 || ^19.0.0"
52
+ }
53
+ }
@@ -0,0 +1,55 @@
1
+ import { useTheme } from "@tooee/themes"
2
+
3
+ export interface SearchBarProps {
4
+ query: string
5
+ onQueryChange: (query: string) => void
6
+ onSubmit: () => void
7
+ onCancel: () => void
8
+ matchCount?: number
9
+ currentMatch?: number
10
+ }
11
+
12
+ export function SearchBar({
13
+ query,
14
+ onQueryChange,
15
+ onSubmit,
16
+ onCancel: _onCancel,
17
+ matchCount,
18
+ currentMatch,
19
+ }: SearchBarProps) {
20
+ const { theme } = useTheme()
21
+
22
+ const matchDisplay =
23
+ matchCount !== undefined && matchCount > 0
24
+ ? `${(currentMatch ?? 0) + 1}/${matchCount}`
25
+ : matchCount === 0 && query.length > 0
26
+ ? "No matches"
27
+ : ""
28
+
29
+ return (
30
+ <box
31
+ style={{
32
+ flexDirection: "row",
33
+ flexShrink: 0,
34
+ backgroundColor: theme.backgroundPanel,
35
+ paddingLeft: 1,
36
+ paddingRight: 1,
37
+ }}
38
+ >
39
+ <text content="/" style={{ fg: theme.accent }} />
40
+ <input
41
+ value={query}
42
+ focused
43
+ onInput={onQueryChange}
44
+ onSubmit={onSubmit}
45
+ backgroundColor="transparent"
46
+ focusedBackgroundColor="transparent"
47
+ textColor={theme.text}
48
+ cursorColor={theme.accent}
49
+ cursorStyle={{ style: "line", blinking: true }}
50
+ style={{ flexGrow: 1 }}
51
+ />
52
+ {matchDisplay ? <text content={` ${matchDisplay}`} style={{ fg: theme.textMuted }} /> : null}
53
+ </box>
54
+ )
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { useSearch } from "./search-hook.js"
2
+ export type { UseSearchOptions, SearchState } from "./search-hook.js"
3
+ export { findMatchingLines } from "./search.js"
4
+ export { SearchBar } from "./SearchBar.js"
5
+ export type { SearchBarProps } from "./SearchBar.js"
@@ -0,0 +1,147 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react"
2
+ import { useCommand, useMode, useSetMode, type Mode } from "@tooee/commands"
3
+
4
+ export interface UseSearchOptions {
5
+ match: (query: string) => number[]
6
+ onJump: (index: number) => void
7
+ }
8
+
9
+ export interface SearchState {
10
+ searchQuery: string
11
+ searchActive: boolean
12
+ setSearchQuery: (query: string) => void
13
+ matchingLines: number[]
14
+ currentMatchIndex: number
15
+ submitSearch: () => void
16
+ }
17
+
18
+ const EMPTY: number[] = []
19
+ const CURSOR_MODES: Mode[] = ["cursor"]
20
+ const ALL_MODES: Mode[] = ["cursor", "select", "insert"]
21
+
22
+ export function useSearch({ match, onJump }: UseSearchOptions): SearchState {
23
+ const mode = useMode()
24
+ const setMode = useSetMode()
25
+
26
+ const [searchQuery, setSearchQuery] = useState("")
27
+ const [searchActive, setSearchActive] = useState(false)
28
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
29
+ const [committedQuery, setCommittedQuery] = useState("")
30
+ const preSearchModeRef = useRef<Mode>("cursor")
31
+
32
+ const matchRef = useRef(match)
33
+ matchRef.current = match
34
+
35
+ const onJumpRef = useRef(onJump)
36
+ onJumpRef.current = onJump
37
+
38
+ const activeQuery = searchActive ? searchQuery : committedQuery
39
+
40
+ const matchingLines = useMemo(() => {
41
+ if (!activeQuery) return EMPTY
42
+ return matchRef.current(activeQuery)
43
+ }, [activeQuery])
44
+
45
+ const matchingLinesRef = useRef(matchingLines)
46
+ matchingLinesRef.current = matchingLines
47
+
48
+ // Imperatively set search query, reset match index, and jump to first match.
49
+ const updateSearchQuery = useCallback((query: string) => {
50
+ setSearchQuery(query)
51
+ setCurrentMatchIndex(0)
52
+ const matches = query ? matchRef.current(query) : []
53
+ if (matches[0] != null) {
54
+ onJumpRef.current(matches[0])
55
+ }
56
+ }, [])
57
+
58
+ useCommand({
59
+ id: "cursor-search-start",
60
+ title: "Search",
61
+ hotkey: "/",
62
+ modes: CURSOR_MODES,
63
+ handler: () => {
64
+ preSearchModeRef.current = mode
65
+ setSearchActive(true)
66
+ setSearchQuery("")
67
+ setMode("insert")
68
+ },
69
+ })
70
+
71
+ useCommand({
72
+ id: "cursor-search-next",
73
+ title: "Next match",
74
+ hotkey: "n",
75
+ modes: CURSOR_MODES,
76
+ when: () => !searchActive,
77
+ handler: () => {
78
+ const matches = matchingLinesRef.current
79
+ if (matches.length === 0) return
80
+
81
+ setCurrentMatchIndex((index) => {
82
+ const nextIndex = (index + 1) % matches.length
83
+ const nextMatch = matches[nextIndex]
84
+ if (nextMatch != null) {
85
+ onJumpRef.current(nextMatch)
86
+ }
87
+ return nextIndex
88
+ })
89
+ },
90
+ })
91
+
92
+ useCommand({
93
+ id: "cursor-search-prev",
94
+ title: "Previous match",
95
+ hotkey: "shift+n",
96
+ modes: CURSOR_MODES,
97
+ when: () => !searchActive,
98
+ handler: () => {
99
+ const matches = matchingLinesRef.current
100
+ if (matches.length === 0) return
101
+
102
+ setCurrentMatchIndex((index) => {
103
+ const nextIndex = (index - 1 + matches.length) % matches.length
104
+ const nextMatch = matches[nextIndex]
105
+ if (nextMatch != null) {
106
+ onJumpRef.current(nextMatch)
107
+ }
108
+ return nextIndex
109
+ })
110
+ },
111
+ })
112
+
113
+ useCommand({
114
+ id: "search-cancel",
115
+ title: "Cancel search",
116
+ hotkey: "escape",
117
+ modes: ALL_MODES,
118
+ when: () => searchActive,
119
+ handler: () => {
120
+ setSearchActive(false)
121
+ setSearchQuery("")
122
+ setCommittedQuery("")
123
+ setCurrentMatchIndex(0)
124
+ setMode(preSearchModeRef.current)
125
+ },
126
+ })
127
+
128
+ const submitSearch = useCallback(() => {
129
+ setCommittedQuery(searchQuery)
130
+ setSearchActive(false)
131
+ setCurrentMatchIndex(0)
132
+ const matches = searchQuery ? matchRef.current(searchQuery) : []
133
+ if (matches[0] != null) {
134
+ onJumpRef.current(matches[0])
135
+ }
136
+ setMode(preSearchModeRef.current)
137
+ }, [searchQuery, setMode])
138
+
139
+ return {
140
+ searchQuery,
141
+ searchActive,
142
+ setSearchQuery: updateSearchQuery,
143
+ matchingLines,
144
+ currentMatchIndex,
145
+ submitSearch,
146
+ }
147
+ }
package/src/search.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function findMatchingLines(text: string, query: string): number[] {
2
+ if (!query) return []
3
+ const lowerQuery = query.toLowerCase()
4
+ const results: number[] = []
5
+ let lineStart = 0
6
+ let lineNum = 0
7
+ for (let i = 0; i <= text.length; i++) {
8
+ if (i === text.length || text[i] === "\n") {
9
+ const line = text.slice(lineStart, i).toLowerCase()
10
+ if (line.includes(lowerQuery)) {
11
+ results.push(lineNum)
12
+ }
13
+ lineStart = i + 1
14
+ lineNum++
15
+ }
16
+ }
17
+ return results
18
+ }