@elefunc/send 0.1.6 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elefunc/send",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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/tui/app.ts CHANGED
@@ -45,6 +45,7 @@ export interface TuiState {
45
45
  sessionSeed: SessionSeed
46
46
  peerSelectionByRoom: Map<string, Map<string, boolean>>
47
47
  snapshot: SessionSnapshot
48
+ peerSearch: string
48
49
  focusedId: string | null
49
50
  roomInput: string
50
51
  nameInput: string
@@ -72,6 +73,7 @@ export interface TuiActions {
72
73
  jumpToNewSelf: TuiAction
73
74
  commitName: TuiAction
74
75
  setNameInput: (value: string) => void
76
+ setPeerSearch: (value: string) => void
75
77
  toggleSelectReadyPeers: TuiAction
76
78
  clearPeerSelection: TuiAction
77
79
  toggleHideTerminalPeers: TuiAction
@@ -97,6 +99,7 @@ export interface TuiActions {
97
99
 
98
100
  const ROOM_INPUT_ID = "room-input"
99
101
  const NAME_INPUT_ID = "name-input"
102
+ const PEER_SEARCH_INPUT_ID = "peer-search-input"
100
103
  const DRAFT_INPUT_ID = "draft-input"
101
104
  const TRANSPARENT_BORDER_STYLE = { fg: rgb(7, 10, 12) } as const
102
105
  const METRIC_BORDER_STYLE = { fg: rgb(20, 25, 32) } as const
@@ -151,6 +154,7 @@ export const createNoopTuiActions = (): TuiActions => ({
151
154
  jumpToNewSelf: noop,
152
155
  commitName: noop,
153
156
  setNameInput: noop,
157
+ setPeerSearch: noop,
154
158
  toggleSelectReadyPeers: noop,
155
159
  clearPeerSelection: noop,
156
160
  toggleHideTerminalPeers: noop,
@@ -206,6 +210,16 @@ const transferStatusKind = (status: TransferSnapshot["status"]) => ({
206
210
  error: "offline",
207
211
  }[status] || "unknown") as "online" | "offline" | "away" | "busy" | "unknown"
208
212
  const visiblePeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean) => hideTerminalPeers ? peers.filter(peer => peer.presence === "active") : peers
213
+ const peerSearchNeedle = (value: string) => `${value ?? ""}`.trim().toLowerCase()
214
+ const peerMatchesSearch = (peer: PeerSnapshot, search: string) => !search || peer.displayName.toLowerCase().includes(search)
215
+ export const renderedPeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean, search: string) => {
216
+ const needle = peerSearchNeedle(search)
217
+ return visiblePeers(peers, hideTerminalPeers)
218
+ .filter(peer => peerMatchesSearch(peer, needle))
219
+ .sort((left, right) => left.id.localeCompare(right.id))
220
+ }
221
+ export const renderedReadySelectedPeers = (peers: PeerSnapshot[], hideTerminalPeers: boolean, search: string) =>
222
+ renderedPeers(peers, hideTerminalPeers, search).filter(peer => peer.selected && peer.ready)
209
223
  const transferProgress = (transfer: TransferSnapshot) => Math.max(0, Math.min(1, transfer.progress / 100))
210
224
  const isPendingOffer = (transfer: TransferSnapshot) => transfer.direction === "out" && (transfer.status === "queued" || transfer.status === "offered")
211
225
  const statusVariant = (status: TransferSnapshot["status"]): BadgeVariant => ({
@@ -479,6 +493,7 @@ export const createInitialTuiState = (initialConfig: SessionConfig, showEvents =
479
493
  sessionSeed,
480
494
  peerSelectionByRoom,
481
495
  snapshot: session.snapshot(),
496
+ peerSearch: "",
482
497
  focusedId: null,
483
498
  roomInput: sessionSeed.room,
484
499
  nameInput: visibleNameInput(sessionSeed.name),
@@ -700,18 +715,18 @@ const renderPeerRow = (peer: PeerSnapshot, turnShareEnabled: boolean, actions: T
700
715
  ])
701
716
 
702
717
  const renderPeersCard = (state: TuiState, actions: TuiActions) => {
703
- const peers = visiblePeers(state.snapshot.peers, state.hideTerminalPeers)
704
- const activeCount = state.snapshot.peers.filter(peer => peer.presence === "active").length
705
- const selectedCount = state.snapshot.peers.filter(peer => peer.selectable && peer.selected).length
718
+ const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
719
+ const activeCount = peers.filter(peer => peer.presence === "active").length
720
+ const selectedCount = peers.filter(peer => peer.selectable && peer.selected).length
706
721
  const canShareTurn = state.session.canShareTurn()
707
722
  return denseSection({
708
723
  id: "peers-card",
709
724
  titleNode: ui.row({ id: "peers-title-row", gap: 1, items: "center" }, [
710
725
  headingTextButton("share-turn-all-peers", "Peers", canShareTurn && !!activeCount ? actions.shareTurnWithAllPeers : undefined, {
711
726
  focusable: canShareTurn && !!activeCount,
712
- accessibleLabel: "share TURN with all active peers",
727
+ accessibleLabel: "share TURN with matching active peers",
713
728
  }),
714
- ui.text(`${selectedCount}/${activeCount}`, { id: "peers-count-text", variant: "heading" }),
729
+ ui.text(`${selectedCount}/${peers.length}`, { id: "peers-count-text", variant: "heading" }),
715
730
  ]),
716
731
  flex: 1,
717
732
  actions: [
@@ -720,10 +735,16 @@ const renderPeersCard = (state: TuiState, actions: TuiActions) => {
720
735
  toggleButton("toggle-clean-peers", "Clean", state.hideTerminalPeers, actions.toggleHideTerminalPeers),
721
736
  ],
722
737
  }, [
738
+ ui.input({
739
+ id: PEER_SEARCH_INPUT_ID,
740
+ value: state.peerSearch,
741
+ placeholder: "filter",
742
+ onInput: value => actions.setPeerSearch(value),
743
+ }),
723
744
  ui.box({ id: "peers-list", flex: 1, minHeight: 0, overflow: "scroll", border: "none" }, [
724
745
  peers.length
725
746
  ? ui.column({ gap: 0 }, peers.map(peer => renderPeerRow(peer, canShareTurn, actions)))
726
- : ui.empty(`Waiting for peers in ${state.snapshot.room}...`),
747
+ : ui.empty(state.snapshot.peers.length ? "No peers match current filters." : `Waiting for peers in ${state.snapshot.room}...`),
727
748
  ]),
728
749
  ])
729
750
  }
@@ -768,6 +789,7 @@ const renderFilePreviewRow = (match: FileSearchMatch, index: number, selected: b
768
789
  offsetFileSearchMatchIndices(displayPrefix, match.indices),
769
790
  { id: `file-preview-path-${index}`, flex: 1 },
770
791
  ),
792
+ match.kind === "file" && typeof match.size === "number" ? ui.text(formatBytes(match.size), { style: { dim: true } }) : null,
771
793
  match.kind === "directory" ? tightTag("dir", { variant: "info", bare: true }) : null,
772
794
  ])
773
795
 
@@ -1233,11 +1255,12 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
1233
1255
 
1234
1256
  const maybeOfferDrafts = () => {
1235
1257
  if (!state.autoOfferOutgoing || !state.drafts.length || state.offeringDrafts) return
1236
- if (!state.snapshot.peers.some(peer => peer.presence === "active" && peer.ready && peer.selected)) return
1258
+ const targetPeerIds = renderedReadySelectedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch).map(peer => peer.id)
1259
+ if (!targetPeerIds.length) return
1237
1260
  const session = state.session
1238
1261
  const pendingDrafts = [...state.drafts]
1239
1262
  commit(current => ({ ...current, offeringDrafts: true }))
1240
- void session.offerToSelectedPeers(pendingDrafts.map(draft => draft.path)).then(
1263
+ void session.queueFiles(pendingDrafts.map(draft => draft.path), targetPeerIds).then(
1241
1264
  ids => {
1242
1265
  if (state.session !== session) return
1243
1266
  const offeredIds = new Set(pendingDrafts.map(draft => draft.id))
@@ -1277,6 +1300,7 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
1277
1300
  sessionSeed: nextSeed,
1278
1301
  peerSelectionByRoom: current.peerSelectionByRoom,
1279
1302
  snapshot: nextSession.snapshot(),
1303
+ peerSearch: "",
1280
1304
  roomInput: nextSeed.room,
1281
1305
  nameInput: visibleNameInput(nextSeed.name),
1282
1306
  draftInput: "",
@@ -1350,16 +1374,19 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
1350
1374
  jumpToNewSelf: () => replaceSession({ ...state.sessionSeed, localId: cleanLocalId(uid(8)) }, "Started a fresh self ID.", { reseedBootFocus: true }),
1351
1375
  commitName,
1352
1376
  setNameInput: value => commit(current => ({ ...current, nameInput: value })),
1377
+ setPeerSearch: value => commit(current => ({ ...current, peerSearch: value })),
1353
1378
  toggleSelectReadyPeers: () => {
1379
+ const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
1354
1380
  let changed = 0
1355
- for (const peer of state.snapshot.peers) if (state.session.setPeerSelected(peer.id, peer.presence === "active" && peer.ready)) changed += 1
1356
- commit(current => withNotice(current, { text: changed ? "Selected ready peers." : "No ready peers to select.", variant: changed ? "success" : "info" }))
1381
+ for (const peer of peers) if (state.session.setPeerSelected(peer.id, peer.presence === "active" && peer.ready)) changed += 1
1382
+ commit(current => withNotice(current, { text: changed ? "Selected matching ready peers." : "No matching ready peers to select.", variant: changed ? "success" : "info" }))
1357
1383
  maybeOfferDrafts()
1358
1384
  },
1359
1385
  clearPeerSelection: () => {
1386
+ const peers = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
1360
1387
  let changed = 0
1361
- for (const peer of state.snapshot.peers) if (state.session.setPeerSelected(peer.id, false)) changed += 1
1362
- commit(current => withNotice(current, { text: changed ? `Cleared ${plural(changed, "peer selection")}.` : "No peer selections to clear.", variant: changed ? "warning" : "info" }))
1388
+ for (const peer of peers) if (state.session.setPeerSelected(peer.id, false)) changed += 1
1389
+ commit(current => withNotice(current, { text: changed ? `Cleared ${plural(changed, "matching peer selection")}.` : "No matching peer selections to clear.", variant: changed ? "warning" : "info" }))
1363
1390
  },
1364
1391
  toggleHideTerminalPeers: () => commit(current => withNotice({ ...current, hideTerminalPeers: !current.hideTerminalPeers }, { text: current.hideTerminalPeers ? "Terminal peers shown." : "Terminal peers hidden.", variant: "info" })),
1365
1392
  togglePeer: peerId => {
@@ -1379,13 +1406,16 @@ export const startTui = async (initialConfig: SessionConfig, showEvents = false)
1379
1406
  }))
1380
1407
  },
1381
1408
  shareTurnWithAllPeers: () => {
1382
- const shared = state.session.shareTurnWithAllPeers()
1409
+ const targetPeerIds = renderedPeers(state.snapshot.peers, state.hideTerminalPeers, state.peerSearch)
1410
+ .filter(peer => peer.presence === "active")
1411
+ .map(peer => peer.id)
1412
+ const shared = state.session.shareTurnWithPeers(targetPeerIds)
1383
1413
  commit(current => withNotice(current, {
1384
1414
  text: !state.session.canShareTurn()
1385
1415
  ? "TURN is not configured."
1386
1416
  : shared
1387
- ? `Shared TURN with ${plural(shared, "active peer")}.`
1388
- : "No active peers to share TURN with.",
1417
+ ? `Shared TURN with ${plural(shared, "matching peer")}.`
1418
+ : "No matching active peers to share TURN with.",
1389
1419
  variant: !state.session.canShareTurn() ? "info" : shared ? "success" : "info",
1390
1420
  }))
1391
1421
  },
@@ -3,6 +3,7 @@ export interface FileSearchMatch {
3
3
  absolutePath: string
4
4
  fileName: string
5
5
  kind: "file" | "directory"
6
+ size?: number
6
7
  score: number
7
8
  indices: number[]
8
9
  }
@@ -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 {