import { EventEmitter, NgZone } from '@angular/core'; import { ConfigService, FileTransfer, PlatformService } from 'tabby-core'; import { SidebarPlusI18nService } from './i18n'; export type TransferDirection = 'up' | 'down'; /** * `handover` sits between the last byte we serve and the file actually being * where the user dropped it — see `HANDOVER_BYTES_PER_MS`. * * `unsound` is a transfer that *finished* but whose arrival check failed: every * byte we knew of was served, yet the file at destination is missing or has the * wrong size. Distinct from `failed` because "interrompu à N %" would be a lie — * nothing was interrupted, the copy just is not what it should be. */ export type TransferState = 'active' | 'handover' | 'done' | 'cancelled' | 'failed' | 'unsound'; /** * What a transfer is *about*, which the `FileTransfer` object cannot say: * `getName()` is all it exposes. Supplied by whoever starts the transfer — * the only party that knows — and shown on the line's tooltip. */ export interface TransferContext { remotePath?: string; /** The owning SSH tab's display name, same fallback rule as the sessions list. */ sessionLabel?: string; } /** * One line of the panel. * * Every displayed value is a plain field, refreshed on a tick — never a call * into the transfer from the template. A getter there would be re-evaluated on * every change detection pass, and Tabby runs a great many of them (piège #54). */ export interface TransferEntry { id: number; transfer: FileTransfer; direction: TransferDirection; name: string; size: number; percent: number; /** Bytes per second, smoothed here rather than read from the transfer — see `tick()`. */ speed: number; state: TransferState; /** Formatted once: the size never changes, and formatting it per pass would be the very mistake above. */ sizeLabel: string; speedLabel: string; /** Time since the drop, counting up. Frozen at the final duration once the transfer ends. */ elapsedLabel: string; /** Empty whenever it cannot be told honestly: unknown size, stalled, or already over. */ etaLabel: string; startedAtMs: number; lastBytes: number; lastTickAt: number; /** What killed the transfer, shown on hover. Empty unless the state is `failed`. */ failureReason: string; /** * True when the last byte served is not the end of the story: the shell * still has to write the file where it was dropped. */ handsOver: boolean; /** When the handover is estimated to be over. Only meaningful while the state is `handover`. */ handoverUntil: number; /** Where the entry lives on the server. Empty when the starter never said. */ remotePath: string; /** Which session carries it. Empty when the starter never said. */ sessionLabel: string; /** The row's whole tooltip, composed once — name, remote path and session on their own lines. */ tooltip: string; } /** * The one formatting of a transfer's speed — a plain `bytes/s` reading, blank * while nothing has moved yet. Exported rather than kept private: the active * sessions list's compact aggregate (sidebarTree.component.ts) reuses it for * its own combined-speed segment instead of duplicating the `filesize()` call * with its own rounding. */ export declare function formatSpeed(bytesPerSecond: number): string; /** * Every file transfer of the application, as the sidebar shows them. * * Fed by `fileTransferStarted$` — the same stream Tabby's own tab bar * subscribes to — rather than by a registry of our own. That is what the * roadmap's option (c) proposed, and it means this panel reflects *all* * transfers, including those started by the native SFTP panel or another * plugin, not just the ones this plugin initiates. * * Completed entries are kept as history until dismissed, in memory only: * `config.yaml` has no business growing a transfer log, and neither has * localStorage. */ export declare class SidebarPlusTransfersService { private config; private zone; private i18n; entries: TransferEntry[]; /** What the header badge shows — a field, not a getter, for the reason given above. */ summary: string; /** The full breakdown, on hover: the badge itself only has room for what matters now. */ summaryTitle: string; private counter; private timer; /** * Fires whenever an entry is added or leaves the `active` state, and when * one is dropped or the list is cleared — never on the tick's per-entry * percent/speed refresh, which nothing outside this service needs to react * to. Consumed by the sidebar tree's per-profile activity arrows, so they * can recompute their own precomputed map without polling the registry * from a template getter (piège #54). * * Emitted from inside code that already runs in the Angular zone — `track()` * wraps its body in `zone.run()`, `finish()` only ever runs from `tick()` * (itself scheduled from inside that same `zone.run()`) or from * `markFailed()`/`markUnsound()` (their own `zone.run()`), and `remove()`/ * `clear()` are only ever called from a template click binding. A subscriber * outside this service should still not assume that and re-enter the zone * itself if it mutates state a template reads (piège #41). */ readonly changed: EventEmitter; constructor(platform: PlatformService, config: ConfigService, zone: NgZone, i18n: SidebarPlusI18nService); /** * Whether the transfers panel is on. Off means this service does nothing at * all — no history, no tick. * * Conditioned on the SFTP view as well as on its own switch: the panel is * an option *of* that view, so it goes with it. Read from the config rather * than from the tree component, which is not always mounted. */ get enabled(): boolean; get activeCount(): number; /** * What still needs the tick to run — which is not the same as what can still * be cancelled. * * A line in `handover` has nothing left to interrupt on our side, so it does * not belong in `activeCount` (which drives "clearing will cancel them"). * It does need the tick, though: its own end is a deadline nothing else * watches, and stopping the timer on the last *active* transfer would leave * it saying "remise au système" for the rest of the session. */ private get runningCount(); /** * Shows a transfer this plugin started itself. * * `fileTransferStarted$` above covers everything that goes through * `PlatformService`, which is most things — but not a transfer we construct * ourselves, like the one serving a drag-and-drop out of the panel. Feeding * that one to the native stream instead would have two unwanted effects: * Tabby's own transfers dropdown *opens itself* on every emission, and its * list is only ever emptied by clicking each line, so a hidden menu would * accumulate entries nobody can reach. */ track(transfer: FileTransfer, handsOver?: boolean, context?: TransferContext): void; /** * Completes an entry's context after the fact. * * Needed by the one feed that cannot pass it at `track()` time: a transfer * started through `platform.startDownload()`/`startUpload()` is registered * by the `fileTransferStarted$` subscription, synchronously, *inside* that * call — the caller only holds the transfer once it is already tracked. * Attaching afterwards closes the gap without a second bookkeeping path. */ attachContext(transfer: FileTransfer, context: TransferContext): void; /** One line each, present parts only: the tooltip is read on hover, not parsed. */ private composeTooltip; /** * Marks a transfer that died instead of finishing. * * Nothing else can. The tick only ever reads `isCancelled()` and * `isComplete()`, and a transfer whose SSH transport disappeared answers * false to both: its line stayed `active` for good, frozen at whatever * percentage it had reached, speed decaying to zero — which reads as "still * going" for something that is over. Only the caller awaiting the transfer * sees the rejection, so the caller is who reports it here. */ markFailed(transfer: FileTransfer, reason?: string): void; /** * Marks a transfer that finished but did not arrive sound. * * Reported by the arrival check that runs after a download to an imposed * path — the only route where a destination exists to inspect. By the time * the check runs, the tick may already have flipped the line to `done`, * which is why `done` is accepted here where `markFailed()` refuses it: * this is precisely a verdict *on* a finished transfer. */ markUnsound(transfer: FileTransfer, reason?: string): void; /** * Says what is happening rather than just how many lines there are. * * A bare count next to the title read as "Transferts2" and told nothing: * two running? two finished? The badge now spells out the state that is * worth acting on — something still in flight — and falls back to the plain * total when everything is over, the breakdown moving to the tooltip. */ private refreshSummary; /** * Runs only while something is in flight. * * A permanent interval would trigger a change detection pass twice a second * for a panel that is idle almost always — the exact cost this panel was * asked to avoid paying elsewhere. */ private startTicking; private tick; /** * Recomputes the smoothed speed, and from it the ETA. * * The transfer's own `getSpeed()` is not used: it reports the last chunk's * rate, measured between two `increaseProgress()` calls, so it swings by an * order of magnitude from one chunk to the next. Averaging over the bytes * that actually moved between two ticks is both steadier and independent of * how the transport happens to chop the stream up. */ private updateSpeed; /** How long the shell is assumed to need to put a file of this size in place. */ private handoverDuration; /** Freezes a finished line: the duration it took, and no figure that keeps moving. */ private finish; private stopTicking; /** * Drops one line — cancelling it first when it is still running. * * Same semantics as Tabby's own transfers menu: removing a live transfer * *stops* it rather than just hiding it, which is the only reading that * does not leave an invisible transfer writing to a server. */ remove(entry: TransferEntry): void; /** Clears the list. Live transfers are cancelled — the caller asks first. */ clear(): void; }