@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tui/app.ts +110 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elefunc/send",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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://send.rt.ht/"
131
+ const DEFAULT_WEB_URL = "https://rtme.sh/"
125
132
  const DEFAULT_SAVE_DIR = resolve(process.cwd())
126
- const ABOUT_ELEFUNC_URL = "https://elefunc.com/send"
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 ABOUT_CLI_LABEL = "bunx @elefunc/send@latest"
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", "@elefunc/send@latest"] : []
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 = (state: TuiState, actions: TuiActions) => {
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: "elefunc.com/send",
849
- accessibleLabel: "Open Elefunc Send page",
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.link({
881
- id: "room-invite-link",
907
+ ui.button({
908
+ id: ROOM_INVITE_BUTTON_ID,
882
909
  label: "📋",
883
- accessibleLabel: "Open invite link",
884
- url: webInviteUrl(state),
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
- return state.aboutOpen
1309
- ? ui.layers([basePage, renderAboutModal(state, actions)])
1310
- : basePage
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 {