@brimveyn/aimux 1.22.12 → 1.23.1
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 +2 -2
- package/src/app-runtime/settings-actions.ts +24 -13
- package/src/app-runtime/side-effects.ts +5 -0
- package/src/app-runtime/use-mouse-handlers.ts +2 -0
- package/src/app.tsx +6 -0
- package/src/index.tsx +6 -0
- package/src/input/keymap/help-entries.ts +1 -0
- package/src/input/modes/bridge.ts +2 -1
- package/src/input/modes/transitions.ts +7 -3
- package/src/input/modes/types.ts +2 -1
- package/src/restart-daemon.ts +5 -0
- package/src/services/ai-usage/projection.ts +44 -0
- package/src/services/aimux-counters/index.ts +87 -0
- package/src/services/aimux-counters/observe.ts +39 -0
- package/src/services/aimux-counters/store.ts +150 -0
- package/src/services/aimux-counters/summary.ts +78 -0
- package/src/services/usage-history/cost.ts +149 -0
- package/src/services/usage-history/insights.ts +314 -0
- package/src/services/usage-history/rollup.ts +78 -6
- package/src/services/usage-history/stats.ts +66 -35
- package/src/services/usage-history/store.ts +128 -9
- package/src/settings/search.ts +13 -9
- package/src/settings/sections/about.ts +1 -0
- package/src/settings/sections/appearance.ts +1 -0
- package/src/settings/sections/automation.ts +1 -0
- package/src/settings/sections/commands.ts +1 -0
- package/src/settings/sections/editor.ts +1 -0
- package/src/settings/sections/experimental.ts +1 -0
- package/src/settings/sections/git.ts +1 -0
- package/src/settings/sections/index.ts +23 -12
- package/src/settings/sections/integrations.ts +1 -0
- package/src/settings/sections/layout.ts +1 -0
- package/src/settings/sections/notifications.ts +1 -0
- package/src/settings/sections/setup.ts +1 -0
- package/src/settings/sections/status-bar.ts +1 -0
- package/src/settings/sections/workspace.ts +1 -0
- package/src/settings/types.ts +10 -0
- package/src/state/actions.ts +13 -3
- package/src/state/app-store.ts +12 -1
- package/src/state/reducers/modal-state.ts +12 -17
- package/src/state/reducers/settings-state.ts +29 -51
- package/src/state/reducers/stats-state.ts +56 -0
- package/src/state/stats-pages.ts +33 -0
- package/src/state/store.ts +5 -0
- package/src/state/types.ts +22 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +32 -1
- package/src/ui/components/modals/app/quotas-modal.tsx +42 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +4 -4
- package/src/ui/components/settings/row-value.tsx +22 -3
- package/src/ui/components/settings/settings-footer.tsx +170 -0
- package/src/ui/components/settings/settings-row.tsx +109 -33
- package/src/ui/components/settings/settings-search-bar.tsx +61 -0
- package/src/ui/components/settings/settings-view.tsx +163 -77
- package/src/ui/components/stats/aimux-page.tsx +245 -0
- package/src/ui/components/stats/chart.ts +157 -0
- package/src/ui/components/stats/day-facts.tsx +268 -0
- package/src/ui/components/stats/format.ts +98 -0
- package/src/ui/components/stats/heatmap.tsx +305 -0
- package/src/ui/components/stats/projects-page.tsx +293 -0
- package/src/ui/components/stats/quotas.tsx +210 -0
- package/src/ui/components/stats/shared.tsx +654 -0
- package/src/ui/components/stats/stats-view.tsx +153 -0
- package/src/ui/components/stats/usage-page.tsx +291 -0
- package/src/ui/components/stats/use-stats-data.ts +48 -0
- package/src/ui/root.tsx +7 -3
- package/src/ui/components/modals/app/ai-usage-modal.tsx +0 -520
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { SettingRow } from '../../../settings/types'
|
|
4
|
+
|
|
5
|
+
import { readRow, storedRow, useSettingsStore } from '../../../settings/settings-store'
|
|
6
|
+
import { useAppStore } from '../../../state/app-store'
|
|
7
|
+
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
8
|
+
import { useTheme } from '../../theme'
|
|
9
|
+
import { truncate } from '../../truncate'
|
|
10
|
+
import { Rule } from '../stats/shared'
|
|
11
|
+
import { describeValue } from './row-value'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Everything the rows used to carry on a second line of their own, moved to the
|
|
15
|
+
* foot of the screen and shown for the selected row only.
|
|
16
|
+
*
|
|
17
|
+
* Three lines, always, whatever the row is: a footer that grows and shrinks
|
|
18
|
+
* moves the list above it, and a list that shifts while you walk down it is the
|
|
19
|
+
* thing this screen was rebuilt to stop doing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `[key, what it does]`, so the keys can wear the theme and the prose cannot.
|
|
24
|
+
*
|
|
25
|
+
* Cut to what works on the row you are standing on: `←/→` does nothing to a
|
|
26
|
+
* text field, and a hint for a key that does nothing is worse than no hint.
|
|
27
|
+
*/
|
|
28
|
+
function hintsFor(row: SettingRow | null): [string, string][] {
|
|
29
|
+
const hints: [string, string][] = []
|
|
30
|
+
if (row?.kind === 'number' || row?.kind === 'select' || row?.kind === 'toggle') {
|
|
31
|
+
hints.push(['←/→', 'adjust'])
|
|
32
|
+
}
|
|
33
|
+
if (row != null && row.kind !== 'info') {
|
|
34
|
+
hints.push(['↵', row.kind === 'number' || row.kind === 'text' ? 'type a value' : 'change'])
|
|
35
|
+
}
|
|
36
|
+
hints.push(['r', 'reset'], ['/', 'search'], ['} {', 'section'], ['esc', 'close'])
|
|
37
|
+
return hints
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What the row itself does not say. Not its description — the row carries that
|
|
42
|
+
* under its own label now — but where its value comes from and when it takes
|
|
43
|
+
* effect, which belong to the row you are on rather than to the setting.
|
|
44
|
+
*/
|
|
45
|
+
function notesFor(row: SettingRow, fromConfigFile: boolean): string {
|
|
46
|
+
if (fromConfigFile) return 'set in aimux.config.ts — comes back on restart'
|
|
47
|
+
return storedRow(row)?.restart === true ? 'applies on restart' : ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The options of a select, spelled out with the current one lit. The row can
|
|
52
|
+
* only show the one it is on, and stepping through the rest to find out what
|
|
53
|
+
* they are is not reading, it is guessing.
|
|
54
|
+
*/
|
|
55
|
+
function OptionStrip({ row }: { row: Extract<SettingRow, { kind: 'select' }> }) {
|
|
56
|
+
const t = useTheme()
|
|
57
|
+
const values = useSettingsStore((s) => s.values)
|
|
58
|
+
const current = useAppStore((s) => readRow(row, { state: s, values }))
|
|
59
|
+
return (
|
|
60
|
+
<box flexDirection="row" gap={1}>
|
|
61
|
+
{row.options.map((option) => (
|
|
62
|
+
<text
|
|
63
|
+
key={String(option.value)}
|
|
64
|
+
fg={option.value === current ? t.text : t.textMuted}
|
|
65
|
+
selectable={false}
|
|
66
|
+
wrapMode="none"
|
|
67
|
+
>
|
|
68
|
+
{option.value === current ? `[${option.label}]` : option.label}
|
|
69
|
+
</text>
|
|
70
|
+
))}
|
|
71
|
+
</box>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What the second line says when the row is not one with options to list. */
|
|
76
|
+
function rangeOf(row: SettingRow): string {
|
|
77
|
+
if (row.kind === 'number') return `${String(row.min)} – ${String(row.max)}`
|
|
78
|
+
if (row.kind === 'toggle') return 'on · off'
|
|
79
|
+
return ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The whole of a value the row could only show the start of. A command or a
|
|
84
|
+
* path is the one kind of value that does not fit its column, and it is also
|
|
85
|
+
* the one you most need to read before changing it.
|
|
86
|
+
*/
|
|
87
|
+
function FullValue({ row }: { row: Extract<SettingRow, { kind: 'text' | 'action' | 'info' }> }) {
|
|
88
|
+
const t = useTheme()
|
|
89
|
+
const values = useSettingsStore((s) => s.values)
|
|
90
|
+
const value = useAppStore((s) => readRow(row, { state: s, values }))
|
|
91
|
+
return (
|
|
92
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
93
|
+
{String(value)}
|
|
94
|
+
</text>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const SettingsFooter = memo(function SettingsFooter({
|
|
99
|
+
pad,
|
|
100
|
+
row,
|
|
101
|
+
width,
|
|
102
|
+
}: {
|
|
103
|
+
pad: number
|
|
104
|
+
row: SettingRow | null
|
|
105
|
+
width: number
|
|
106
|
+
}) {
|
|
107
|
+
const t = useTheme()
|
|
108
|
+
const fromConfigFile = useSettingsStore((s) => (row ? s.fromConfigFile.has(row.id) : false))
|
|
109
|
+
const touched = useSettingsStore((s) => (row ? s.touched.has(row.id) : false))
|
|
110
|
+
const defaultValue = useSettingsStore((s) => (row ? s.defaults[row.id] : undefined))
|
|
111
|
+
|
|
112
|
+
const handleClose = useCallback(() => dispatchGlobal({ type: 'exit-settings' }), [])
|
|
113
|
+
|
|
114
|
+
const notes = row ? notesFor(row, fromConfigFile) : ''
|
|
115
|
+
// Only once the row has been changed — `r` is otherwise a key you press to
|
|
116
|
+
// find out what it does.
|
|
117
|
+
const resetHint =
|
|
118
|
+
row && touched && defaultValue !== undefined
|
|
119
|
+
? `default: ${describeValue(row, defaultValue)}`
|
|
120
|
+
: null
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<box flexDirection="column" flexShrink={0} paddingLeft={pad} paddingRight={pad}>
|
|
124
|
+
<Rule width={width} />
|
|
125
|
+
<box width={width} flexDirection="row" flexShrink={0}>
|
|
126
|
+
<box flexGrow={1} flexShrink={1}>
|
|
127
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
128
|
+
{truncate(notes, Math.max(0, width - (resetHint?.length ?? 0) - 2))}
|
|
129
|
+
</text>
|
|
130
|
+
</box>
|
|
131
|
+
{resetHint != null ? (
|
|
132
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
133
|
+
{resetHint}
|
|
134
|
+
</text>
|
|
135
|
+
) : null}
|
|
136
|
+
</box>
|
|
137
|
+
<box height={1} flexShrink={0} overflow="hidden">
|
|
138
|
+
{row?.kind === 'select' ? <OptionStrip row={row} /> : null}
|
|
139
|
+
{row?.kind === 'text' || row?.kind === 'action' || row?.kind === 'info' ? (
|
|
140
|
+
<FullValue row={row} />
|
|
141
|
+
) : null}
|
|
142
|
+
{row?.kind === 'number' || row?.kind === 'toggle' ? (
|
|
143
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
144
|
+
{rangeOf(row)}
|
|
145
|
+
</text>
|
|
146
|
+
) : null}
|
|
147
|
+
</box>
|
|
148
|
+
{/* Clipped rather than squeezed: a row of hints allowed to shrink loses
|
|
149
|
+
the spaces *inside* the words first, and `↵change -/+step` is worse
|
|
150
|
+
than the same line with its tail cut off on a narrow terminal. */}
|
|
151
|
+
<box width={width} flexDirection="row" flexShrink={0} gap={2} overflow="hidden">
|
|
152
|
+
{hintsFor(row).map(([key, what]) => (
|
|
153
|
+
<box key={key} flexDirection="row" flexShrink={0}>
|
|
154
|
+
<text
|
|
155
|
+
fg={t.primary}
|
|
156
|
+
selectable={false}
|
|
157
|
+
wrapMode="none"
|
|
158
|
+
onMouseDown={key === 'esc' ? handleClose : undefined}
|
|
159
|
+
>
|
|
160
|
+
{key}
|
|
161
|
+
</text>
|
|
162
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
163
|
+
{` ${what}`}
|
|
164
|
+
</text>
|
|
165
|
+
</box>
|
|
166
|
+
))}
|
|
167
|
+
</box>
|
|
168
|
+
</box>
|
|
169
|
+
)
|
|
170
|
+
})
|
|
@@ -2,53 +2,118 @@ import { memo } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import type { SettingRow } from '../../../settings/types'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { readRow, useSettingsStore } from '../../../settings/settings-store'
|
|
6
|
+
import { useAppStore } from '../../../state/app-store'
|
|
6
7
|
import { useTheme } from '../../theme'
|
|
8
|
+
import { truncate } from '../../truncate'
|
|
7
9
|
import { ListItem } from '../primitives/list-item'
|
|
8
|
-
import {
|
|
10
|
+
import { Bar } from '../stats/shared'
|
|
11
|
+
import { RowValue } from './row-value'
|
|
9
12
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Where the value comes from, on the left, out of the value column's way. Both
|
|
15
|
+
* are legended by the headline row at the top of the screen, which counts them.
|
|
16
|
+
*/
|
|
17
|
+
export const CONFIG_FILE_MARK = '*'
|
|
18
|
+
export const TOUCHED_MARK = '~'
|
|
19
|
+
|
|
20
|
+
/** The cursor, the mark, their spaces, and the row's own padding either side. */
|
|
21
|
+
const ROW_CHROME = 6
|
|
22
|
+
/** The column every value ends on, and the least the widest of them needs. */
|
|
23
|
+
const VALUE_WIDTH = 12
|
|
24
|
+
/**
|
|
25
|
+
* Uncapped, unlike the stats bars, which stop at 32.
|
|
26
|
+
*
|
|
27
|
+
* That cap is there because those bars share a scale and get compared to each
|
|
28
|
+
* other, and comparing two lengths gets harder as they grow. These do not: each
|
|
29
|
+
* one is a position inside its own row's range, read on its own. So it runs to
|
|
30
|
+
* the value column and the row has no gap in the middle of it.
|
|
31
|
+
*/
|
|
32
|
+
const BAR_MAX = Infinity
|
|
33
|
+
/**
|
|
34
|
+
* Every label there is fits in this, so nothing on the screen is cropped when
|
|
35
|
+
* the column is wide enough to give it — which is what the column layout is
|
|
36
|
+
* sized around. (`Auto-rename minimum words` is the longest at 25.)
|
|
37
|
+
*/
|
|
38
|
+
const LABEL_MAX = 28
|
|
13
39
|
|
|
14
40
|
interface SettingsRowProps {
|
|
15
41
|
row: SettingRow
|
|
16
42
|
active: boolean
|
|
17
43
|
index: number
|
|
44
|
+
/** The column this row stands in — it fills it, edge to edge. */
|
|
45
|
+
width: number
|
|
18
46
|
onSelect: (index: number) => void
|
|
19
47
|
}
|
|
20
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The gauge on a row that has a floor and a ceiling.
|
|
51
|
+
*
|
|
52
|
+
* A bar is honest here for the same reason it is honest on a quota and dishonest
|
|
53
|
+
* on a headline number: `min` and `max` are a real scale, so the fraction means
|
|
54
|
+
* something. It is measured from `min`, not from zero — a delay that runs 60s to
|
|
55
|
+
* 3600s sitting at 60 is at the bottom of its range, not at 2% of it.
|
|
56
|
+
*/
|
|
57
|
+
function RangeGauge({
|
|
58
|
+
row,
|
|
59
|
+
width,
|
|
60
|
+
}: {
|
|
61
|
+
row: Extract<SettingRow, { kind: 'number' }>
|
|
62
|
+
width: number
|
|
63
|
+
}) {
|
|
64
|
+
const t = useTheme()
|
|
65
|
+
const values = useSettingsStore((s) => s.values)
|
|
66
|
+
const value = useAppStore((s) => readRow(row, { state: s, values }))
|
|
67
|
+
const span = row.max - row.min
|
|
68
|
+
if (span <= 0) return null
|
|
69
|
+
return (
|
|
70
|
+
<Bar
|
|
71
|
+
color={t.primary}
|
|
72
|
+
max={span}
|
|
73
|
+
segments={width}
|
|
74
|
+
value={Math.max(0, (typeof value === 'number' ? value : row.min) - row.min)}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What it is, what it is set to, where that sits in its range, and what it does
|
|
81
|
+
* underneath. It can afford the second line because the screen shows one section
|
|
82
|
+
* at a time — fifty rows in one scroll could not, which is what made the old
|
|
83
|
+
* two-line row a wall rather than a paragraph.
|
|
84
|
+
*
|
|
85
|
+
* The same three-part geometry as a stats bar row: label on the left, the gauge
|
|
86
|
+
* in the middle, the column's right edge on the right. Every value on the screen
|
|
87
|
+
* therefore ends on one column, which is what makes a page of them read as a
|
|
88
|
+
* page rather than as rows that happen to sit above each other.
|
|
89
|
+
*
|
|
90
|
+
* Both columns at full strength, unlike a stats row, which mutes its label. On
|
|
91
|
+
* that screen the number is the content and the label only says which number it
|
|
92
|
+
* is; here you are scanning the labels to find the one you came for.
|
|
93
|
+
*/
|
|
21
94
|
export const SettingsRow = memo(function SettingsRow({
|
|
22
95
|
active,
|
|
23
96
|
index,
|
|
24
97
|
onSelect,
|
|
25
98
|
row,
|
|
99
|
+
width,
|
|
26
100
|
}: SettingsRowProps) {
|
|
27
101
|
const t = useTheme()
|
|
28
102
|
const fromConfigFile = useSettingsStore((s) => s.fromConfigFile.has(row.id))
|
|
29
103
|
const touched = useSettingsStore((s) => s.touched.has(row.id))
|
|
30
|
-
const defaultValue = useSettingsStore((s) => s.defaults[row.id])
|
|
31
|
-
const editable = row.kind !== 'info'
|
|
32
104
|
// A blank when neither applies, never nothing: a marker that comes and goes
|
|
33
105
|
// would shift the label of every row it is missing from.
|
|
34
106
|
let mark = ' '
|
|
35
107
|
if (fromConfigFile) mark = CONFIG_FILE_MARK
|
|
36
108
|
else if (touched) mark = TOUCHED_MARK
|
|
37
109
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
else if (storedRow(row)?.restart === true) notes.push('applies on restart')
|
|
46
|
-
// Right-aligned on the same line rather than tacked onto the end of the
|
|
47
|
-
// description: it is about the row, not about what the setting does, and the
|
|
48
|
-
// value column above it is on that edge too. Only once the row has been
|
|
49
|
-
// changed — `r` is otherwise a key you press to find out what it does.
|
|
50
|
-
const resetHint =
|
|
51
|
-
touched && defaultValue !== undefined ? `r resets to ${describeValue(row, defaultValue)}` : null
|
|
110
|
+
// The label gets first call on the column and the gauge takes what is left,
|
|
111
|
+
// not the other way round: a bar two cells shorter still reads as the same
|
|
112
|
+
// proportion, where `Transparent bac…` and `Transparent bak…` do not read as
|
|
113
|
+
// different settings.
|
|
114
|
+
const inner = Math.max(12, width - ROW_CHROME)
|
|
115
|
+
const labelWidth = Math.max(10, Math.min(LABEL_MAX, inner - VALUE_WIDTH))
|
|
116
|
+
const barWidth = Math.min(BAR_MAX, inner - labelWidth - VALUE_WIDTH - 1)
|
|
52
117
|
|
|
53
118
|
return (
|
|
54
119
|
<ListItem
|
|
@@ -57,22 +122,33 @@ export const SettingsRow = memo(function SettingsRow({
|
|
|
57
122
|
index={index}
|
|
58
123
|
onClickIndex={onSelect}
|
|
59
124
|
leading={<text fg={fromConfigFile ? t.warning : t.secondary}>{mark}</text>}
|
|
60
|
-
title={<text fg={editable ? t.text : t.textMuted}>{row.label}</text>}
|
|
61
125
|
subtitle={
|
|
62
|
-
|
|
63
|
-
<box
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</
|
|
67
|
-
{resetHint != null ? (
|
|
68
|
-
<text fg={t.textMuted} wrapMode="none">
|
|
69
|
-
{resetHint}
|
|
70
|
-
</text>
|
|
71
|
-
) : null}
|
|
126
|
+
row.description != null && row.description !== '' ? (
|
|
127
|
+
<box width={inner}>
|
|
128
|
+
<text fg={t.textMuted} selectable={false}>
|
|
129
|
+
{row.description}
|
|
130
|
+
</text>
|
|
72
131
|
</box>
|
|
73
132
|
) : undefined
|
|
74
133
|
}
|
|
75
|
-
|
|
134
|
+
title={
|
|
135
|
+
<box width={inner} flexDirection="row" flexShrink={0}>
|
|
136
|
+
<box width={labelWidth} flexShrink={0}>
|
|
137
|
+
<text fg={row.kind === 'info' ? t.textMuted : t.text} wrapMode="none">
|
|
138
|
+
{truncate(row.label, labelWidth - 1)}
|
|
139
|
+
</text>
|
|
140
|
+
</box>
|
|
141
|
+
{row.kind === 'number' && barWidth > 0 ? <RangeGauge row={row} width={barWidth} /> : null}
|
|
142
|
+
{/* Grows into whatever the label and the gauge left, so the value ends
|
|
143
|
+
on the column's edge whether or not the row has a gauge. */}
|
|
144
|
+
<box flexGrow={1} flexDirection="row" justifyContent="flex-end">
|
|
145
|
+
<RowValue
|
|
146
|
+
row={row}
|
|
147
|
+
maxWidth={inner - labelWidth - (barWidth > 0 && row.kind === 'number' ? barWidth : 0)}
|
|
148
|
+
/>
|
|
149
|
+
</box>
|
|
150
|
+
</box>
|
|
151
|
+
}
|
|
76
152
|
/>
|
|
77
153
|
)
|
|
78
154
|
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
4
|
+
import { useTheme } from '../../theme'
|
|
5
|
+
|
|
6
|
+
const SEARCH_GLYPH = '\u{2315}'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The way into the search, drawn rather than remembered.
|
|
10
|
+
*
|
|
11
|
+
* `/` opened the picker long before this existed, and the footer said so — but a
|
|
12
|
+
* key you have to read a hint to know about is a key nobody presses. A field you
|
|
13
|
+
* can see and click is the only version of this that gets used, so the screen
|
|
14
|
+
* spends three rows on it and keeps it out of the scroll.
|
|
15
|
+
*
|
|
16
|
+
* It never holds text. Clicking it opens the picker, which is where the typing
|
|
17
|
+
* and the filtering actually happen; this is its handle.
|
|
18
|
+
*/
|
|
19
|
+
export const SettingsSearchBar = memo(function SettingsSearchBar({ width }: { width: number }) {
|
|
20
|
+
const t = useTheme()
|
|
21
|
+
const handleOpen = useCallback(() => {
|
|
22
|
+
dispatchGlobal({ type: 'open-settings-search' })
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
// Filled, bordered in the active colour, and the prompt at full strength: it
|
|
26
|
+
// is the one control on a read-mostly screen, and a control drawn like the
|
|
27
|
+
// text around it is a control nobody sees.
|
|
28
|
+
return (
|
|
29
|
+
<box
|
|
30
|
+
border
|
|
31
|
+
borderStyle="rounded"
|
|
32
|
+
borderColor={t.borderActive}
|
|
33
|
+
backgroundColor={t.backgroundElement}
|
|
34
|
+
width={width}
|
|
35
|
+
flexShrink={0}
|
|
36
|
+
flexDirection="row"
|
|
37
|
+
paddingLeft={1}
|
|
38
|
+
paddingRight={1}
|
|
39
|
+
onMouseDown={handleOpen}
|
|
40
|
+
>
|
|
41
|
+
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
42
|
+
{`${SEARCH_GLYPH} `}
|
|
43
|
+
</text>
|
|
44
|
+
<box flexGrow={1} flexShrink={1}>
|
|
45
|
+
<text fg={t.text} selectable={false} wrapMode="none">
|
|
46
|
+
Search every setting…
|
|
47
|
+
</text>
|
|
48
|
+
</box>
|
|
49
|
+
{/* The key drawn as the cap you press, not as a letter in a sentence. */}
|
|
50
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
51
|
+
{'[ '}
|
|
52
|
+
</text>
|
|
53
|
+
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
54
|
+
/
|
|
55
|
+
</text>
|
|
56
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
57
|
+
{' ]'}
|
|
58
|
+
</text>
|
|
59
|
+
</box>
|
|
60
|
+
)
|
|
61
|
+
})
|