@elefunc/send 0.1.6 → 0.1.8
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 +1 -1
- package/src/core/session.ts +14 -0
- package/src/index.ts +75 -50
- package/src/tui/app.ts +239 -40
- package/src/tui/file-search-protocol.ts +1 -0
- package/src/tui/file-search.ts +4 -0
package/package.json
CHANGED
package/src/core/session.ts
CHANGED
|
@@ -492,6 +492,20 @@ export class SendSession {
|
|
|
492
492
|
return sent
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
shareTurnWithPeers(peerIds: string[]) {
|
|
496
|
+
if (!this.extraTurnServers.length) return 0
|
|
497
|
+
const iceServers = this.sharedTurnServers()
|
|
498
|
+
const sentPeers: string[] = []
|
|
499
|
+
for (const peerId of new Set(peerIds.filter(Boolean))) {
|
|
500
|
+
const peer = this.peers.get(peerId)
|
|
501
|
+
if (!peer || peer.presence !== "active") continue
|
|
502
|
+
if (!this.sendSignal({ kind: "turn-share", to: peer.id, iceServers })) continue
|
|
503
|
+
sentPeers.push(peer.id)
|
|
504
|
+
}
|
|
505
|
+
if (sentPeers.length) this.pushLog("turn:share-sent", { peers: sentPeers.length, scope: "filtered", peerIds: sentPeers }, "info")
|
|
506
|
+
return sentPeers.length
|
|
507
|
+
}
|
|
508
|
+
|
|
495
509
|
shareTurnWithAllPeers() {
|
|
496
510
|
const count = this.activePeers().length
|
|
497
511
|
if (!count || !this.extraTurnServers.length) return 0
|
package/src/index.ts
CHANGED
|
@@ -37,11 +37,45 @@ const waitPeerTimeout = (value: unknown) => {
|
|
|
37
37
|
if (!Number.isFinite(parsed) || parsed < 0) throw new ExitError("--wait-peer must be a finite non-negative number of milliseconds", 1)
|
|
38
38
|
return parsed
|
|
39
39
|
}
|
|
40
|
+
const BINARY_OPTION_FLAGS = {
|
|
41
|
+
clean: "--clean",
|
|
42
|
+
accept: "--accept",
|
|
43
|
+
offer: "--offer",
|
|
44
|
+
save: "--save",
|
|
45
|
+
} as const
|
|
46
|
+
type BinaryOptionKey = keyof typeof BINARY_OPTION_FLAGS
|
|
47
|
+
const binaryOption = (value: unknown, flag: string) => {
|
|
48
|
+
if (value == null) return undefined
|
|
49
|
+
if (value === 1 || value === "1") return true
|
|
50
|
+
if (value === 0 || value === "0") return false
|
|
51
|
+
throw new ExitError(`${flag} must be 0 or 1`, 1)
|
|
52
|
+
}
|
|
53
|
+
const parseBinaryOptions = <K extends BinaryOptionKey>(options: Record<string, unknown>, keys: readonly K[]) =>
|
|
54
|
+
Object.fromEntries(keys.map(key => [key, binaryOption(options[key], BINARY_OPTION_FLAGS[key])])) as Record<K, boolean | undefined>
|
|
40
55
|
|
|
41
56
|
const SELF_ID_LENGTH = 8
|
|
42
57
|
const SELF_ID_PATTERN = new RegExp(`^[a-z0-9]{${SELF_ID_LENGTH}}$`)
|
|
43
58
|
const SELF_HELP_TEXT = "self identity: name, name-ID, or -ID (use --self=-ID)"
|
|
44
59
|
const INVALID_SELF_ID_MESSAGE = `--self ID suffix must be exactly ${SELF_ID_LENGTH} lowercase alphanumeric characters`
|
|
60
|
+
type CliCommand = ReturnType<CAC["command"]>
|
|
61
|
+
const ROOM_SELF_OPTIONS = [
|
|
62
|
+
["--room <room>", "room id; omit to create a random room"],
|
|
63
|
+
["--self <self>", SELF_HELP_TEXT],
|
|
64
|
+
] as const
|
|
65
|
+
const TURN_OPTIONS = [
|
|
66
|
+
["--turn-url <url>", "custom TURN url, repeat or comma-separate"],
|
|
67
|
+
["--turn-username <value>", "custom TURN username"],
|
|
68
|
+
["--turn-credential <value>", "custom TURN credential"],
|
|
69
|
+
] as const
|
|
70
|
+
const SAVE_DIR_OPTION = ["--save-dir <dir>", "save directory"] as const
|
|
71
|
+
const TUI_TOGGLE_OPTIONS = [
|
|
72
|
+
["--clean <0|1>", "show only active peers when 1; show terminal peers too when 0"],
|
|
73
|
+
["--accept <0|1>", "auto-accept incoming offers: 1 on, 0 off"],
|
|
74
|
+
["--offer <0|1>", "auto-offer drafts to matching ready peers: 1 on, 0 off"],
|
|
75
|
+
["--save <0|1>", "auto-save completed incoming files: 1 on, 0 off"],
|
|
76
|
+
] as const
|
|
77
|
+
const addOptions = (command: CliCommand, definitions: readonly (readonly [string, string])[]) =>
|
|
78
|
+
definitions.reduce((next, [flag, description]) => next.option(flag, description), command)
|
|
45
79
|
|
|
46
80
|
const requireSelfId = (value: string) => {
|
|
47
81
|
if (!SELF_ID_PATTERN.test(value)) throw new ExitError(INVALID_SELF_ID_MESSAGE, 1)
|
|
@@ -61,12 +95,13 @@ const parseSelfOption = (value: unknown): Pick<SessionConfig, "name" | "localId"
|
|
|
61
95
|
export const sessionConfigFrom = (options: Record<string, unknown>, defaults: { autoAcceptIncoming?: boolean; autoSaveIncoming?: boolean }): SessionConfig & { room: string } => {
|
|
62
96
|
const room = cleanRoom(firstNonEmptyText(options.room, process.env.SEND_ROOM))
|
|
63
97
|
const self = parseSelfOption(options.self ?? process.env.SEND_SELF)
|
|
98
|
+
const { accept, save } = parseBinaryOptions(options, ["accept", "save"] as const)
|
|
64
99
|
return {
|
|
65
100
|
room,
|
|
66
101
|
...self,
|
|
67
102
|
saveDir: resolve(`${options.saveDir ?? process.env.SEND_SAVE_DIR ?? "downloads"}`),
|
|
68
|
-
autoAcceptIncoming: defaults.autoAcceptIncoming ?? false,
|
|
69
|
-
autoSaveIncoming: defaults.autoSaveIncoming ?? false,
|
|
103
|
+
autoAcceptIncoming: accept ?? defaults.autoAcceptIncoming ?? false,
|
|
104
|
+
autoSaveIncoming: save ?? defaults.autoSaveIncoming ?? false,
|
|
70
105
|
turnUrls: splitList(options.turnUrl ?? process.env.SEND_TURN_URL),
|
|
71
106
|
turnUsername: `${options.turnUsername ?? process.env.SEND_TURN_USERNAME ?? ""}`.trim() || undefined,
|
|
72
107
|
turnCredential: `${options.turnCredential ?? process.env.SEND_TURN_CREDENTIAL ?? ""}`.trim() || undefined,
|
|
@@ -218,8 +253,13 @@ const acceptCommand = async (options: Record<string, unknown>) => {
|
|
|
218
253
|
|
|
219
254
|
const tuiCommand = async (options: Record<string, unknown>) => {
|
|
220
255
|
const initialConfig = sessionConfigFrom(options, { autoAcceptIncoming: true, autoSaveIncoming: true })
|
|
256
|
+
const { clean, offer } = parseBinaryOptions(options, ["clean", "offer"] as const)
|
|
221
257
|
const { startTui } = await loadTuiRuntime()
|
|
222
|
-
await startTui(initialConfig,
|
|
258
|
+
await startTui(initialConfig, {
|
|
259
|
+
events: !!options.events,
|
|
260
|
+
clean: clean ?? true,
|
|
261
|
+
offer: offer ?? true,
|
|
262
|
+
})
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
type CliHandlers = {
|
|
@@ -240,53 +280,38 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
|
|
|
240
280
|
const cli = cac("send")
|
|
241
281
|
cli.usage("[command] [options]")
|
|
242
282
|
|
|
243
|
-
cli
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
cli
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
.option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
|
|
276
|
-
.option("--turn-username <value>", "custom TURN username")
|
|
277
|
-
.option("--turn-credential <value>", "custom TURN credential")
|
|
278
|
-
.action(handlers.accept)
|
|
279
|
-
|
|
280
|
-
cli
|
|
281
|
-
.command("tui", "launch the interactive terminal UI")
|
|
282
|
-
.option("--room <room>", "room id; omit to create a random room")
|
|
283
|
-
.option("--self <self>", SELF_HELP_TEXT)
|
|
284
|
-
.option("--events", "show the event log pane")
|
|
285
|
-
.option("--save-dir <dir>", "save directory")
|
|
286
|
-
.option("--turn-url <url>", "custom TURN url, repeat or comma-separate")
|
|
287
|
-
.option("--turn-username <value>", "custom TURN username")
|
|
288
|
-
.option("--turn-credential <value>", "custom TURN credential")
|
|
289
|
-
.action(handlers.tui)
|
|
283
|
+
addOptions(cli.command("peers", "list discovered peers"), [
|
|
284
|
+
...ROOM_SELF_OPTIONS,
|
|
285
|
+
["--wait <ms>", "discovery wait in milliseconds"],
|
|
286
|
+
["--json", "print a json snapshot"],
|
|
287
|
+
SAVE_DIR_OPTION,
|
|
288
|
+
...TURN_OPTIONS,
|
|
289
|
+
]).action(handlers.peers)
|
|
290
|
+
|
|
291
|
+
addOptions(cli.command("offer [...files]", "offer files to browser-compatible peers"), [
|
|
292
|
+
...ROOM_SELF_OPTIONS,
|
|
293
|
+
["--to <peer>", "target peer id or name-suffix, or `.` for all ready peers; default: `.`"],
|
|
294
|
+
["--wait-peer <ms>", "wait for eligible peers in milliseconds; omit to wait indefinitely"],
|
|
295
|
+
["--json", "emit ndjson events"],
|
|
296
|
+
SAVE_DIR_OPTION,
|
|
297
|
+
...TURN_OPTIONS,
|
|
298
|
+
]).action(handlers.offer)
|
|
299
|
+
|
|
300
|
+
addOptions(cli.command("accept", "receive and save files"), [
|
|
301
|
+
...ROOM_SELF_OPTIONS,
|
|
302
|
+
SAVE_DIR_OPTION,
|
|
303
|
+
["--once", "exit after the first saved incoming transfer"],
|
|
304
|
+
["--json", "emit ndjson events"],
|
|
305
|
+
...TURN_OPTIONS,
|
|
306
|
+
]).action(handlers.accept)
|
|
307
|
+
|
|
308
|
+
addOptions(cli.command("tui", "launch the interactive terminal UI"), [
|
|
309
|
+
...ROOM_SELF_OPTIONS,
|
|
310
|
+
...TUI_TOGGLE_OPTIONS,
|
|
311
|
+
["--events", "show the event log pane"],
|
|
312
|
+
SAVE_DIR_OPTION,
|
|
313
|
+
...TURN_OPTIONS,
|
|
314
|
+
]).action(handlers.tui)
|
|
290
315
|
|
|
291
316
|
cli.help(sections => {
|
|
292
317
|
const usage = sections.find(section => section.title === "Usage:")
|
package/src/tui/app.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
1
2
|
import { rgb, ui, type BadgeVariant, type VNode } from "@rezi-ui/core"
|
|
2
3
|
import { createNodeApp } from "@rezi-ui/node"
|
|
3
4
|
import { inspectLocalFile } from "../core/files"
|
|
@@ -37,6 +38,7 @@ type DenseSectionOptions = {
|
|
|
37
38
|
border?: "rounded" | "single" | "none"
|
|
38
39
|
flex?: number
|
|
39
40
|
}
|
|
41
|
+
type TuiLaunchOptions = { events?: boolean; clean?: boolean; offer?: boolean }
|
|
40
42
|
|
|
41
43
|
export type VisiblePane = "peers" | "transfers" | "logs"
|
|
42
44
|
|
|
@@ -45,6 +47,8 @@ export interface TuiState {
|
|
|
45
47
|
sessionSeed: SessionSeed
|
|
46
48
|
peerSelectionByRoom: Map<string, Map<string, boolean>>
|
|
47
49
|
snapshot: SessionSnapshot
|
|
50
|
+
aboutOpen: boolean
|
|
51
|
+
peerSearch: string
|
|
48
52
|
focusedId: string | null
|
|
49
53
|
roomInput: string
|
|
50
54
|
nameInput: string
|
|
@@ -66,12 +70,15 @@ export interface TuiState {
|
|
|
66
70
|
|
|
67
71
|
export interface TuiActions {
|
|
68
72
|
toggleEvents: TuiAction
|
|
73
|
+
openAbout: TuiAction
|
|
74
|
+
closeAbout: TuiAction
|
|
69
75
|
jumpToRandomRoom: TuiAction
|
|
70
76
|
commitRoom: TuiAction
|
|
71
77
|
setRoomInput: (value: string) => void
|
|
72
78
|
jumpToNewSelf: TuiAction
|
|
73
79
|
commitName: TuiAction
|
|
74
80
|
setNameInput: (value: string) => void
|
|
81
|
+
setPeerSearch: (value: string) => void
|
|
75
82
|
toggleSelectReadyPeers: TuiAction
|
|
76
83
|
clearPeerSelection: TuiAction
|
|
77
84
|
toggleHideTerminalPeers: TuiAction
|
|
@@ -97,12 +104,22 @@ export interface TuiActions {
|
|
|
97
104
|
|
|
98
105
|
const ROOM_INPUT_ID = "room-input"
|
|
99
106
|
const NAME_INPUT_ID = "name-input"
|
|
107
|
+
const PEER_SEARCH_INPUT_ID = "peer-search-input"
|
|
100
108
|
const DRAFT_INPUT_ID = "draft-input"
|
|
109
|
+
const ABOUT_TRIGGER_ID = "open-about"
|
|
101
110
|
const TRANSPARENT_BORDER_STYLE = { fg: rgb(7, 10, 12) } as const
|
|
102
111
|
const METRIC_BORDER_STYLE = { fg: rgb(20, 25, 32) } as const
|
|
103
112
|
const PRIMARY_TEXT_STYLE = { fg: rgb(255, 255, 255) } as const
|
|
104
113
|
const HEADING_TEXT_STYLE = { fg: rgb(255, 255, 255), bold: true } as const
|
|
105
114
|
const DEFAULT_WEB_URL = "https://send.rt.ht/"
|
|
115
|
+
const DEFAULT_SAVE_DIR = resolve(process.cwd(), "downloads")
|
|
116
|
+
const ABOUT_ELEFUNC_URL = "https://elefunc.com"
|
|
117
|
+
const ABOUT_TITLE = "About Send"
|
|
118
|
+
const ABOUT_INTRO = "Room-based peer-to-peer file transfers for the web and terminal"
|
|
119
|
+
const ABOUT_SUMMARY = "Join the same room, see who is there, and offer files directly to selected peers."
|
|
120
|
+
const ABOUT_RUNTIME = "Send uses lightweight signaling to discover peers and negotiate WebRTC. Files move over WebRTC data channels, using direct paths when possible and TURN relay when needed."
|
|
121
|
+
const ABOUT_CLI_LABEL = "bunx @elefunc/send@latest"
|
|
122
|
+
const ABOUT_WEB_LINK_LABEL = "Web"
|
|
106
123
|
const TRANSFER_DIRECTION_ARROW = {
|
|
107
124
|
out: { glyph: "↗", style: { fg: rgb(170, 217, 76), bold: true } },
|
|
108
125
|
in: { glyph: "↙", style: { fg: rgb(240, 113, 120), bold: true } },
|
|
@@ -118,6 +135,45 @@ export const visiblePanes = (showEvents: boolean): VisiblePane[] => showEvents ?
|
|
|
118
135
|
const noop = () => {}
|
|
119
136
|
|
|
120
137
|
const hashBool = (value: boolean) => value ? "1" : "0"
|
|
138
|
+
const safeShellArgPattern = /^[A-Za-z0-9._/:?=&,+@%-]+$/
|
|
139
|
+
const TUI_QUIT_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const
|
|
140
|
+
type TuiQuitSignal = (typeof TUI_QUIT_SIGNALS)[number]
|
|
141
|
+
type ProcessSignalLike = {
|
|
142
|
+
on?: (signal: TuiQuitSignal, handler: () => void) => unknown
|
|
143
|
+
off?: (signal: TuiQuitSignal, handler: () => void) => unknown
|
|
144
|
+
removeListener?: (signal: TuiQuitSignal, handler: () => void) => unknown
|
|
145
|
+
}
|
|
146
|
+
type ShareUrlState = Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming">
|
|
147
|
+
type ShareCliState = ShareUrlState & Pick<TuiState, "sessionSeed" | "eventsExpanded">
|
|
148
|
+
const shellQuote = (value: string) => safeShellArgPattern.test(value) ? value : `'${value.replaceAll("'", `'\"'\"'`)}'`
|
|
149
|
+
const appendCliFlag = (args: string[], flag: string, value?: string | null) => {
|
|
150
|
+
const text = `${value ?? ""}`.trim()
|
|
151
|
+
if (!text) return
|
|
152
|
+
args.push(flag, shellQuote(text))
|
|
153
|
+
}
|
|
154
|
+
const SHARE_TOGGLE_FLAGS = [
|
|
155
|
+
["clean", "hideTerminalPeers", "--clean"],
|
|
156
|
+
["accept", "autoAcceptIncoming", "--accept"],
|
|
157
|
+
["offer", "autoOfferOutgoing", "--offer"],
|
|
158
|
+
["save", "autoSaveIncoming", "--save"],
|
|
159
|
+
] as const satisfies readonly (readonly [string, keyof ShareUrlState, string])[]
|
|
160
|
+
const appendToggleCliFlags = (args: string[], state: ShareUrlState) => {
|
|
161
|
+
for (const [, stateKey, flag] of SHARE_TOGGLE_FLAGS) if (!state[stateKey]) appendCliFlag(args, flag, "0")
|
|
162
|
+
}
|
|
163
|
+
const buildHashParams = (state: ShareUrlState, omitDefaults = false) => {
|
|
164
|
+
const params = new URLSearchParams({ room: cleanRoom(state.snapshot.room) })
|
|
165
|
+
for (const [key, stateKey] of SHARE_TOGGLE_FLAGS) if (!omitDefaults || !state[stateKey]) params.set(key, hashBool(state[stateKey]))
|
|
166
|
+
return params
|
|
167
|
+
}
|
|
168
|
+
const shareTurnCliArgs = (sessionSeed: SessionSeed) => {
|
|
169
|
+
const turnUrls = [...new Set((sessionSeed.turnUrls ?? []).map(url => `${url ?? ""}`.trim()).filter(Boolean))]
|
|
170
|
+
if (!turnUrls.length) return []
|
|
171
|
+
const args: string[] = []
|
|
172
|
+
for (const turnUrl of turnUrls) appendCliFlag(args, "--turn-url", turnUrl)
|
|
173
|
+
appendCliFlag(args, "--turn-username", sessionSeed.turnUsername)
|
|
174
|
+
appendCliFlag(args, "--turn-credential", sessionSeed.turnCredential)
|
|
175
|
+
return args
|
|
176
|
+
}
|
|
121
177
|
|
|
122
178
|
export const resolveWebUrlBase = (value = process.env.SEND_WEB_URL) => {
|
|
123
179
|
const candidate = `${value ?? ""}`.trim() || DEFAULT_WEB_URL
|
|
@@ -128,29 +184,82 @@ export const resolveWebUrlBase = (value = process.env.SEND_WEB_URL) => {
|
|
|
128
184
|
}
|
|
129
185
|
}
|
|
130
186
|
|
|
131
|
-
|
|
132
|
-
state: Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming">,
|
|
133
|
-
baseUrl = resolveWebUrlBase(),
|
|
134
|
-
) => {
|
|
187
|
+
const renderWebUrl = (state: ShareUrlState, baseUrl = DEFAULT_WEB_URL, omitDefaults = true) => {
|
|
135
188
|
const url = new URL(baseUrl)
|
|
136
|
-
url.hash =
|
|
137
|
-
room: cleanRoom(state.snapshot.room),
|
|
138
|
-
clean: hashBool(state.hideTerminalPeers),
|
|
139
|
-
accept: hashBool(state.autoAcceptIncoming),
|
|
140
|
-
offer: hashBool(state.autoOfferOutgoing),
|
|
141
|
-
save: hashBool(state.autoSaveIncoming),
|
|
142
|
-
}).toString()
|
|
189
|
+
url.hash = buildHashParams(state, omitDefaults).toString()
|
|
143
190
|
return url.toString()
|
|
144
191
|
}
|
|
192
|
+
const renderCliCommand = (state: ShareCliState, { includeSelf = false, includePrefix = false } = {}) => {
|
|
193
|
+
const args = includePrefix ? ["bunx", "@elefunc/send@latest"] : []
|
|
194
|
+
appendCliFlag(args, "--room", state.snapshot.room)
|
|
195
|
+
if (includeSelf) appendCliFlag(args, "--self", displayPeerName(state.snapshot.name, state.snapshot.localId))
|
|
196
|
+
appendToggleCliFlags(args, state)
|
|
197
|
+
if (state.eventsExpanded) args.push("--events")
|
|
198
|
+
if (resolve(state.snapshot.saveDir) !== DEFAULT_SAVE_DIR) appendCliFlag(args, "--save-dir", state.snapshot.saveDir)
|
|
199
|
+
args.push(...shareTurnCliArgs(state.sessionSeed))
|
|
200
|
+
return args.join(" ")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const webInviteUrl = (state: ShareUrlState, baseUrl = resolveWebUrlBase()) => {
|
|
204
|
+
return renderWebUrl(state, baseUrl, false)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const aboutWebUrl = (state: ShareUrlState, baseUrl = DEFAULT_WEB_URL) => renderWebUrl(state, baseUrl)
|
|
208
|
+
|
|
209
|
+
export const aboutWebLabel = (state: ShareUrlState, baseUrl = DEFAULT_WEB_URL) => aboutWebUrl(state, baseUrl).replace(/^https:\/\//, "")
|
|
210
|
+
|
|
211
|
+
export const aboutCliCommand = (state: ShareCliState) => renderCliCommand(state)
|
|
212
|
+
|
|
213
|
+
export const resumeWebUrl = (state: ShareUrlState, baseUrl = DEFAULT_WEB_URL) => renderWebUrl(state, baseUrl)
|
|
214
|
+
|
|
215
|
+
export const resumeCliCommand = (state: ShareCliState) => renderCliCommand(state, { includeSelf: true, includePrefix: true })
|
|
216
|
+
|
|
217
|
+
export const resumeOutputLines = (state: ShareCliState) => [
|
|
218
|
+
"Rejoin with:",
|
|
219
|
+
"",
|
|
220
|
+
"Web",
|
|
221
|
+
resumeWebUrl(state),
|
|
222
|
+
"",
|
|
223
|
+
"CLI",
|
|
224
|
+
resumeCliCommand(state),
|
|
225
|
+
"",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
export const createQuitController = (processLike: ProcessSignalLike | null = process) => {
|
|
229
|
+
let settled = false
|
|
230
|
+
let resolvePromise = () => {}
|
|
231
|
+
const promise = new Promise<void>(resolve => { resolvePromise = resolve })
|
|
232
|
+
const requestStop = () => {
|
|
233
|
+
if (settled) return false
|
|
234
|
+
settled = true
|
|
235
|
+
resolvePromise()
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
const handler = () => { requestStop() }
|
|
239
|
+
for (const signal of TUI_QUIT_SIGNALS) processLike?.on?.(signal, handler)
|
|
240
|
+
return {
|
|
241
|
+
promise,
|
|
242
|
+
requestStop,
|
|
243
|
+
detach: () => {
|
|
244
|
+
for (const signal of TUI_QUIT_SIGNALS) {
|
|
245
|
+
processLike?.off?.(signal, handler)
|
|
246
|
+
processLike?.removeListener?.(signal, handler)
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
145
251
|
|
|
146
252
|
export const createNoopTuiActions = (): TuiActions => ({
|
|
147
253
|
toggleEvents: noop,
|
|
254
|
+
openAbout: noop,
|
|
255
|
+
closeAbout: noop,
|
|
148
256
|
jumpToRandomRoom: noop,
|
|
149
257
|
commitRoom: noop,
|
|
150
258
|
setRoomInput: noop,
|
|
151
259
|
jumpToNewSelf: noop,
|
|
152
260
|
commitName: noop,
|
|
153
261
|
setNameInput: noop,
|
|
262
|
+
setPeerSearch: noop,
|
|
154
263
|
toggleSelectReadyPeers: noop,
|
|
155
264
|
clearPeerSelection: noop,
|
|
156
265
|
toggleHideTerminalPeers: noop,
|
|
@@ -206,6 +315,16 @@ const transferStatusKind = (status: TransferSnapshot["status"]) => ({
|
|
|
206
315
|
error: "offline",
|
|
207
316
|
}[status] || "unknown") as "online" | "offline" | "away" | "busy" | "unknown"
|
|
208
317
|
const visiblePeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean) => hideTerminalPeers ? peers.filter(peer => peer.presence === "active") : peers
|
|
318
|
+
const peerSearchNeedle = (value: string) => `${value ?? ""}`.trim().toLowerCase()
|
|
319
|
+
const peerMatchesSearch = (peer: PeerSnapshot, search: string) => !search || peer.displayName.toLowerCase().includes(search)
|
|
320
|
+
export const renderedPeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean, search: string) => {
|
|
321
|
+
const needle = peerSearchNeedle(search)
|
|
322
|
+
return visiblePeers(peers, hideTerminalPeers)
|
|
323
|
+
.filter(peer => peerMatchesSearch(peer, needle))
|
|
324
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
325
|
+
}
|
|
326
|
+
export const renderedReadySelectedPeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean, search: string) =>
|
|
327
|
+
renderedPeers(peers, hideTerminalPeers, search).filter(peer => peer.selected && peer.ready)
|
|
209
328
|
const transferProgress = (transfer: TransferSnapshot) => Math.max(0, Math.min(1, transfer.progress / 100))
|
|
210
329
|
const isPendingOffer = (transfer: TransferSnapshot) => transfer.direction === "out" && (transfer.status === "queued" || transfer.status === "offered")
|
|
211
330
|
const statusVariant = (status: TransferSnapshot["status"]): BadgeVariant => ({
|
|
@@ -467,7 +586,7 @@ export const groupTransfersByPeer = (transfers: TransferSnapshot[], peers: PeerS
|
|
|
467
586
|
return [...groups.values()]
|
|
468
587
|
}
|
|
469
588
|
|
|
470
|
-
export const createInitialTuiState = (initialConfig: SessionConfig, showEvents = false): TuiState => {
|
|
589
|
+
export const createInitialTuiState = (initialConfig: SessionConfig, showEvents = false, launchOptions: Pick<TuiLaunchOptions, "clean" | "offer"> = {}): TuiState => {
|
|
471
590
|
const sessionSeed = normalizeSessionSeed(initialConfig)
|
|
472
591
|
const autoAcceptIncoming = initialConfig.autoAcceptIncoming ?? true
|
|
473
592
|
const autoSaveIncoming = initialConfig.autoSaveIncoming ?? true
|
|
@@ -479,6 +598,8 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
|
|
|
479
598
|
sessionSeed,
|
|
480
599
|
peerSelectionByRoom,
|
|
481
600
|
snapshot: session.snapshot(),
|
|
601
|
+
aboutOpen: false,
|
|
602
|
+
peerSearch: "",
|
|
482
603
|
focusedId: null,
|
|
483
604
|
roomInput: sessionSeed.room,
|
|
484
605
|
nameInput: visibleNameInput(sessionSeed.name),
|
|
@@ -487,10 +608,10 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
|
|
|
487
608
|
draftInputKeyVersion: 0,
|
|
488
609
|
filePreview: emptyFilePreviewState(),
|
|
489
610
|
drafts: [],
|
|
490
|
-
autoOfferOutgoing: true,
|
|
611
|
+
autoOfferOutgoing: launchOptions.offer ?? true,
|
|
491
612
|
autoAcceptIncoming,
|
|
492
613
|
autoSaveIncoming,
|
|
493
|
-
hideTerminalPeers: true,
|
|
614
|
+
hideTerminalPeers: launchOptions.clean ?? true,
|
|
494
615
|
eventsExpanded: showEvents,
|
|
495
616
|
offeringDrafts: false,
|
|
496
617
|
notice: { text: "Tab focus", variant: "info" },
|
|
@@ -579,9 +700,55 @@ const renderHeaderBrand = () => ui.row({ id: "brand-title", gap: 1, items: "cent
|
|
|
579
700
|
const renderHeader = (state: TuiState, actions: TuiActions) => denseSection({
|
|
580
701
|
id: "header-shell",
|
|
581
702
|
titleNode: renderHeaderBrand(),
|
|
582
|
-
actions: [
|
|
703
|
+
actions: [
|
|
704
|
+
toggleButton("toggle-events", "Events", state.eventsExpanded, actions.toggleEvents),
|
|
705
|
+
actionButton(ABOUT_TRIGGER_ID, "About", actions.openAbout),
|
|
706
|
+
],
|
|
583
707
|
}, [])
|
|
584
708
|
|
|
709
|
+
const renderAboutModal = (state: TuiState, actions: TuiActions) => {
|
|
710
|
+
const cliCommand = aboutCliCommand(state)
|
|
711
|
+
const currentWebUrl = aboutWebUrl(state)
|
|
712
|
+
const currentWebLabel = aboutWebLabel(state)
|
|
713
|
+
return ui.modal({
|
|
714
|
+
id: "about-modal",
|
|
715
|
+
title: ABOUT_TITLE,
|
|
716
|
+
content: ui.column({ gap: 1 }, [
|
|
717
|
+
ui.text(ABOUT_INTRO, { id: "about-intro", variant: "heading" }),
|
|
718
|
+
ui.text(ABOUT_SUMMARY, { id: "about-summary" }),
|
|
719
|
+
ui.text(ABOUT_RUNTIME, { id: "about-runtime" }),
|
|
720
|
+
ui.column({ gap: 0 }, [
|
|
721
|
+
ui.text(ABOUT_CLI_LABEL, { id: "about-cli-label", variant: "caption" }),
|
|
722
|
+
ui.text(cliCommand, { id: "about-current-cli" }),
|
|
723
|
+
]),
|
|
724
|
+
ui.column({ gap: 0 }, [
|
|
725
|
+
ui.text(ABOUT_WEB_LINK_LABEL, { id: "about-web-link-label", variant: "caption" }),
|
|
726
|
+
ui.link({
|
|
727
|
+
id: "about-current-web-link",
|
|
728
|
+
label: currentWebLabel,
|
|
729
|
+
accessibleLabel: "Open current web link",
|
|
730
|
+
url: currentWebUrl,
|
|
731
|
+
}),
|
|
732
|
+
]),
|
|
733
|
+
]),
|
|
734
|
+
actions: [
|
|
735
|
+
ui.link({
|
|
736
|
+
id: "about-elefunc-link",
|
|
737
|
+
label: "elefunc.com",
|
|
738
|
+
accessibleLabel: "Open Elefunc website",
|
|
739
|
+
url: ABOUT_ELEFUNC_URL,
|
|
740
|
+
}),
|
|
741
|
+
actionButton("close-about", "Close", actions.closeAbout, "primary"),
|
|
742
|
+
],
|
|
743
|
+
width: 72,
|
|
744
|
+
maxWidth: 84,
|
|
745
|
+
minWidth: 54,
|
|
746
|
+
initialFocus: "close-about",
|
|
747
|
+
returnFocusTo: ABOUT_TRIGGER_ID,
|
|
748
|
+
onClose: actions.closeAbout,
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
|
|
585
752
|
const renderRoomCard = (state: TuiState, actions: TuiActions) => denseSection({
|
|
586
753
|
id: "room-card",
|
|
587
754
|
}, [
|
|
@@ -700,18 +867,18 @@ const renderPeerRow = (peer: PeerSnapshot, turnShareEnabled: boolean, actions: T
|
|
|
700
867
|
])
|
|
701
868
|
|
|
702
869
|
const renderPeersCard = (state: TuiState, actions: TuiActions) => {
|
|
703
|
-
const peers =
|
|
704
|
-
const activeCount =
|
|
705
|
-
const selectedCount =
|
|
870
|
+
const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
|
|
871
|
+
const activeCount = peers.filter(peer => peer.presence === "active").length
|
|
872
|
+
const selectedCount = peers.filter(peer => peer.selectable && peer.selected).length
|
|
706
873
|
const canShareTurn = state.session.canShareTurn()
|
|
707
874
|
return denseSection({
|
|
708
875
|
id: "peers-card",
|
|
709
876
|
titleNode: ui.row({ id: "peers-title-row", gap: 1, items: "center" }, [
|
|
710
877
|
headingTextButton("share-turn-all-peers", "Peers", canShareTurn && !!activeCount ? actions.shareTurnWithAllPeers : undefined, {
|
|
711
878
|
focusable: canShareTurn && !!activeCount,
|
|
712
|
-
accessibleLabel: "share TURN with
|
|
879
|
+
accessibleLabel: "share TURN with matching active peers",
|
|
713
880
|
}),
|
|
714
|
-
ui.text(`${selectedCount}/${
|
|
881
|
+
ui.text(`${selectedCount}/${peers.length}`, { id: "peers-count-text", variant: "heading" }),
|
|
715
882
|
]),
|
|
716
883
|
flex: 1,
|
|
717
884
|
actions: [
|
|
@@ -720,10 +887,16 @@ const renderPeersCard = (state: TuiState, actions: TuiActions) => {
|
|
|
720
887
|
toggleButton("toggle-clean-peers", "Clean", state.hideTerminalPeers, actions.toggleHideTerminalPeers),
|
|
721
888
|
],
|
|
722
889
|
}, [
|
|
890
|
+
ui.input({
|
|
891
|
+
id: PEER_SEARCH_INPUT_ID,
|
|
892
|
+
value: state.peerSearch,
|
|
893
|
+
placeholder: "filter",
|
|
894
|
+
onInput: value => actions.setPeerSearch(value),
|
|
895
|
+
}),
|
|
723
896
|
ui.box({ id: "peers-list", flex: 1, minHeight: 0, overflow: "scroll", border: "none" }, [
|
|
724
897
|
peers.length
|
|
725
898
|
? ui.column({ gap: 0 }, peers.map(peer => renderPeerRow(peer, canShareTurn, actions)))
|
|
726
|
-
: ui.empty(`Waiting for peers in ${state.snapshot.room}...`),
|
|
899
|
+
: ui.empty(state.snapshot.peers.length ? "No peers match current filters." : `Waiting for peers in ${state.snapshot.room}...`),
|
|
727
900
|
]),
|
|
728
901
|
])
|
|
729
902
|
}
|
|
@@ -768,6 +941,7 @@ const renderFilePreviewRow = (match: FileSearchMatch, index: number, selected: b
|
|
|
768
941
|
offsetFileSearchMatchIndices(displayPrefix, match.indices),
|
|
769
942
|
{ id: `file-preview-path-${index}`, flex: 1 },
|
|
770
943
|
),
|
|
944
|
+
match.kind === "file" && typeof match.size === "number" ? ui.text(formatBytes(match.size), { style: { dim: true } }) : null,
|
|
771
945
|
match.kind === "directory" ? tightTag("dir", { variant: "info", bare: true }) : null,
|
|
772
946
|
])
|
|
773
947
|
|
|
@@ -999,7 +1173,7 @@ export const renderTuiView = (state: TuiState, actions: TuiActions): VNode => {
|
|
|
999
1173
|
gap: 0,
|
|
1000
1174
|
})
|
|
1001
1175
|
|
|
1002
|
-
|
|
1176
|
+
const basePage = state.pendingFocusTarget
|
|
1003
1177
|
? ui.focusTrap({
|
|
1004
1178
|
id: `focus-request-${state.focusRequestEpoch}`,
|
|
1005
1179
|
key: `focus-request-${state.focusRequestEpoch}`,
|
|
@@ -1007,6 +1181,10 @@ export const renderTuiView = (state: TuiState, actions: TuiActions): VNode => {
|
|
|
1007
1181
|
initialFocus: state.pendingFocusTarget,
|
|
1008
1182
|
}, [page])
|
|
1009
1183
|
: page
|
|
1184
|
+
|
|
1185
|
+
return state.aboutOpen
|
|
1186
|
+
? ui.layers([basePage, renderAboutModal(state, actions)])
|
|
1187
|
+
: basePage
|
|
1010
1188
|
}
|
|
1011
1189
|
|
|
1012
1190
|
const withNotice = (state: TuiState, notice: Notice): TuiState => ({ ...state, notice })
|
|
@@ -1021,10 +1199,11 @@ export const withAcceptedDraftInput = (state: TuiState, draftInput: string, file
|
|
|
1021
1199
|
focusRequestEpoch: state.focusRequestEpoch + 1,
|
|
1022
1200
|
}, notice)
|
|
1023
1201
|
|
|
1024
|
-
export const startTui = async (initialConfig: SessionConfig,
|
|
1202
|
+
export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiLaunchOptions = {}) => {
|
|
1025
1203
|
await installCheckboxClickPatch()
|
|
1026
|
-
const initialState = createInitialTuiState(initialConfig,
|
|
1204
|
+
const initialState = createInitialTuiState(initialConfig, !!launchOptions.events, launchOptions)
|
|
1027
1205
|
const app = createNodeApp<TuiState>({ initialState })
|
|
1206
|
+
const quitController = createQuitController()
|
|
1028
1207
|
let state = initialState
|
|
1029
1208
|
let unsubscribe = () => {}
|
|
1030
1209
|
let stopping = false
|
|
@@ -1064,11 +1243,7 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1064
1243
|
const requestStop = () => {
|
|
1065
1244
|
if (stopping) return
|
|
1066
1245
|
stopping = true
|
|
1067
|
-
|
|
1068
|
-
process.kill(process.pid, "SIGINT")
|
|
1069
|
-
} catch {
|
|
1070
|
-
void app.stop()
|
|
1071
|
-
}
|
|
1246
|
+
quitController.requestStop()
|
|
1072
1247
|
}
|
|
1073
1248
|
|
|
1074
1249
|
const resetFilePreview = (overrides: Partial<FilePreviewState> = {}): FilePreviewState => ({
|
|
@@ -1233,11 +1408,12 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1233
1408
|
|
|
1234
1409
|
const maybeOfferDrafts = () => {
|
|
1235
1410
|
if (!state.autoOfferOutgoing || !state.drafts.length || state.offeringDrafts) return
|
|
1236
|
-
|
|
1411
|
+
const targetPeerIds = renderedReadySelectedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch).map(peer => peer.id)
|
|
1412
|
+
if (!targetPeerIds.length) return
|
|
1237
1413
|
const session = state.session
|
|
1238
1414
|
const pendingDrafts = [...state.drafts]
|
|
1239
1415
|
commit(current => ({ ...current, offeringDrafts: true }))
|
|
1240
|
-
void session.
|
|
1416
|
+
void session.queueFiles(pendingDrafts.map(draft => draft.path), targetPeerIds).then(
|
|
1241
1417
|
ids => {
|
|
1242
1418
|
if (state.session !== session) return
|
|
1243
1419
|
const offeredIds = new Set(pendingDrafts.map(draft => draft.id))
|
|
@@ -1277,6 +1453,7 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1277
1453
|
sessionSeed: nextSeed,
|
|
1278
1454
|
peerSelectionByRoom: current.peerSelectionByRoom,
|
|
1279
1455
|
snapshot: nextSession.snapshot(),
|
|
1456
|
+
peerSearch: "",
|
|
1280
1457
|
roomInput: nextSeed.room,
|
|
1281
1458
|
nameInput: visibleNameInput(nextSeed.name),
|
|
1282
1459
|
draftInput: "",
|
|
@@ -1344,22 +1521,27 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1344
1521
|
|
|
1345
1522
|
const actions: TuiActions = {
|
|
1346
1523
|
toggleEvents: () => commit(current => ({ ...withNotice(current, { text: current.eventsExpanded ? "Events hidden." : "Events shown.", variant: "info" }), eventsExpanded: !current.eventsExpanded })),
|
|
1524
|
+
openAbout: () => commit(current => ({ ...current, aboutOpen: true })),
|
|
1525
|
+
closeAbout: () => commit(current => ({ ...current, aboutOpen: false })),
|
|
1347
1526
|
jumpToRandomRoom: () => replaceSession({ ...state.sessionSeed, room: uid(8) }, "Joined a new room."),
|
|
1348
1527
|
commitRoom,
|
|
1349
1528
|
setRoomInput: value => commit(current => ({ ...current, roomInput: value })),
|
|
1350
1529
|
jumpToNewSelf: () => replaceSession({ ...state.sessionSeed, localId: cleanLocalId(uid(8)) }, "Started a fresh self ID.", { reseedBootFocus: true }),
|
|
1351
1530
|
commitName,
|
|
1352
1531
|
setNameInput: value => commit(current => ({ ...current, nameInput: value })),
|
|
1532
|
+
setPeerSearch: value => commit(current => ({ ...current, peerSearch: value })),
|
|
1353
1533
|
toggleSelectReadyPeers: () => {
|
|
1534
|
+
const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
|
|
1354
1535
|
let changed = 0
|
|
1355
|
-
for (const peer of
|
|
1356
|
-
commit(current => withNotice(current, { text: changed ? "Selected ready peers." : "No ready peers to select.", variant: changed ? "success" : "info" }))
|
|
1536
|
+
for (const peer of peers) if (state.session.setPeerSelected(peer.id, peer.presence === "active" && peer.ready)) changed += 1
|
|
1537
|
+
commit(current => withNotice(current, { text: changed ? "Selected matching ready peers." : "No matching ready peers to select.", variant: changed ? "success" : "info" }))
|
|
1357
1538
|
maybeOfferDrafts()
|
|
1358
1539
|
},
|
|
1359
1540
|
clearPeerSelection: () => {
|
|
1541
|
+
const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
|
|
1360
1542
|
let changed = 0
|
|
1361
|
-
for (const peer of
|
|
1362
|
-
commit(current => withNotice(current, { text: changed ? `Cleared ${plural(changed, "peer selection")}.` : "No peer selections to clear.", variant: changed ? "warning" : "info" }))
|
|
1543
|
+
for (const peer of peers) if (state.session.setPeerSelected(peer.id, false)) changed += 1
|
|
1544
|
+
commit(current => withNotice(current, { text: changed ? `Cleared ${plural(changed, "matching peer selection")}.` : "No matching peer selections to clear.", variant: changed ? "warning" : "info" }))
|
|
1363
1545
|
},
|
|
1364
1546
|
toggleHideTerminalPeers: () => commit(current => withNotice({ ...current, hideTerminalPeers: !current.hideTerminalPeers }, { text: current.hideTerminalPeers ? "Terminal peers shown." : "Terminal peers hidden.", variant: "info" })),
|
|
1365
1547
|
togglePeer: peerId => {
|
|
@@ -1379,13 +1561,16 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1379
1561
|
}))
|
|
1380
1562
|
},
|
|
1381
1563
|
shareTurnWithAllPeers: () => {
|
|
1382
|
-
const
|
|
1564
|
+
const targetPeerIds = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
|
|
1565
|
+
.filter(peer => peer.presence === "active")
|
|
1566
|
+
.map(peer => peer.id)
|
|
1567
|
+
const shared = state.session.shareTurnWithPeers(targetPeerIds)
|
|
1383
1568
|
commit(current => withNotice(current, {
|
|
1384
1569
|
text: !state.session.canShareTurn()
|
|
1385
1570
|
? "TURN is not configured."
|
|
1386
1571
|
: shared
|
|
1387
|
-
? `Shared TURN with ${plural(shared, "
|
|
1388
|
-
: "No active peers to share TURN with.",
|
|
1572
|
+
? `Shared TURN with ${plural(shared, "matching peer")}.`
|
|
1573
|
+
: "No matching active peers to share TURN with.",
|
|
1389
1574
|
variant: !state.session.canShareTurn() ? "info" : shared ? "success" : "info",
|
|
1390
1575
|
}))
|
|
1391
1576
|
},
|
|
@@ -1563,17 +1748,31 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
|
|
|
1563
1748
|
stopping = true
|
|
1564
1749
|
unsubscribe()
|
|
1565
1750
|
stopPreviewSession()
|
|
1751
|
+
try {
|
|
1752
|
+
await app.stop()
|
|
1753
|
+
} catch {}
|
|
1566
1754
|
await state.session.close()
|
|
1567
1755
|
}
|
|
1568
1756
|
|
|
1757
|
+
const printResumeOutput = async () => {
|
|
1758
|
+
const output = `${resumeOutputLines(state).join("\n")}\n`
|
|
1759
|
+
await new Promise<void>(resolve => process.stdout.write(output, () => resolve()))
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1569
1762
|
try {
|
|
1570
1763
|
bindSession(state.session)
|
|
1571
|
-
await app.
|
|
1764
|
+
await app.start()
|
|
1765
|
+
await quitController.promise
|
|
1572
1766
|
} finally {
|
|
1573
1767
|
try {
|
|
1574
1768
|
await stop()
|
|
1575
1769
|
} finally {
|
|
1576
|
-
|
|
1770
|
+
try {
|
|
1771
|
+
app.dispose()
|
|
1772
|
+
} finally {
|
|
1773
|
+
await printResumeOutput()
|
|
1774
|
+
quitController.detach()
|
|
1775
|
+
}
|
|
1577
1776
|
}
|
|
1578
1777
|
}
|
|
1579
1778
|
}
|
package/src/tui/file-search.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface IndexedEntry {
|
|
|
8
8
|
relativePath: string
|
|
9
9
|
fileName: string
|
|
10
10
|
kind: "file" | "directory"
|
|
11
|
+
size?: number
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface FileSearchScope {
|
|
@@ -187,6 +188,7 @@ const browseEntries = (entries: readonly IndexedEntry[], resultLimit: number) =>
|
|
|
187
188
|
absolutePath: entry.absolutePath,
|
|
188
189
|
fileName: entry.fileName,
|
|
189
190
|
kind: entry.kind,
|
|
191
|
+
size: entry.size,
|
|
190
192
|
score: 0,
|
|
191
193
|
indices: [],
|
|
192
194
|
} satisfies FileSearchMatch))
|
|
@@ -203,6 +205,7 @@ export const searchEntries = (entries: readonly IndexedEntry[], query: string, r
|
|
|
203
205
|
absolutePath: entry.absolutePath,
|
|
204
206
|
fileName: entry.fileName,
|
|
205
207
|
kind: entry.kind,
|
|
208
|
+
size: entry.size,
|
|
206
209
|
score: match.score,
|
|
207
210
|
indices: match.indices,
|
|
208
211
|
})
|
|
@@ -272,6 +275,7 @@ export const crawlWorkspaceEntries = async (workspaceRoot: string, onEntry: (ent
|
|
|
272
275
|
relativePath: normalizeRelativePath(relative(root, absolutePath)),
|
|
273
276
|
fileName: child.name,
|
|
274
277
|
kind: "file",
|
|
278
|
+
size: info.size,
|
|
275
279
|
})
|
|
276
280
|
}
|
|
277
281
|
} catch {
|