diff --git a/.eslintrc.yml b/.eslintrc.yml index a32a28f6..adb170d0 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -65,9 +65,6 @@ rules: eqeqeq: - error - smart - linebreak-style: - - error - - unix max-depth: - 1 - 5 diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index e2dbad90..6099d20f 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -10,7 +10,7 @@ export { Theme } from './theme' export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { SelectorOption } from './selector' export { CLIHandler, CLIEvent } from './cli' -export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions } from './platform' +export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, FileUploadOptions } from './platform' export { MenuItemOptions } from './menu' export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess' export { HostWindowService } from './hostWindow' diff --git a/terminus-core/src/api/platform.ts b/terminus-core/src/api/platform.ts index 22673c96..559f33b7 100644 --- a/terminus-core/src/api/platform.ts +++ b/terminus-core/src/api/platform.ts @@ -1,4 +1,5 @@ import { MenuItemOptions } from './menu' +import { Subject, Observable } from 'rxjs' /* eslint-disable @typescript-eslint/no-unused-vars */ export interface ClipboardContent { @@ -18,14 +19,75 @@ export interface MessageBoxResult { response: number } +export abstract class FileTransfer { + abstract getName (): string + abstract getSize (): number + abstract close (): void + + getCompletedBytes (): number { + return this.completedBytes + } + + isComplete (): boolean { + return this.completedBytes >= this.getSize() + } + + isCancelled (): boolean { + return this.cancelled + } + + cancel (): void { + this.cancelled = true + this.close() + } + + protected increaseProgress (bytes: number): void { + this.completedBytes += bytes + } + + private completedBytes = 0 + private cancelled = false +} + +export abstract class FileDownload extends FileTransfer { + abstract write (buffer: Buffer): Promise +} + +export abstract class FileUpload extends FileTransfer { + abstract read (): Promise + + async readAll (): Promise { + const buffers: Buffer[] = [] + while (true) { + const buf = await this.read() + if (!buf.length) { + break + } + buffers.push(Buffer.from(buf)) + } + return Buffer.concat(buffers) + } +} + +export interface FileUploadOptions { + multiple: boolean +} + export abstract class PlatformService { supportsWindowControls = false + get fileTransferStarted$ (): Observable { return this.fileTransferStarted } + + protected fileTransferStarted = new Subject() + abstract readClipboard (): string abstract setClipboard (content: ClipboardContent): void abstract loadConfig (): Promise abstract saveConfig (content: string): Promise + abstract startDownload (name: string, size: number): Promise + abstract startUpload (options?: FileUploadOptions): Promise + getConfigPath (): string|null { return null } diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug index 11bdf211..c4fd36d3 100644 --- a/terminus-core/src/components/appRoot.component.pug +++ b/terminus-core/src/components/appRoot.component.pug @@ -57,6 +57,17 @@ title-bar( ) div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}} + .d-flex( + *ngIf='activeTransfers.length > 0', + ngbDropdown, + [(open)]='activeTransfersDropdownOpen' + ) + button.btn.btn-secondary.btn-tab-bar( + title='File transfers', + ngbDropdownToggle + ) !{require('../icons/download-solid.svg')} + transfers-menu(ngbDropdownMenu, [(transfers)]='activeTransfers') + .drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS') .btn-group.background @@ -86,9 +97,8 @@ title-bar( button.btn.btn-secondary.btn-tab-bar.btn-update( *ngIf='updatesAvailable', title='Update available - Click to install', - (click)='updater.update()', - [fastHtmlBind]='updateIcon' - ) + (click)='updater.update()' + ) !{require('../icons/gift.svg')} window-controls.background( *ngIf='config.store.appearance.frame == "thin" \ diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index 16bde733..bdc2a7fd 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -12,7 +12,7 @@ import { UpdaterService } from '../services/updater.service' import { BaseTabComponent } from './baseTab.component' import { SafeModeModalComponent } from './safeModeModal.component' -import { AppService, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' +import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' /** @hidden */ @Component({ @@ -59,8 +59,9 @@ export class AppRootComponent { @HostBinding('class.no-tabs') noTabs = true tabsDragging = false unsortedTabs: BaseTabComponent[] = [] - updateIcon: string updatesAvailable = false + activeTransfers: FileTransfer[] = [] + activeTransfersDropdownOpen = false private logger: Logger private constructor ( @@ -79,8 +80,6 @@ export class AppRootComponent { this.logger = log.create('main') this.logger.info('v', platform.getAppVersion()) - this.updateIcon = require('../icons/gift.svg') - this.hotkeys.matchedHotkey.subscribe((hotkey: string) => { if (hotkey.startsWith('tab-')) { const index = parseInt(hotkey.split('-')[1]) @@ -134,6 +133,11 @@ export class AppRootComponent { this.noTabs = app.tabs.length === 0 }) + platform.fileTransferStarted$.subscribe(transfer => { + this.activeTransfers.push(transfer) + this.activeTransfersDropdownOpen = true + }) + config.ready$.toPromise().then(() => { this.leftToolbarButtons = this.getToolbarButtons(false) this.rightToolbarButtons = this.getToolbarButtons(true) diff --git a/terminus-core/src/components/transfersMenu.component.pug b/terminus-core/src/components/transfersMenu.component.pug new file mode 100644 index 00000000..e460eccb --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.pug @@ -0,0 +1,13 @@ +.dropdown-header File transfers +.dropdown-item.transfer(*ngFor='let transfer of transfers', (click)='showTransfer(transfer)') + .icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')} + .icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')} + .main + label {{transfer.getName()}} + .status(*ngIf='transfer.isComplete()') + ngb-progressbar(type='success', [value]='100') + .status(*ngIf='transfer.isCancelled()') + ngb-progressbar(type='danger', [value]='100') + .status(*ngIf='!transfer.isComplete() && !transfer.isCancelled()') + ngb-progressbar(type='info', [value]='getProgress(transfer)') + button.btn.btn-link((click)='removeTransfer(transfer); $event.stopPropagation()') !{require('../icons/times.svg')} diff --git a/terminus-core/src/components/transfersMenu.component.scss b/terminus-core/src/components/transfersMenu.component.scss new file mode 100644 index 00000000..947a0d06 --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.scss @@ -0,0 +1,31 @@ +:host { + min-width: 300px; +} + +.transfer { + display: flex; + align-items: center; + padding: 5px 0 5px 25px; + + .icon { + padding: 4px 10px; + width: 36px; + height: 32px; + background: rgba(0,0,0,.25); + margin-right: 12px; + } + + .main { + width: 100%; + margin-right: auto; + margin-bottom: 7px; + + label { + margin: 0; + } + } + + > i { + margin-right: 10px; + } +} diff --git a/terminus-core/src/components/transfersMenu.component.ts b/terminus-core/src/components/transfersMenu.component.ts new file mode 100644 index 00000000..bb0426a6 --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core' +import { FileDownload, FileTransfer, PlatformService } from '../api/platform' + +/** @hidden */ +@Component({ + selector: 'transfers-menu', + template: require('./transfersMenu.component.pug'), + styles: [require('./transfersMenu.component.scss')], +}) +export class TransfersMenuComponent { + @Input() transfers: FileTransfer[] + @Output() transfersChange = new EventEmitter() + + constructor (private platform: PlatformService) { } + + isDownload (transfer: FileTransfer): boolean { + return transfer instanceof FileDownload + } + + getProgress (transfer: FileTransfer): number { + return Math.round(100 * transfer.getCompletedBytes() / transfer.getSize()) + } + + showTransfer (transfer: FileTransfer): void { + const fp = transfer['filePath'] + if (fp) { + this.platform.showItemInFolder(fp) + } + } + + removeTransfer (transfer: FileTransfer): void { + if (!transfer.isComplete()) { + transfer.cancel() + } + this.transfers = this.transfers.filter(x => x !== transfer) + this.transfersChange.emit(this.transfers) + } +} diff --git a/terminus-core/src/icons/download-solid.svg b/terminus-core/src/icons/download-solid.svg new file mode 100644 index 00000000..5170a03f --- /dev/null +++ b/terminus-core/src/icons/download-solid.svg @@ -0,0 +1 @@ + diff --git a/terminus-core/src/icons/download.svg b/terminus-core/src/icons/download.svg new file mode 100644 index 00000000..fec6e2a9 --- /dev/null +++ b/terminus-core/src/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/terminus-core/src/icons/times.svg b/terminus-core/src/icons/times.svg new file mode 100644 index 00000000..a984649c --- /dev/null +++ b/terminus-core/src/icons/times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/terminus-core/src/icons/upload.svg b/terminus-core/src/icons/upload.svg new file mode 100644 index 00000000..6b12ff4a --- /dev/null +++ b/terminus-core/src/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 99fbe30d..3aadcd58 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -21,6 +21,7 @@ import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitT import { SplitTabSpannerComponent } from './components/splitTabSpanner.component' import { UnlockVaultModalComponent } from './components/unlockVaultModal.component' import { WelcomeTabComponent } from './components/welcomeTab.component' +import { TransfersMenuComponent } from './components/transfersMenu.component' import { AutofocusDirective } from './directives/autofocus.directive' import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' @@ -81,6 +82,7 @@ const PROVIDERS = [ SplitTabSpannerComponent, UnlockVaultModalComponent, WelcomeTabComponent, + TransfersMenuComponent, ], entryComponents: [ RenameTabModalComponent, diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index 4c4bc4c6..c9135687 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -242,7 +242,7 @@ export class ConfigService { private migrate (config) { config.version ??= 0 if (config.version < 1) { - for (const connection of config.ssh?.connections) { + for (const connection of config.ssh?.connections ?? []) { if (connection.privateKey) { connection.privateKeys = [connection.privateKey] delete connection.privateKey diff --git a/terminus-core/src/theme.scss b/terminus-core/src/theme.scss index f958ad3a..4f9c025b 100644 --- a/terminus-core/src/theme.scss +++ b/terminus-core/src/theme.scss @@ -43,7 +43,10 @@ app-root { .btn-tab-bar { background: transparent; &:hover { background: rgba(0, 0, 0, .25) !important; } - &:active { background: rgba(0, 0, 0, .5) !important; } + &:active, &[aria-expanded-true] { background: rgba(0, 0, 0, .5) !important; } + &:focus { + box-shadow: none; + } &::after { display: none; @@ -386,3 +389,7 @@ search-panel { hr { border-color: $list-group-border-color; } + +.dropdown-menu { + box-shadow: $dropdown-box-shadow; +} diff --git a/terminus-core/src/theme.vars.scss b/terminus-core/src/theme.vars.scss index e24c27ea..a7eb6177 100644 --- a/terminus-core/src/theme.vars.scss +++ b/terminus-core/src/theme.vars.scss @@ -190,3 +190,6 @@ $modal-header-border-width: 0; $modal-footer-border-color: #222; $modal-footer-border-width: 1px; $modal-content-border-width: 0; + +$progress-bg: $table-bg; +$progress-height: 3px; diff --git a/terminus-electron/src/services/platform.service.ts b/terminus-electron/src/services/platform.service.ts index 631bb87d..486621d4 100644 --- a/terminus-electron/src/services/platform.service.ts +++ b/terminus-electron/src/services/platform.service.ts @@ -1,10 +1,11 @@ import * as path from 'path' -import * as fs from 'mz/fs' +import * as fs from 'fs/promises' +import * as fsSync from 'fs' import * as os from 'os' import promiseIpc from 'electron-promise-ipc' import { execFile } from 'mz/child_process' -import { Injectable } from '@angular/core' -import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult } from 'terminus-core' +import { Injectable, NgZone } from '@angular/core' +import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions } from 'terminus-core' const fontManager = require('fontmanager-redux') // eslint-disable-line /* eslint-disable block-scoped-var */ @@ -25,6 +26,7 @@ export class ElectronPlatformService extends PlatformService { constructor ( private hostApp: HostAppService, private electron: ElectronService, + private zone: NgZone, ) { super() this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml') @@ -89,7 +91,7 @@ export class ElectronPlatformService extends PlatformService { } async loadConfig (): Promise { - if (await fs.exists(this.configPath)) { + if (fsSync.existsSync(this.configPath)) { return fs.readFile(this.configPath, 'utf8') } else { return '' @@ -157,4 +159,127 @@ export class ElectronPlatformService extends PlatformService { quit (): void { this.electron.app.exit(0) } + + async startUpload (options?: FileUploadOptions): Promise { + options ??= { multiple: false } + + const properties: any[] = ['openFile', 'treatPackageAsDirectory'] + if (options.multiple) { + properties.push('multiSelections') + } + + const result = await this.electron.dialog.showOpenDialog( + this.hostApp.getWindow(), + { + buttonLabel: 'Select', + properties, + }, + ) + if (result.canceled) { + return [] + } + + return Promise.all(result.filePaths.map(async p => { + const transfer = new ElectronFileUpload(p) + await this.wrapPromise(transfer.open()) + this.fileTransferStarted.next(transfer) + return transfer + })) + } + + async startDownload (name: string, size: number): Promise { + const result = await this.electron.dialog.showSaveDialog( + this.hostApp.getWindow(), + { + defaultPath: name, + }, + ) + if (!result.filePath) { + return null + } + const transfer = new ElectronFileDownload(result.filePath, size) + await this.wrapPromise(transfer.open()) + this.fileTransferStarted.next(transfer) + return transfer + } + + private wrapPromise (promise: Promise): Promise { + return new Promise((resolve, reject) => { + promise.then(result => { + this.zone.run(() => resolve(result)) + }).catch(error => { + this.zone.run(() => reject(error)) + }) + }) + } +} + +class ElectronFileUpload extends FileUpload { + private size: number + private file: fs.FileHandle + private buffer: Buffer + + constructor (private filePath: string) { + super() + this.buffer = Buffer.alloc(256 * 1024) + } + + async open (): Promise { + this.size = (await fs.stat(this.filePath)).size + this.file = await fs.open(this.filePath, 'r') + } + + getName (): string { + return path.basename(this.filePath) + } + + getSize (): number { + return this.size + } + + async read (): Promise { + const result = await this.file.read(this.buffer, 0, this.buffer.length, null) + this.increaseProgress(result.bytesRead) + return this.buffer.slice(0, result.bytesRead) + } + + close (): void { + this.file.close() + } +} + +class ElectronFileDownload extends FileDownload { + private file: fs.FileHandle + + constructor ( + private filePath: string, + private size: number, + ) { + super() + } + + async open (): Promise { + this.file = await fs.open(this.filePath, 'w') + } + + getName (): string { + return path.basename(this.filePath) + } + + getSize (): number { + return this.size + } + + async write (buffer: Buffer): Promise { + let pos = 0 + while (pos < buffer.length) { + const result = await this.file.write(buffer, pos, buffer.length - pos, null) + this.increaseProgress(result.bytesWritten) + pos += result.bytesWritten + } + } + + close (): void { + this.file.close() + } } diff --git a/terminus-terminal/src/features/debug.ts b/terminus-terminal/src/features/debug.ts index e792d503..064d1d09 100644 --- a/terminus-terminal/src/features/debug.ts +++ b/terminus-terminal/src/features/debug.ts @@ -1,16 +1,13 @@ -import * as fs from 'fs' import { Injectable } from '@angular/core' import { TerminalDecorator } from '../api/decorator' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' -import { ElectronService, HostAppService, PlatformService } from 'terminus-core' +import { PlatformService } from 'terminus-core' /** @hidden */ @Injectable() export class DebugDecorator extends TerminalDecorator { constructor ( - private electron: ElectronService, private platform: PlatformService, - private hostApp: HostAppService, ) { super() } @@ -63,28 +60,21 @@ export class DebugDecorator extends TerminalDecorator { } private async loadFile (): Promise { - const result = await this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), - { - buttonLabel: 'Load', - properties: ['openFile', 'treatPackageAsDirectory'], - }, - ) - if (result.filePaths.length) { - return fs.readFileSync(result.filePaths[0], { encoding: 'utf-8' }) + const transfer = await this.platform.startUpload() + if (!transfer.length) { + return null } - return null + const data = await transfer[0].readAll() + transfer[0].close() + return data.toString() } private async saveFile (content: string, name: string) { - const result = await this.electron.dialog.showSaveDialog( - this.hostApp.getWindow(), - { - defaultPath: name, - }, - ) - if (result.filePath) { - fs.writeFileSync(result.filePath, content) + const data = Buffer.from(content) + const transfer = await this.platform.startDownload(name, data.length) + if (transfer) { + transfer.write(data) + transfer.close() } } diff --git a/terminus-terminal/src/features/zmodem.ts b/terminus-terminal/src/features/zmodem.ts index c855fa64..8df0c33e 100644 --- a/terminus-terminal/src/features/zmodem.ts +++ b/terminus-terminal/src/features/zmodem.ts @@ -1,13 +1,11 @@ import colors from 'ansi-colors' import * as ZModem from 'zmodem.js' -import * as fs from 'fs' -import * as path from 'path' import { Observable } from 'rxjs' -import { filter } from 'rxjs/operators' +import { filter, first } from 'rxjs/operators' import { Injectable } from '@angular/core' import { TerminalDecorator } from '../api/decorator' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' -import { LogService, Logger, ElectronService, HostAppService, HotkeysService } from 'terminus-core' +import { LogService, Logger, HotkeysService, PlatformService, FileUpload } from 'terminus-core' const SPACER = ' ' @@ -21,8 +19,7 @@ export class ZModemDecorator extends TerminalDecorator { constructor ( log: LogService, hotkeys: HotkeysService, - private electron: ElectronService, - private hostApp: HostAppService, + private platform: PlatformService, ) { super() this.logger = log.create('zmodem') @@ -87,22 +84,13 @@ export class ZModemDecorator extends TerminalDecorator { this.logger.info('new session', zsession) if (zsession.type === 'send') { - const result = await this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), - { - buttonLabel: 'Send', - properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'], - }, - ) - if (result.canceled) { - zsession.close() - return - } - - let filesRemaining = result.filePaths.length - for (const filePath of result.filePaths) { - await this.sendFile(terminal, zsession, filePath, filesRemaining) + const transfers = await this.platform.startUpload({ multiple: true }) + let filesRemaining = transfers.length + let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0) + for (const transfer of transfers) { + await this.sendFile(terminal, zsession, transfer, filesRemaining, sizeRemaining) filesRemaining-- + sizeRemaining -= transfer.getSize() } this.activeSession = null await zsession.close() @@ -125,20 +113,14 @@ export class ZModemDecorator extends TerminalDecorator { } = xfer.get_details() this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + details.name, true) this.logger.info('offered', xfer) - const result = await this.electron.dialog.showSaveDialog( - this.hostApp.getWindow(), - { - defaultPath: details.name, - }, - ) - if (!result.filePath) { + + const transfer = await this.platform.startDownload(details.name, details.size) + if (!transfer) { this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + details.name) xfer.skip() return } - const stream = fs.createWriteStream(result.filePath) - let bytesSent = 0 let canceled = false const cancelSubscription = this.cancelEvent.subscribe(() => { if (terminal.hasFocus) { @@ -156,18 +138,19 @@ export class ZModemDecorator extends TerminalDecorator { if (canceled) { return } - stream.write(Buffer.from(chunk)) - bytesSent += chunk.length - this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true) + transfer.write(Buffer.from(chunk)) + this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true) }, }), - this.cancelEvent.toPromise(), + this.cancelEvent.pipe(first()).toPromise(), ]) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (canceled) { + transfer.cancel() this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name) } else { + transfer.close() this.showMessage(terminal, colors.bgGreen.black(' Received ') + ' ' + details.name) } } catch { @@ -175,47 +158,45 @@ export class ZModemDecorator extends TerminalDecorator { } cancelSubscription.unsubscribe() - stream.end() } - private async sendFile (terminal, zsession, filePath, filesRemaining) { - const stat = fs.statSync(filePath) + private async sendFile (terminal, zsession, transfer: FileUpload, filesRemaining, sizeRemaining) { const offer = { - name: path.basename(filePath), - size: stat.size, - mode: stat.mode, - mtime: Math.floor(stat.mtimeMs / 1000), + name: transfer.getName(), + size: transfer.getSize(), + mode: 0o755, files_remaining: filesRemaining, - bytes_remaining: stat.size, + bytes_remaining: sizeRemaining, } this.logger.info('offering', offer) this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true) const xfer = await zsession.send_offer(offer) if (xfer) { - let bytesSent = 0 let canceled = false - const stream = fs.createReadStream(filePath) const cancelSubscription = this.cancelEvent.subscribe(() => { if (terminal.hasFocus) { canceled = true } }) - stream.on('data', chunk => { - if (canceled) { - stream.close() - return + while (true) { + const chunk = await transfer.read() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (canceled || !chunk.length) { + break } - xfer.send(chunk) - bytesSent += chunk.length - this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true) - }) - await Promise.race([ - new Promise(resolve => stream.on('end', resolve)), - this.cancelEvent.toPromise(), - ]) + await xfer.send(chunk) + this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true) + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (canceled) { + transfer.cancel() + } else { + transfer.close() + } await xfer.end() @@ -226,9 +207,9 @@ export class ZModemDecorator extends TerminalDecorator { this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name) } - stream.close() cancelSubscription.unsubscribe() } else { + transfer.cancel() this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name) this.logger.warn('rejected by the other side') }