├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── QuickSearchInput.tsx └── examples │ ├── Example1.tsx │ ├── Example2.tsx │ ├── Example3.tsx │ ├── Example4.tsx │ └── ExampleDirectory.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | .vscode 4 | 5 | # Build 6 | build/ 7 | *.pyc 8 | a.out 9 | 10 | # Metafiles 11 | .DS_Store 12 | 13 | # Dependencies 14 | bower_components/ 15 | node_modules/ 16 | venv/ 17 | 18 | # Debug 19 | npm-debug.log 20 | 21 | # Extras 22 | temp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andrei Cioara (http://andrei.cioara.me) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ink-quicksearch-input 2 | 3 | > QuickSearch Component for [Ink 2](https://github.com/vadimdemedes/ink), [demo on repl.it](https://repl.it/@johnosullivan1/ink-quicksearch-input) 4 | 5 | Forked from [@aicioara](https://github.com/aicioara)'s original [`ink-quicksearch`](https://github.com/aicioara/ink-quicksearch) component to upgrade it to Ink 2. Big thanks to him for laying out the original logic in v1! If you are looking for a component that works with Ink v1, that's where to go. This re-write uses modern React (e.g. function components and hooks), and it is also in Typescript, improving the developer experience. The only behavioral difference is that this component always filters out items which do not match the query. Note that the demo runs a good bit slower than it does in an actual terminal; there's some uncanny-valley lag which is not present during normal use. 6 | 7 | ## Install 8 | 9 | ``` 10 | $ npm install ink-quicksearch-input 11 | ``` 12 | 13 | ## Quickstart 14 | 15 | If you'd like to get a feel for how the component works, you can see the examples in action by running: 16 | 17 | ```bash 18 | npm install 19 | npm start 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```jsx 25 | import React, { useState } from 'react'; 26 | import { render, Text } from 'ink'; 27 | import { QuickSearchInput } from 'ink-quicksearch-input'; 28 | 29 | const Demo = (props) => { 30 | const [result, setResult] = useState(''); 31 | return ( 32 | <> 33 | The user selected {result}. 34 | {'\n'} 35 | setResult(item.label)} /> 47 | 48 | ) 49 | } 50 | 51 | render(); 52 | ``` 53 | 54 | 55 | ## Props 56 | 57 | | Parameter | Type | Default | Description 58 | | --- | --- | --- | --- | 59 | | items | `Array(object)` | `[]` | Items to display in a list.
Each item must be an object and have at least a `label` prop. 60 | | onSelect | `function` | | Function to call when user selects an item.
Item object is passed to that function as an argument. 61 | | focus | `boolean` | `true` | Listen to user's input.
Useful in case there are multiple input components at the same time and input must be "routed" to a specific component. 62 | | caseSensitive | `boolean` | `false` | Whether or not quicksearch matching will be case sensitive. 63 | | limit | `int` | `0` | Limit the number of rows to display. `0` is unlimited
Use in conjunction with https://www.npmjs.com/package/term-size. 64 | | forceMatchingQuery | `bool` | `false` | If set to true, queries that return no results are not allowed. In particular, if previous query `X` returns at least one result and `X + new_character` would not, query will not update to `X + new_character`. 65 | | clearQueryChars | `Array(char)` | `['\u0015', '\u0017']`
(Ctrl + u, Ctrl + w) | Key Combinations that will clear the query.
`ch` follows the `keypress` API `process.stdin.on('keypress', (ch, key) => {})`. 66 | | initialSelectionIndex | `int` | `0` | Selection index when the component is initially rendered or when `props.items` changes. Can be set together with new `props.items` to automatically select an option. 67 | | label | `string` | | Optionally provide a label which will appear before the current query. 68 | | indicatorComponent | Component | | Custom component to override the default indicator component (default - arrow). 69 | | itemComponent | Component | | Custom component to override the default item style (default - selection coloring). 70 | | highlightComponent | Component | | Custom component to override the default highlight style (default - background highlight). 71 | | statusComponent | Component | | Custom component to override the status component (default - current query, optional value label). 72 | 73 | ## Component Props 74 | 75 | ### indicatorComponent 76 | 77 | Type: `Component`
78 | Props: 79 | 80 | - `isSelected`: `boolean` 81 | - `isHighlighted`: `boolean` 82 | - `item`: `Object` - The corresponding object inside `props.items` 83 | 84 | 85 | ### itemComponent 86 | 87 | Type: `Component`
88 | Props: 89 | 90 | - `isSelected`: `boolean` 91 | - `isHighlighted`: `boolean` 92 | - `item`: `Object` - The corresponding object inside `props.items` 93 | - `children`: `any` 94 | 95 | 96 | ### highlightComponent 97 | 98 | Type: `Component`
99 | Props: 100 | 101 | - `isSelected`: `boolean` 102 | - `isHighlighted`: `boolean` 103 | - `item`: `Object` - The corresponding object inside `props.items` 104 | - `children`: `any` 105 | 106 | 107 | ### statusComponent 108 | 109 | Type: `Component`
110 | Props: 111 | 112 | - `hasMatch`: `boolean` 113 | - `children`: `any` 114 | - `label`: Optional `string` 115 | 116 | 117 | 118 | ## Default Components 119 | 120 | ```jsx 121 | // For the following four, whitespace is important 122 | const IndicatorComponent = ({isSelected}) => { 123 | return {isSelected ? '>' : ' '} ; 124 | }; 125 | 126 | const ItemComponent = ({isSelected, children}) => ( 127 | {children} 128 | ); 129 | 130 | const HighlightComponent = ({children}) => ( 131 | {children} 132 | ); 133 | 134 | const StatusComponent = ({hasMatch, children}) => ( 135 | {children} 136 | ); 137 | ``` 138 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ink-quicksearch-input", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/color-name": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 10 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", 11 | "dev": true 12 | }, 13 | "@types/has-ansi": { 14 | "version": "3.0.0", 15 | "resolved": "https://registry.npmjs.org/@types/has-ansi/-/has-ansi-3.0.0.tgz", 16 | "integrity": "sha512-H3vFOwfLlFEC0MOOrcSkus8PCnMCzz4N0EqUbdJZCdDhBTfkAu86aRYA+MTxjKW6jCpUvxcn4715US8g+28BMA==" 17 | }, 18 | "@types/ink": { 19 | "version": "2.0.3", 20 | "resolved": "https://registry.npmjs.org/@types/ink/-/ink-2.0.3.tgz", 21 | "integrity": "sha512-DYKIKEJqhsGfQ/jgX0t9BzfHmBJ/9dBBT2MDsHAQRAfOPhEe7LZm5QeNBx1J34/e108StCPuJ3r4bh1y38kCJA==", 22 | "dev": true, 23 | "requires": { 24 | "ink": "*" 25 | } 26 | }, 27 | "@types/lodash": { 28 | "version": "4.14.149", 29 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", 30 | "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" 31 | }, 32 | "@types/lodash.isequal": { 33 | "version": "4.5.5", 34 | "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz", 35 | "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==", 36 | "requires": { 37 | "@types/lodash": "*" 38 | } 39 | }, 40 | "@types/node": { 41 | "version": "12.12.14", 42 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz", 43 | "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==", 44 | "dev": true 45 | }, 46 | "@types/prop-types": { 47 | "version": "15.7.3", 48 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", 49 | "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", 50 | "dev": true 51 | }, 52 | "@types/react": { 53 | "version": "16.9.15", 54 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.15.tgz", 55 | "integrity": "sha512-WsmM1b6xQn1tG3X2Hx4F3bZwc2E82pJXt5OPs2YJgg71IzvUoKOSSSYOvLXYCg1ttipM+UuA4Lj3sfvqjVxyZw==", 56 | "dev": true, 57 | "requires": { 58 | "@types/prop-types": "*", 59 | "csstype": "^2.2.0" 60 | } 61 | }, 62 | "ansi-escapes": { 63 | "version": "4.3.0", 64 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", 65 | "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", 66 | "dev": true, 67 | "requires": { 68 | "type-fest": "^0.8.1" 69 | } 70 | }, 71 | "ansi-regex": { 72 | "version": "3.0.0", 73 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 74 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 75 | }, 76 | "ansi-styles": { 77 | "version": "4.2.0", 78 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", 79 | "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", 80 | "dev": true, 81 | "requires": { 82 | "@types/color-name": "^1.1.1", 83 | "color-convert": "^2.0.1" 84 | } 85 | }, 86 | "arrify": { 87 | "version": "2.0.1", 88 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", 89 | "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", 90 | "dev": true 91 | }, 92 | "astral-regex": { 93 | "version": "2.0.0", 94 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", 95 | "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", 96 | "dev": true 97 | }, 98 | "auto-bind": { 99 | "version": "3.0.0", 100 | "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-3.0.0.tgz", 101 | "integrity": "sha512-v0A231a/lfOo6kxQtmEkdBfTApvC21aJYukA8pkKnoTvVqh3Wmm7/Rwy4GBCHTTHVoLVA5qsBDDvf1XY1nIV2g==", 102 | "dev": true 103 | }, 104 | "chalk": { 105 | "version": "3.0.0", 106 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", 107 | "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", 108 | "dev": true, 109 | "requires": { 110 | "ansi-styles": "^4.1.0", 111 | "supports-color": "^7.1.0" 112 | } 113 | }, 114 | "ci-info": { 115 | "version": "2.0.0", 116 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", 117 | "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", 118 | "dev": true 119 | }, 120 | "cli-cursor": { 121 | "version": "3.1.0", 122 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 123 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 124 | "dev": true, 125 | "requires": { 126 | "restore-cursor": "^3.1.0" 127 | } 128 | }, 129 | "cli-truncate": { 130 | "version": "2.1.0", 131 | "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", 132 | "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", 133 | "dev": true, 134 | "requires": { 135 | "slice-ansi": "^3.0.0", 136 | "string-width": "^4.2.0" 137 | } 138 | }, 139 | "color-convert": { 140 | "version": "2.0.1", 141 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 142 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 143 | "dev": true, 144 | "requires": { 145 | "color-name": "~1.1.4" 146 | } 147 | }, 148 | "color-name": { 149 | "version": "1.1.4", 150 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 151 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 152 | "dev": true 153 | }, 154 | "csstype": { 155 | "version": "2.6.7", 156 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", 157 | "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==", 158 | "dev": true 159 | }, 160 | "emoji-regex": { 161 | "version": "8.0.0", 162 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 163 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 164 | "dev": true 165 | }, 166 | "has-ansi": { 167 | "version": "3.0.0", 168 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-3.0.0.tgz", 169 | "integrity": "sha1-Ngd+8dFfMzSEqn+neihgbxxlWzc=", 170 | "requires": { 171 | "ansi-regex": "^3.0.0" 172 | } 173 | }, 174 | "has-flag": { 175 | "version": "4.0.0", 176 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 177 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 178 | "dev": true 179 | }, 180 | "ink": { 181 | "version": "2.6.0", 182 | "resolved": "https://registry.npmjs.org/ink/-/ink-2.6.0.tgz", 183 | "integrity": "sha512-nD/wlSuB6WnFsFB0nUcOJdy28YvvDer3eo+gezjvZqojGA4Rx5sQpacvN//Aai83DRgwrRTyKBl5aciOcfP3zQ==", 184 | "dev": true, 185 | "requires": { 186 | "ansi-escapes": "^4.2.1", 187 | "arrify": "^2.0.1", 188 | "auto-bind": "^3.0.0", 189 | "chalk": "^3.0.0", 190 | "cli-cursor": "^3.1.0", 191 | "cli-truncate": "^2.0.0", 192 | "is-ci": "^2.0.0", 193 | "lodash.throttle": "^4.1.1", 194 | "log-update": "^3.0.0", 195 | "prop-types": "^15.6.2", 196 | "react-reconciler": "^0.24.0", 197 | "scheduler": "^0.18.0", 198 | "signal-exit": "^3.0.2", 199 | "slice-ansi": "^3.0.0", 200 | "string-length": "^3.1.0", 201 | "widest-line": "^3.1.0", 202 | "wrap-ansi": "^6.2.0", 203 | "yoga-layout-prebuilt": "^1.9.3" 204 | } 205 | }, 206 | "is-ci": { 207 | "version": "2.0.0", 208 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", 209 | "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", 210 | "dev": true, 211 | "requires": { 212 | "ci-info": "^2.0.0" 213 | } 214 | }, 215 | "is-fullwidth-code-point": { 216 | "version": "3.0.0", 217 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 218 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 219 | "dev": true 220 | }, 221 | "js-tokens": { 222 | "version": "4.0.0", 223 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 224 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 225 | "dev": true 226 | }, 227 | "keypress": { 228 | "version": "0.2.1", 229 | "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", 230 | "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc=" 231 | }, 232 | "lodash.isequal": { 233 | "version": "4.5.0", 234 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 235 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" 236 | }, 237 | "lodash.throttle": { 238 | "version": "4.1.1", 239 | "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", 240 | "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=", 241 | "dev": true 242 | }, 243 | "log-update": { 244 | "version": "3.3.0", 245 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.3.0.tgz", 246 | "integrity": "sha512-YSKm5n+YjZoGZT5lfmOqasVH1fIH9xQA9A81Y48nZ99PxAP62vdCCtua+Gcu6oTn0nqtZd/LwRV+Vflo53ZDWA==", 247 | "dev": true, 248 | "requires": { 249 | "ansi-escapes": "^3.2.0", 250 | "cli-cursor": "^2.1.0", 251 | "wrap-ansi": "^5.0.0" 252 | }, 253 | "dependencies": { 254 | "ansi-escapes": { 255 | "version": "3.2.0", 256 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 257 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", 258 | "dev": true 259 | }, 260 | "ansi-regex": { 261 | "version": "4.1.0", 262 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 263 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 264 | "dev": true 265 | }, 266 | "ansi-styles": { 267 | "version": "3.2.1", 268 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 269 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 270 | "dev": true, 271 | "requires": { 272 | "color-convert": "^1.9.0" 273 | } 274 | }, 275 | "cli-cursor": { 276 | "version": "2.1.0", 277 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 278 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 279 | "dev": true, 280 | "requires": { 281 | "restore-cursor": "^2.0.0" 282 | } 283 | }, 284 | "color-convert": { 285 | "version": "1.9.3", 286 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 287 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 288 | "dev": true, 289 | "requires": { 290 | "color-name": "1.1.3" 291 | } 292 | }, 293 | "color-name": { 294 | "version": "1.1.3", 295 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 296 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 297 | "dev": true 298 | }, 299 | "emoji-regex": { 300 | "version": "7.0.3", 301 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 302 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 303 | "dev": true 304 | }, 305 | "is-fullwidth-code-point": { 306 | "version": "2.0.0", 307 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 308 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 309 | "dev": true 310 | }, 311 | "mimic-fn": { 312 | "version": "1.2.0", 313 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 314 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 315 | "dev": true 316 | }, 317 | "onetime": { 318 | "version": "2.0.1", 319 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 320 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 321 | "dev": true, 322 | "requires": { 323 | "mimic-fn": "^1.0.0" 324 | } 325 | }, 326 | "restore-cursor": { 327 | "version": "2.0.0", 328 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 329 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 330 | "dev": true, 331 | "requires": { 332 | "onetime": "^2.0.0", 333 | "signal-exit": "^3.0.2" 334 | } 335 | }, 336 | "string-width": { 337 | "version": "3.1.0", 338 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 339 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 340 | "dev": true, 341 | "requires": { 342 | "emoji-regex": "^7.0.1", 343 | "is-fullwidth-code-point": "^2.0.0", 344 | "strip-ansi": "^5.1.0" 345 | } 346 | }, 347 | "strip-ansi": { 348 | "version": "5.2.0", 349 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 350 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 351 | "dev": true, 352 | "requires": { 353 | "ansi-regex": "^4.1.0" 354 | } 355 | }, 356 | "wrap-ansi": { 357 | "version": "5.1.0", 358 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", 359 | "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", 360 | "dev": true, 361 | "requires": { 362 | "ansi-styles": "^3.2.0", 363 | "string-width": "^3.0.0", 364 | "strip-ansi": "^5.0.0" 365 | } 366 | } 367 | } 368 | }, 369 | "loose-envify": { 370 | "version": "1.4.0", 371 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 372 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 373 | "dev": true, 374 | "requires": { 375 | "js-tokens": "^3.0.0 || ^4.0.0" 376 | } 377 | }, 378 | "mimic-fn": { 379 | "version": "2.1.0", 380 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 381 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 382 | "dev": true 383 | }, 384 | "object-assign": { 385 | "version": "4.1.1", 386 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 387 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 388 | "dev": true 389 | }, 390 | "onetime": { 391 | "version": "5.1.0", 392 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 393 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 394 | "dev": true, 395 | "requires": { 396 | "mimic-fn": "^2.1.0" 397 | } 398 | }, 399 | "prop-types": { 400 | "version": "15.7.2", 401 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 402 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 403 | "dev": true, 404 | "requires": { 405 | "loose-envify": "^1.4.0", 406 | "object-assign": "^4.1.1", 407 | "react-is": "^16.8.1" 408 | } 409 | }, 410 | "react": { 411 | "version": "16.12.0", 412 | "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", 413 | "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", 414 | "dev": true, 415 | "requires": { 416 | "loose-envify": "^1.1.0", 417 | "object-assign": "^4.1.1", 418 | "prop-types": "^15.6.2" 419 | } 420 | }, 421 | "react-is": { 422 | "version": "16.12.0", 423 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", 424 | "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", 425 | "dev": true 426 | }, 427 | "react-reconciler": { 428 | "version": "0.24.0", 429 | "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.24.0.tgz", 430 | "integrity": "sha512-gAGnwWkf+NOTig9oOowqid9O0HjTDC+XVGBCAmJYYJ2A2cN/O4gDdIuuUQjv8A4v6GDwVfJkagpBBLW5OW9HSw==", 431 | "dev": true, 432 | "requires": { 433 | "loose-envify": "^1.1.0", 434 | "object-assign": "^4.1.1", 435 | "prop-types": "^15.6.2", 436 | "scheduler": "^0.18.0" 437 | } 438 | }, 439 | "restore-cursor": { 440 | "version": "3.1.0", 441 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 442 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 443 | "dev": true, 444 | "requires": { 445 | "onetime": "^5.1.0", 446 | "signal-exit": "^3.0.2" 447 | } 448 | }, 449 | "scheduler": { 450 | "version": "0.18.0", 451 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", 452 | "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", 453 | "dev": true, 454 | "requires": { 455 | "loose-envify": "^1.1.0", 456 | "object-assign": "^4.1.1" 457 | } 458 | }, 459 | "signal-exit": { 460 | "version": "3.0.2", 461 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 462 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 463 | "dev": true 464 | }, 465 | "slice-ansi": { 466 | "version": "3.0.0", 467 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", 468 | "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", 469 | "dev": true, 470 | "requires": { 471 | "ansi-styles": "^4.0.0", 472 | "astral-regex": "^2.0.0", 473 | "is-fullwidth-code-point": "^3.0.0" 474 | } 475 | }, 476 | "string-length": { 477 | "version": "3.1.0", 478 | "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", 479 | "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", 480 | "dev": true, 481 | "requires": { 482 | "astral-regex": "^1.0.0", 483 | "strip-ansi": "^5.2.0" 484 | }, 485 | "dependencies": { 486 | "ansi-regex": { 487 | "version": "4.1.0", 488 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 489 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 490 | "dev": true 491 | }, 492 | "astral-regex": { 493 | "version": "1.0.0", 494 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 495 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 496 | "dev": true 497 | }, 498 | "strip-ansi": { 499 | "version": "5.2.0", 500 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 501 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 502 | "dev": true, 503 | "requires": { 504 | "ansi-regex": "^4.1.0" 505 | } 506 | } 507 | } 508 | }, 509 | "string-width": { 510 | "version": "4.2.0", 511 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 512 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 513 | "dev": true, 514 | "requires": { 515 | "emoji-regex": "^8.0.0", 516 | "is-fullwidth-code-point": "^3.0.0", 517 | "strip-ansi": "^6.0.0" 518 | } 519 | }, 520 | "strip-ansi": { 521 | "version": "6.0.0", 522 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 523 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 524 | "dev": true, 525 | "requires": { 526 | "ansi-regex": "^5.0.0" 527 | }, 528 | "dependencies": { 529 | "ansi-regex": { 530 | "version": "5.0.0", 531 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 532 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 533 | "dev": true 534 | } 535 | } 536 | }, 537 | "supports-color": { 538 | "version": "7.1.0", 539 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 540 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 541 | "dev": true, 542 | "requires": { 543 | "has-flag": "^4.0.0" 544 | } 545 | }, 546 | "term-size": { 547 | "version": "2.1.0", 548 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.1.0.tgz", 549 | "integrity": "sha512-I42EWhJ+2aeNQawGx1VtpO0DFI9YcfuvAMNIdKyf/6sRbHJ4P+ZQ/zIT87tE+ln1ymAGcCJds4dolfSAS0AcNg==", 550 | "dev": true 551 | }, 552 | "type-fest": { 553 | "version": "0.8.1", 554 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 555 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", 556 | "dev": true 557 | }, 558 | "typescript": { 559 | "version": "3.7.3", 560 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", 561 | "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", 562 | "dev": true 563 | }, 564 | "widest-line": { 565 | "version": "3.1.0", 566 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", 567 | "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", 568 | "dev": true, 569 | "requires": { 570 | "string-width": "^4.0.0" 571 | } 572 | }, 573 | "wrap-ansi": { 574 | "version": "6.2.0", 575 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", 576 | "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", 577 | "dev": true, 578 | "requires": { 579 | "ansi-styles": "^4.0.0", 580 | "string-width": "^4.1.0", 581 | "strip-ansi": "^6.0.0" 582 | } 583 | }, 584 | "yoga-layout-prebuilt": { 585 | "version": "1.9.3", 586 | "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.9.3.tgz", 587 | "integrity": "sha512-9SNQpwuEh2NucU83i2KMZnONVudZ86YNcFk9tq74YaqrQfgJWO3yB9uzH1tAg8iqh5c9F5j0wuyJ2z72wcum2w==", 588 | "dev": true 589 | } 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ink-quicksearch-input", 3 | "version": "1.0.0", 4 | "description": "Quicksearch Input Component for Ink 2", 5 | "main": "build/QuickSearchInput.js", 6 | "types": "src/QuickSearchInput.tsx", 7 | "author": { 8 | "name": "John O'Sullivan " 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "prepare": "npm run build", 13 | "build": "tsc", 14 | "dev": "tsc --watch", 15 | "start": "node build/examples/ExampleDirectory.js" 16 | }, 17 | "files": [ 18 | "src/QuickSearchInput.tsx", 19 | "build/QuickSearchInput.js", 20 | "tsconfig.json" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Eximchain/ink-quicksearch-input.git" 25 | }, 26 | "keywords": [ 27 | "ink", 28 | "ink-component" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/Eximchain/ink-quicksearch-input/issues" 32 | }, 33 | "homepage": "https://github.com/Eximchain/ink-quicksearch-input#readme", 34 | "dependencies": { 35 | "@types/has-ansi": "^3.0.0", 36 | "@types/lodash.isequal": "^4.5.5", 37 | "has-ansi": "3.0.0", 38 | "keypress": "^0.2.1", 39 | "lodash.isequal": "4.5.0" 40 | }, 41 | "devDependencies": { 42 | "@types/ink": "^2.0.3", 43 | "@types/node": "^12.12.14", 44 | "@types/react": "^16.9.15", 45 | "ink": "^2.6.0", 46 | "react": "^16.12.0", 47 | "term-size": "^2.1.0", 48 | "typescript": "^3.7.3" 49 | }, 50 | "peerDependencies": { 51 | "ink": "2.6.x" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/QuickSearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, FunctionComponent, FC, useState, useEffect, useRef, useMemo, PropsWithChildren } from 'react'; 2 | import { Color, useStdin, Text, Box } from 'ink'; 3 | import hasAnsi from 'has-ansi'; 4 | import isEqual from 'lodash.isequal'; 5 | // @ts-ignore This module makes stdin emit keypress events, 6 | // that's it. Hasn't been published in six years, no types 7 | // available. 8 | import keypress from 'keypress'; 9 | 10 | const defaultValue = { label: '' }; // Used as return for empty array 11 | 12 | 13 | export type IsSelected = PropsWithChildren<{ 14 | isSelected: boolean 15 | }> 16 | 17 | export interface ItemProps extends IsSelected { 18 | item: Item 19 | isHighlighted: boolean | undefined 20 | } 21 | // For the following four, whitespace is important 22 | const IndicatorComponent: FC = ({ isSelected }) => { 23 | return {isSelected ? '>' : ' '} ; 24 | }; 25 | 26 | const ItemComponent: FC = ({ isSelected, children }) => ( 27 | {children} 28 | ); 29 | 30 | const HighlightComponent: FC = ({ children }) => ( 31 | {children} 32 | ); 33 | 34 | export interface StatusProps { 35 | hasMatch: boolean 36 | label?: string 37 | } 38 | 39 | const StatusComponent: FC = ({ hasMatch, children, label }) => ( 40 | {`${label || 'Query'}: `}{children} 41 | ); 42 | 43 | export interface Item { 44 | label: string 45 | value?: string | number 46 | } 47 | 48 | interface KeyPress { 49 | name: string 50 | sequence: string 51 | shift: boolean 52 | } 53 | 54 | export interface QuickSearchProps { 55 | onSelect: (item: Item) => void 56 | items: Item[] 57 | label?: string 58 | focus?: boolean 59 | caseSensitive?: boolean 60 | limit?: number 61 | forceMatchingQuery?: boolean 62 | clearQueryChars?: string[] 63 | initialSelectionIndex?: number 64 | indicatorComponent?: FunctionComponent 65 | itemComponent?: FunctionComponent 66 | highlightComponent?: FunctionComponent 67 | statusComponent?: FunctionComponent 68 | } 69 | 70 | export const QuickSearch: FC = (props) => { 71 | const { 72 | items, onSelect, focus, clearQueryChars, limit, 73 | indicatorComponent, itemComponent, highlightComponent, 74 | statusComponent, label, forceMatchingQuery 75 | } = Object.assign({}, defaultProps, props); 76 | 77 | // Map prop components onto capitalized names, required 78 | // for JSX to recognize em 79 | const Indicator = indicatorComponent; 80 | const Item = itemComponent; 81 | const Highlight = highlightComponent; 82 | const Status = statusComponent; 83 | 84 | const [windowIndices, setWindowIndices] = useState({ 85 | selection: 0, 86 | start: 0 87 | }) 88 | const [query, setQuery] = useState(''); 89 | 90 | const matchingItems = useMemo(() => { 91 | return getMatchingItems(); 92 | }, [items, query]); 93 | const usingLimitedView = limit !== 0 && matchingItems.length > limit; 94 | 95 | const inkStdin = useStdin(); 96 | useEffect(function listenToRawKeyboard() { 97 | keypress(inkStdin.stdin); 98 | if (inkStdin.isRawModeSupported) inkStdin.setRawMode(true); 99 | inkStdin.stdin.addListener('keypress', handleKeyPress) 100 | return () => { 101 | inkStdin.stdin.removeListener('keypress', handleKeyPress); 102 | if (inkStdin.isRawModeSupported) inkStdin.setRawMode(false); 103 | } 104 | }, [inkStdin, query, items, windowIndices]) 105 | 106 | const itemRef = useRef(items); 107 | useEffect(function resetForNewItems() { 108 | if (!isEqual(items, itemRef.current)) { 109 | itemRef.current = items; 110 | setWindowIndices({ 111 | selection: 0, start: 0 112 | }) 113 | setQuery(''); 114 | } 115 | }, [items]) 116 | 117 | const getValue = () => { 118 | return matchingItems[windowIndices.selection] || defaultValue; 119 | } 120 | 121 | 122 | function getMatchIndex(label: string, query: string) { 123 | return props.caseSensitive ? 124 | label.indexOf(query) : 125 | label.toLowerCase().indexOf(query.toLowerCase()) 126 | } 127 | 128 | function getMatchingItems(alternateQuery?: string) { 129 | const matchQuery = alternateQuery || query; 130 | if (matchQuery === '') return items; 131 | return items.filter(item => getMatchIndex(item.label, matchQuery) >= 0); 132 | } 133 | 134 | function removeCharFromQuery() { 135 | setQuery((query) => query.slice(0, -1) as string) 136 | } 137 | 138 | function addCharToQuery(newChar: string) { 139 | setQuery((query) => { 140 | let newQuery = query + newChar; 141 | let newMatching = getMatchingItems(newQuery); 142 | if (newMatching.length === 0 && forceMatchingQuery) { 143 | return query; 144 | } else { 145 | setWindowIndices({ start: 0, selection: 0 }) 146 | return newQuery 147 | } 148 | }) 149 | } 150 | 151 | function selectUp() { 152 | setWindowIndices((windowIndices) => { 153 | const { selection, start } = windowIndices; 154 | let newSelection = selection; 155 | let newStart = start; 156 | if (selection === 0) { 157 | // Wrap around to the bottom 158 | newSelection = matchingItems.length - 1; 159 | if (usingLimitedView) { 160 | newStart = matchingItems.length - limit; 161 | } 162 | } else { 163 | // Go up, potentially moving up window, unless 164 | // it is already 0. 165 | newSelection -= 1; 166 | if (usingLimitedView) { 167 | if (selection - start <= 1 && start > 0) { 168 | newStart -= 1; 169 | } 170 | } 171 | } 172 | return { 173 | selection: newSelection, 174 | start: newStart 175 | } 176 | }) 177 | } 178 | 179 | function selectDown() { 180 | setWindowIndices(({ start, selection }) => { 181 | let newStart = start; 182 | let newSelection = selection; 183 | if (selection === matchingItems.length - 1) { 184 | // Wrap around to the top 185 | newSelection = 0; 186 | if (newStart !== 0) newStart = 0; 187 | } else { 188 | // Go down, potentially moving window 189 | newSelection++; 190 | if (limit && matchingItems.length > limit && newSelection - newStart >= limit - 1) { 191 | newStart += 1; 192 | } 193 | } 194 | return { 195 | start: newStart, 196 | selection: newSelection 197 | } 198 | }) 199 | } 200 | 201 | function handleKeyPress(ch: string, key: KeyPress) { 202 | if (!focus) return; 203 | if (!key && parseInt(ch) !== NaN) { 204 | addCharToQuery(ch); 205 | return; 206 | } 207 | if (clearQueryChars.indexOf(ch) !== -1) { 208 | setQuery(''); 209 | } else if (key.name === 'return') { 210 | onSelect(getValue()); 211 | } else if (key.name === 'backspace') { 212 | removeCharFromQuery(); 213 | } else if (key.name === 'up') { 214 | selectUp(); 215 | } else if (key.name === 'down') { 216 | selectDown(); 217 | } else if (key.name === 'tab') { 218 | if (key.shift === false) { 219 | selectDown(); 220 | } else { 221 | selectUp(); 222 | } 223 | } else if (hasAnsi(key.sequence)) { 224 | // Ignore fancy Ansi escape codes 225 | } else { 226 | addCharToQuery(ch); 227 | } 228 | } 229 | 230 | const begin = windowIndices.start; 231 | let end = items.length; 232 | if (limit !== 0) end = Math.min(begin + limit, items.length); 233 | const visibleItems = matchingItems.slice(begin, end); 234 | 235 | return ( 236 | 237 | 238 | 0}> 239 | {query} 240 | 241 | 242 | { 243 | visibleItems.length === 0 ? 244 | 245 | No matches : 246 | 247 | visibleItems.map((item) => { 248 | const isSelected = matchingItems.indexOf(item) === windowIndices.selection; 249 | const isHighlighted = undefined; 250 | const itemProps: ItemProps = { isSelected, isHighlighted, item }; 251 | const label = item.label; 252 | 253 | const queryStart = getMatchIndex(label, query); 254 | const queryEnd = queryStart + query.length; 255 | let labelComponent; 256 | itemProps.isHighlighted = true; 257 | const preMatch = label.slice(0, queryStart); 258 | const match = label.slice(queryStart, queryEnd); 259 | const postMatch = label.slice(queryEnd); 260 | labelComponent = ( 261 | {preMatch}{match}{postMatch} 262 | ) 263 | return ( 264 | 265 | 266 | 267 | {labelComponent} 268 | 269 | 270 | ) 271 | }) 272 | } 273 | { 274 | !usingLimitedView ? null : ( 275 | 276 | Viewing {begin}-{end} of {matchingItems.length} matching items ({items.length} items overall) 277 | 278 | ) 279 | } 280 | 281 | ) 282 | } 283 | 284 | const defaultProps = { 285 | focus: true, 286 | caseSensitive: false, 287 | limit: 0, 288 | forceMatchingQuery: true, 289 | clearQueryChars: [ 290 | '\u0015', // Ctrl + U 291 | '\u0017', // Ctrl + W 292 | ], 293 | initialSelectionIndex: 0, 294 | indicatorComponent: IndicatorComponent, 295 | itemComponent: ItemComponent, 296 | highlightComponent: HighlightComponent, 297 | statusComponent: StatusComponent 298 | }; 299 | 300 | 301 | export default QuickSearch; -------------------------------------------------------------------------------- /src/examples/Example1.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { render, Color, Text, Box, Static } from 'ink'; 3 | 4 | import QuickSearch, { Item } from '../QuickSearchInput'; 5 | 6 | export const Example1Name = 'Basic'; 7 | 8 | export const Example1: FC = () => { 9 | const [selectedValue, setSelectedValue] = useState(''); 10 | return ( 11 | <> 12 | Example 1: {Example1Name} 13 | Selected item is {selectedValue} 14 | {'\n\n'} 15 | setSelectedValue(item.label), 26 | }} /> 27 | 28 | ) 29 | } 30 | 31 | if (require.main && require.main.filename === __filename) { 32 | render(); 33 | } -------------------------------------------------------------------------------- /src/examples/Example2.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { render, Color, Text, Box, Static } from 'ink'; 3 | 4 | import QuickSearch from '../QuickSearchInput'; 5 | 6 | export const Example2Name = 'Case-Sensitive, No Query/Status Element'; 7 | 8 | export const Example2: FC = () => { 9 | const [selectedValue, setSelectedValue] = useState(''); 10 | return ( 11 | <> 12 | Example 2: {Example2Name} 13 | Selected item is {selectedValue} 14 | { '\n\n' } 15 | setSelectedValue(d.label), 26 | caseSensitive: true, 27 | // Hide the statusComponent 28 | statusComponent: () => <>, 29 | }} /> 30 | 31 | ) 32 | 33 | } 34 | 35 | if (require.main && require.main.filename === __filename) { 36 | render(); 37 | } -------------------------------------------------------------------------------- /src/examples/Example3.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { render, Color, Text, Box, Static } from 'ink'; 3 | 4 | import QuickSearch, { ItemProps } from '../QuickSearchInput'; 5 | 6 | export const Example3Name = 'Case-Sensitive, Hiding Status & non-selected Items'; 7 | 8 | // Hide elements which are not selected 9 | const ItemComponent = ({isHighlighted, isSelected, children}:ItemProps) => { 10 | if (!isHighlighted) { 11 | return <>; 12 | } 13 | return {children}; 14 | }; 15 | 16 | const StatusComponent = () => <>; // No-op 17 | 18 | export const Example3: FC = () => { 19 | const [selectedValue, setSelectedValue] = useState(''); 20 | return ( 21 | <> 22 | Example 3: {Example3Name} 23 | Selected item is {selectedValue} 24 | { '\n\n' } 25 | setSelectedValue(d.label), 36 | caseSensitive: true, 37 | // Hide the statusComponent 38 | statusComponent: StatusComponent, 39 | // Only show items which are selected 40 | itemComponent: ItemComponent 41 | }} /> 42 | 43 | ) 44 | 45 | } 46 | 47 | if (require.main && require.main.filename === __filename) { 48 | render(); 49 | } -------------------------------------------------------------------------------- /src/examples/Example4.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { render, Color, Text } from 'ink'; 3 | import termSize from 'term-size'; 4 | import QuickSearch from '../QuickSearchInput'; 5 | 6 | export const Example4Name = 'Long list limited to terminal size; non-matching queries are not allowed'; 7 | 8 | export const Example4: FC = () => { 9 | const [selectedValue, setSelectedValue] = useState(''); 10 | return ( 11 | <> 12 | Example 4: {Example4Name} 13 | Selected item is {selectedValue} 14 | { '\n' } 15 | setSelectedValue(d.label), 71 | forceMatchingQuery: true, 72 | limit: termSize().rows - 8, // One for clear screen, one for cursor (Could be 1 more for statusComponent if that exists) 73 | }} /> 74 | 75 | ) 76 | 77 | } 78 | 79 | if (require.main && require.main.filename === __filename) { 80 | render(); 81 | } -------------------------------------------------------------------------------- /src/examples/ExampleDirectory.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { render, Color, Text, Box, Static } from 'ink'; 3 | import QuickSearch, { Item } from '../QuickSearchInput'; 4 | import { Example1, Example1Name } from './Example1'; 5 | import { Example2, Example2Name } from './Example2'; 6 | import { Example3, Example3Name } from './Example3'; 7 | import { Example4, Example4Name } from './Example4'; 8 | 9 | const ExampleDirectory: FC = () => { 10 | const [selectedExample, setSelectedExample] = useState(''); 11 | 12 | if (selectedExample === Example1Name) { 13 | return 14 | } else if (selectedExample === Example2Name) { 15 | return 16 | } else if (selectedExample === Example3Name) { 17 | return 18 | } else if (selectedExample === Example4Name) { 19 | return 20 | } else { 21 | return ( 22 | <> 23 | Which example would you like to explore? 24 | {'\n\n'} 25 | setSelectedExample(item.label), 33 | }} /> 34 | 35 | ) 36 | } 37 | } 38 | 39 | render(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "exclude": ["node_modules", "build"], 6 | "compileOnSave": true, 7 | "compilerOptions": { 8 | "target": "es5", // Compatible with older browsers 9 | "module": "commonjs", // Compatible with both Node.js and browser 10 | "moduleResolution": "node", // Tell tsc to look in node_modules for modules 11 | "sourceMap": false, // Creates *.js.map files 12 | "inlineSourceMap": true, 13 | "strict": true, // Strict types, eg. prohibits `var x=0; x=null` 14 | "alwaysStrict": true, // Enable JavaScript's "use strict" mode 15 | 16 | "outDir": "build", 17 | "sourceRoot": "./src", 18 | "noImplicitAny": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true, 21 | "typeRoots": ["node_modules/@types"], 22 | "jsx": "react" 23 | }, 24 | "typedocOptions": { 25 | "mode" : "modules", 26 | "out" : "docs/build", 27 | "categorizeByGroup" : true, 28 | "theme" : "docs/themeoverride", 29 | "excludeNotExported" : false 30 | } 31 | } --------------------------------------------------------------------------------