@elefunc/send 0.1.15 → 0.1.17
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/tui/app.ts +110 -40
package/package.json
CHANGED
package/src/tui/app.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path"
|
|
2
|
-
import { rgb, ui, type BadgeVariant, type TextStyle, type UiEvent, type VNode } from "@rezi-ui/core"
|
|
2
|
+
import { BACKEND_RAW_WRITE_MARKER, rgb, ui, type BackendRawWrite, type BadgeVariant, type TextStyle, type UiEvent, type VNode } from "@rezi-ui/core"
|
|
3
3
|
import { createNodeApp } from "@rezi-ui/node"
|
|
4
4
|
import { applyInputEditEvent } from "../../node_modules/@rezi-ui/core/dist/runtime/inputEditor.js"
|
|
5
5
|
import { inspectLocalFile } from "../core/files"
|
|
@@ -56,6 +56,7 @@ export interface TuiState {
|
|
|
56
56
|
peerSelectionByRoom: Map<string, Map<string, boolean>>
|
|
57
57
|
snapshot: SessionSnapshot
|
|
58
58
|
aboutOpen: boolean
|
|
59
|
+
inviteDropdownOpen: boolean
|
|
59
60
|
peerSearch: string
|
|
60
61
|
focusedId: string | null
|
|
61
62
|
roomInput: string
|
|
@@ -81,6 +82,10 @@ export interface TuiActions {
|
|
|
81
82
|
toggleEvents: TuiAction
|
|
82
83
|
openAbout: TuiAction
|
|
83
84
|
closeAbout: TuiAction
|
|
85
|
+
toggleInviteDropdown: TuiAction
|
|
86
|
+
closeInviteDropdown: TuiAction
|
|
87
|
+
copyWebInvite: TuiAction
|
|
88
|
+
copyCliInvite: TuiAction
|
|
84
89
|
jumpToRandomRoom: TuiAction
|
|
85
90
|
commitRoom: TuiAction
|
|
86
91
|
setRoomInput: (value: string) => void
|
|
@@ -115,21 +120,22 @@ const ROOM_INPUT_ID = "room-input"
|
|
|
115
120
|
const NAME_INPUT_ID = "name-input"
|
|
116
121
|
const PEER_SEARCH_INPUT_ID = "peer-search-input"
|
|
117
122
|
const DRAFT_INPUT_ID = "draft-input"
|
|
123
|
+
const ROOM_INVITE_BUTTON_ID = "room-invite-button"
|
|
124
|
+
const INVITE_DROPDOWN_ID = "room-invite-dropdown"
|
|
118
125
|
const ABOUT_TRIGGER_ID = "open-about"
|
|
119
126
|
const TRANSPARENT_BORDER_STYLE = { fg: rgb(7, 10, 12) } as const
|
|
120
127
|
const METRIC_BORDER_STYLE = { fg: rgb(20, 25, 32) } as const
|
|
121
128
|
const PRIMARY_TEXT_STYLE = { fg: rgb(255, 255, 255) } as const
|
|
122
129
|
const HEADING_TEXT_STYLE = { fg: rgb(255, 255, 255), bold: true } as const
|
|
123
130
|
const MUTED_TEXT_STYLE = { fg: rgb(159, 166, 178) } as const
|
|
124
|
-
const DEFAULT_WEB_URL = "https://
|
|
131
|
+
const DEFAULT_WEB_URL = "https://rtme.sh/"
|
|
125
132
|
const DEFAULT_SAVE_DIR = resolve(process.cwd())
|
|
126
|
-
const ABOUT_ELEFUNC_URL = "https://
|
|
133
|
+
const ABOUT_ELEFUNC_URL = "https://rtme.sh/send"
|
|
127
134
|
const ABOUT_TITLE = "About Send"
|
|
128
135
|
const ABOUT_INTRO = "Peer-to-Peer Transfers – Web & CLI"
|
|
129
136
|
const ABOUT_SUMMARY = "Join the same room, see who is there, and offer files directly to selected peers."
|
|
130
137
|
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."
|
|
131
|
-
const
|
|
132
|
-
const ABOUT_WEB_LINK_LABEL = "Web"
|
|
138
|
+
const COPY_SERVICE_URL = "https://copy.rt.ht/"
|
|
133
139
|
const TRANSFER_DIRECTION_ARROW = {
|
|
134
140
|
out: { glyph: "↗", style: { fg: rgb(170, 217, 76), bold: true } },
|
|
135
141
|
in: { glyph: "↙", style: { fg: rgb(240, 113, 120), bold: true } },
|
|
@@ -157,6 +163,12 @@ type ProcessSignalLike = {
|
|
|
157
163
|
off?: (signal: TuiQuitSignal, handler: () => void) => unknown
|
|
158
164
|
removeListener?: (signal: TuiQuitSignal, handler: () => void) => unknown
|
|
159
165
|
}
|
|
166
|
+
type BunSpawn = (cmd: string[], options: {
|
|
167
|
+
stdin?: "pipe" | "inherit" | "ignore"
|
|
168
|
+
stdout?: "pipe" | "inherit" | "ignore"
|
|
169
|
+
stderr?: "pipe" | "inherit" | "ignore"
|
|
170
|
+
}) => { unref?: () => void }
|
|
171
|
+
type BunLike = { spawn?: BunSpawn }
|
|
160
172
|
type ShareUrlState = Pick<TuiState, "snapshot" | "hideTerminalPeers" | "autoAcceptIncoming" | "autoOfferOutgoing" | "autoSaveIncoming">
|
|
161
173
|
type ShareCliState = ShareUrlState & Pick<TuiState, "sessionSeed" | "eventsExpanded">
|
|
162
174
|
const shellQuote = (value: string) => safeShellArgPattern.test(value) ? value : `'${value.replaceAll("'", `'\"'\"'`)}'`
|
|
@@ -203,8 +215,29 @@ const renderWebUrl = (state: ShareUrlState, baseUrl = DEFAULT_WEB_URL, omitDefau
|
|
|
203
215
|
url.hash = buildHashParams(state, omitDefaults).toString()
|
|
204
216
|
return url.toString()
|
|
205
217
|
}
|
|
218
|
+
const schemeLessUrlText = (text: string) => text.replace(/^[a-z]+:\/\//, "")
|
|
219
|
+
export const inviteWebLabel = (state: ShareUrlState, baseUrl = resolveWebUrlBase()) => schemeLessUrlText(webInviteUrl(state, baseUrl))
|
|
220
|
+
export const inviteCliPackageName = (baseUrl = resolveWebUrlBase()) => new URL(resolveWebUrlBase(baseUrl)).hostname
|
|
221
|
+
export const inviteCliCommand = (state: ShareUrlState) => {
|
|
222
|
+
const args: string[] = []
|
|
223
|
+
appendCliFlag(args, "--room", state.snapshot.room)
|
|
224
|
+
appendToggleCliFlags(args, state)
|
|
225
|
+
return args.join(" ")
|
|
226
|
+
}
|
|
227
|
+
export const inviteCliText = (state: ShareUrlState, baseUrl = resolveWebUrlBase()) => `bunx ${inviteCliPackageName(baseUrl)} ${inviteCliCommand(state)}`
|
|
228
|
+
export const inviteCopyUrl = (text: string) => `${COPY_SERVICE_URL}#${new URLSearchParams({ text })}`
|
|
229
|
+
export const buildOsc52ClipboardSequence = (text: string) => text ? `\u001b]52;c;${Buffer.from(text).toString("base64")}\u0007` : ""
|
|
230
|
+
export const externalOpenCommand = (url: string, platform = process.platform) =>
|
|
231
|
+
platform === "darwin" ? ["open", url]
|
|
232
|
+
: platform === "win32" ? ["cmd.exe", "/c", "start", "", url]
|
|
233
|
+
: ["xdg-open", url]
|
|
234
|
+
const getBackendRawWriter = (backend: unknown): BackendRawWrite | null => {
|
|
235
|
+
const marker = (backend as Record<string, unknown>)[BACKEND_RAW_WRITE_MARKER]
|
|
236
|
+
return typeof marker === "function" ? marker as BackendRawWrite : null
|
|
237
|
+
}
|
|
238
|
+
const getBunRuntime = () => (globalThis as typeof globalThis & { Bun?: BunLike }).Bun ?? null
|
|
206
239
|
const renderCliCommand = (state: ShareCliState, { includeSelf = false, includePrefix = false } = {}) => {
|
|
207
|
-
const args = includePrefix ? ["bunx", "
|
|
240
|
+
const args = includePrefix ? ["bunx", "rtme.sh"] : []
|
|
208
241
|
appendCliFlag(args, "--room", state.snapshot.room)
|
|
209
242
|
if (includeSelf) appendCliFlag(args, "--self", displayPeerName(state.snapshot.name, state.snapshot.localId))
|
|
210
243
|
appendToggleCliFlags(args, state)
|
|
@@ -267,6 +300,10 @@ export const createNoopTuiActions = (): TuiActions => ({
|
|
|
267
300
|
toggleEvents: noop,
|
|
268
301
|
openAbout: noop,
|
|
269
302
|
closeAbout: noop,
|
|
303
|
+
toggleInviteDropdown: noop,
|
|
304
|
+
closeInviteDropdown: noop,
|
|
305
|
+
copyWebInvite: noop,
|
|
306
|
+
copyCliInvite: noop,
|
|
270
307
|
jumpToRandomRoom: noop,
|
|
271
308
|
commitRoom: noop,
|
|
272
309
|
setRoomInput: noop,
|
|
@@ -702,6 +739,7 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
|
|
|
702
739
|
peerSelectionByRoom,
|
|
703
740
|
snapshot: session.snapshot(),
|
|
704
741
|
aboutOpen: false,
|
|
742
|
+
inviteDropdownOpen: false,
|
|
705
743
|
peerSearch: "",
|
|
706
744
|
focusedId: null,
|
|
707
745
|
roomInput: sessionSeed.room,
|
|
@@ -810,12 +848,7 @@ const renderHeader = (state: TuiState, actions: TuiActions) => denseSection({
|
|
|
810
848
|
],
|
|
811
849
|
}, [])
|
|
812
850
|
|
|
813
|
-
const renderAboutModal = (
|
|
814
|
-
const cliCommand = aboutCliCommand(state)
|
|
815
|
-
const cliCopyText = `${ABOUT_CLI_LABEL} ${cliCommand}`
|
|
816
|
-
const cliCopyUrl = `https://copy.rt.ht/#${new URLSearchParams({ text: cliCopyText })}`
|
|
817
|
-
const currentWebUrl = aboutWebUrl(state)
|
|
818
|
-
const currentWebLabel = aboutWebLabel(state)
|
|
851
|
+
const renderAboutModal = (_state: TuiState, actions: TuiActions) => {
|
|
819
852
|
return ui.modal({
|
|
820
853
|
id: "about-modal",
|
|
821
854
|
title: ABOUT_TITLE,
|
|
@@ -823,30 +856,12 @@ const renderAboutModal = (state: TuiState, actions: TuiActions) => {
|
|
|
823
856
|
ui.text(ABOUT_INTRO, { id: "about-intro", variant: "heading", wrap: true }),
|
|
824
857
|
ui.text(ABOUT_SUMMARY, { id: "about-summary", wrap: true }),
|
|
825
858
|
ui.text(ABOUT_RUNTIME, { id: "about-runtime", wrap: true }),
|
|
826
|
-
ui.column({ gap: 0 }, [
|
|
827
|
-
ui.text(ABOUT_CLI_LABEL, { id: "about-cli-label", variant: "caption", wrap: true }),
|
|
828
|
-
ui.link({
|
|
829
|
-
id: "about-current-cli",
|
|
830
|
-
label: cliCommand,
|
|
831
|
-
accessibleLabel: "Copy current CLI command",
|
|
832
|
-
url: cliCopyUrl,
|
|
833
|
-
}),
|
|
834
|
-
]),
|
|
835
|
-
ui.column({ gap: 0 }, [
|
|
836
|
-
ui.text(ABOUT_WEB_LINK_LABEL, { id: "about-web-link-label", variant: "caption", wrap: true }),
|
|
837
|
-
ui.link({
|
|
838
|
-
id: "about-current-web-link",
|
|
839
|
-
label: currentWebLabel,
|
|
840
|
-
accessibleLabel: "Open current web link",
|
|
841
|
-
url: currentWebUrl,
|
|
842
|
-
}),
|
|
843
|
-
]),
|
|
844
859
|
]),
|
|
845
860
|
actions: [
|
|
846
861
|
ui.link({
|
|
847
862
|
id: "about-elefunc-link",
|
|
848
|
-
label: "
|
|
849
|
-
accessibleLabel: "Open
|
|
863
|
+
label: "rtme.sh/send",
|
|
864
|
+
accessibleLabel: "Open rtme.sh Send page",
|
|
850
865
|
url: ABOUT_ELEFUNC_URL,
|
|
851
866
|
}),
|
|
852
867
|
actionButton("close-about", "Close", actions.closeAbout, "primary"),
|
|
@@ -862,6 +877,18 @@ const renderAboutModal = (state: TuiState, actions: TuiActions) => {
|
|
|
862
877
|
})
|
|
863
878
|
}
|
|
864
879
|
|
|
880
|
+
const renderInviteDropdown = (state: TuiState, actions: TuiActions) => ui.dropdown({
|
|
881
|
+
id: INVITE_DROPDOWN_ID,
|
|
882
|
+
anchorId: ROOM_INVITE_BUTTON_ID,
|
|
883
|
+
position: "below-end",
|
|
884
|
+
items: [
|
|
885
|
+
{ id: "cli", label: "CLI", shortcut: inviteCliText(state) },
|
|
886
|
+
{ id: "web", label: "WEB", shortcut: inviteWebLabel(state) },
|
|
887
|
+
],
|
|
888
|
+
onSelect: item => { if (item.id === "web") actions.copyWebInvite(); if (item.id === "cli") actions.copyCliInvite() },
|
|
889
|
+
onClose: actions.closeInviteDropdown,
|
|
890
|
+
})
|
|
891
|
+
|
|
865
892
|
const renderRoomCard = (state: TuiState, actions: TuiActions) => denseSection({
|
|
866
893
|
id: "room-card",
|
|
867
894
|
}, [
|
|
@@ -877,11 +904,13 @@ const renderRoomCard = (state: TuiState, actions: TuiActions) => denseSection({
|
|
|
877
904
|
}),
|
|
878
905
|
]),
|
|
879
906
|
ui.row({ id: "room-invite-slot", width: 6, justify: "center", items: "center" }, [
|
|
880
|
-
ui.
|
|
881
|
-
id:
|
|
907
|
+
ui.button({
|
|
908
|
+
id: ROOM_INVITE_BUTTON_ID,
|
|
882
909
|
label: "📋",
|
|
883
|
-
accessibleLabel: "Open invite
|
|
884
|
-
|
|
910
|
+
accessibleLabel: "Open invite links",
|
|
911
|
+
onPress: actions.toggleInviteDropdown,
|
|
912
|
+
dsVariant: "ghost",
|
|
913
|
+
intent: "secondary",
|
|
885
914
|
}),
|
|
886
915
|
]),
|
|
887
916
|
]),
|
|
@@ -1305,9 +1334,12 @@ export const renderTuiView = (state: TuiState, actions: TuiActions): VNode => {
|
|
|
1305
1334
|
}, [page])
|
|
1306
1335
|
: page
|
|
1307
1336
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
:
|
|
1337
|
+
const overlays = [
|
|
1338
|
+
state.inviteDropdownOpen && !state.aboutOpen ? renderInviteDropdown(state, actions) : null,
|
|
1339
|
+
state.aboutOpen ? renderAboutModal(state, actions) : null,
|
|
1340
|
+
].filter((overlay): overlay is VNode => !!overlay)
|
|
1341
|
+
|
|
1342
|
+
return overlays.length ? ui.layers([basePage, ...overlays]) : basePage
|
|
1311
1343
|
}
|
|
1312
1344
|
|
|
1313
1345
|
const withNotice = (state: TuiState, notice: Notice): TuiState => ({ ...state, notice })
|
|
@@ -1339,6 +1371,38 @@ export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiL
|
|
|
1339
1371
|
let previewSessionRoot: string | null = null
|
|
1340
1372
|
let draftCursor = state.draftInput.length
|
|
1341
1373
|
let draftCursorBeforeEvent = draftCursor
|
|
1374
|
+
let osc52SupportPromise: Promise<boolean> | null = null
|
|
1375
|
+
|
|
1376
|
+
const ensureOsc52Support = () => osc52SupportPromise ??= app.backend.getCaps().then(caps => caps.supportsOsc52, () => false)
|
|
1377
|
+
const openExternalUrl = (url: string) => {
|
|
1378
|
+
try {
|
|
1379
|
+
const bun = getBunRuntime()
|
|
1380
|
+
const child = bun?.spawn?.(externalOpenCommand(url), { stdin: "ignore", stdout: "ignore", stderr: "ignore" })
|
|
1381
|
+
if (!child) return false
|
|
1382
|
+
child.unref?.()
|
|
1383
|
+
return true
|
|
1384
|
+
} catch {
|
|
1385
|
+
return false
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const copyInvitePayload = async (payload: string, label: "WEB" | "CLI") => {
|
|
1389
|
+
const closeDropdown = (notice: Notice) => commit(current => withNotice({ ...current, inviteDropdownOpen: false }, notice))
|
|
1390
|
+
const rawWrite = getBackendRawWriter(app.backend)
|
|
1391
|
+
if (rawWrite && await ensureOsc52Support()) {
|
|
1392
|
+
const sequence = buildOsc52ClipboardSequence(payload)
|
|
1393
|
+
if (sequence) {
|
|
1394
|
+
try {
|
|
1395
|
+
rawWrite(sequence)
|
|
1396
|
+
closeDropdown({ text: `Copied ${label} invite.`, variant: "success" })
|
|
1397
|
+
return
|
|
1398
|
+
} catch {}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const opened = openExternalUrl(inviteCopyUrl(payload))
|
|
1402
|
+
closeDropdown(opened
|
|
1403
|
+
? { text: `Opened ${label} copy link.`, variant: "info" }
|
|
1404
|
+
: { text: `Unable to copy ${label} invite.`, variant: "error" })
|
|
1405
|
+
}
|
|
1342
1406
|
|
|
1343
1407
|
const flushUpdate = () => {
|
|
1344
1408
|
if (updateQueued || stopping || cleanedUp) return
|
|
@@ -1603,6 +1667,7 @@ export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiL
|
|
|
1603
1667
|
sessionSeed: nextSeed,
|
|
1604
1668
|
peerSelectionByRoom: current.peerSelectionByRoom,
|
|
1605
1669
|
snapshot: nextSession.snapshot(),
|
|
1670
|
+
inviteDropdownOpen: false,
|
|
1606
1671
|
peerSearch: "",
|
|
1607
1672
|
roomInput: nextSeed.room,
|
|
1608
1673
|
nameInput: visibleNameInput(nextSeed.name),
|
|
@@ -1680,8 +1745,12 @@ export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiL
|
|
|
1680
1745
|
|
|
1681
1746
|
const actions: TuiActions = {
|
|
1682
1747
|
toggleEvents: () => commit(current => ({ ...withNotice(current, { text: current.eventsExpanded ? "Events hidden." : "Events shown.", variant: "info" }), eventsExpanded: !current.eventsExpanded })),
|
|
1683
|
-
openAbout: () => commit(current => ({ ...current, aboutOpen: true })),
|
|
1748
|
+
openAbout: () => commit(current => ({ ...current, aboutOpen: true, inviteDropdownOpen: false })),
|
|
1684
1749
|
closeAbout: () => commit(current => ({ ...current, aboutOpen: false })),
|
|
1750
|
+
toggleInviteDropdown: () => commit(current => ({ ...current, inviteDropdownOpen: !current.inviteDropdownOpen })),
|
|
1751
|
+
closeInviteDropdown: () => commit(current => current.inviteDropdownOpen ? { ...current, inviteDropdownOpen: false } : current),
|
|
1752
|
+
copyWebInvite: () => { void copyInvitePayload(webInviteUrl(state), "WEB") },
|
|
1753
|
+
copyCliInvite: () => { void copyInvitePayload(inviteCliText(state), "CLI") },
|
|
1685
1754
|
jumpToRandomRoom: () => replaceSession({ ...state.sessionSeed, room: uid(8) }, "Joined a new room."),
|
|
1686
1755
|
commitRoom,
|
|
1687
1756
|
setRoomInput: value => commit(current => ({ ...current, roomInput: value })),
|
|
@@ -1953,6 +2022,7 @@ export const startTui = async (initialConfig: SessionConfig, launchOptions: TuiL
|
|
|
1953
2022
|
try {
|
|
1954
2023
|
bindSession(state.session)
|
|
1955
2024
|
await app.start()
|
|
2025
|
+
void ensureOsc52Support()
|
|
1956
2026
|
await quitController.promise
|
|
1957
2027
|
} finally {
|
|
1958
2028
|
try {
|