mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-28 19:16:02 +00:00
Compare commits
16 Commits
bump-elect
...
b31c2a5c11
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b31c2a5c11 | ||
![]() |
3c17654180 | ||
![]() |
7e1905c32c | ||
![]() |
bbf3b785fc | ||
![]() |
0cf9886270 | ||
![]() |
93e43067de | ||
![]() |
d1293c6a89 | ||
![]() |
5a7a06e529 | ||
![]() |
c8aea9d8e0 | ||
![]() |
57001d4dde | ||
![]() |
ef59394b79 | ||
![]() |
020372c902 | ||
![]() |
912e0aa426 | ||
![]() |
4a5087afc1 | ||
![]() |
e0c34ef7bc | ||
![]() |
406e9e1c42 |
@@ -60,7 +60,7 @@ tabby
|
|||||||
| ├─ src # Electron renderer code
|
| ├─ src # Electron renderer code
|
||||||
| └─ main.js # Electron main entry point
|
| └─ main.js # Electron main entry point
|
||||||
├─ build
|
├─ build
|
||||||
├─ clink # Clink distributive, for Windows
|
├─ clink # Clink distribution, for Windows
|
||||||
├─ scripts # Maintenance scripts
|
├─ scripts # Maintenance scripts
|
||||||
├─ tabby-community-color-schemes # Plugin that provides color schemes
|
├─ tabby-community-color-schemes # Plugin that provides color schemes
|
||||||
├─ tabby-core # Plugin that provides base UI and tab management
|
├─ tabby-core # Plugin that provides base UI and tab management
|
||||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017 Eugene Pankov
|
Copyright (c) 2017 Tabby Developers
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@@ -107,7 +107,7 @@ This README is also available in: <a href="./README.md">:gb: English</a> · <a
|
|||||||
|
|
||||||
플러그인과 테마는 Tabby 내부의 설정에서 직접 설치할 수 있습니다.
|
플러그인과 테마는 Tabby 내부의 설정에서 직접 설치할 수 있습니다.
|
||||||
|
|
||||||
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - m터미널의 경로 및 URL을 클릭 가능하게
|
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - 터미널의 경로 및 URL을 클릭 가능하게
|
||||||
* [docker](https://github.com/Eugeny/tabby-docker) - Docker 컨테이너에 연결
|
* [docker](https://github.com/Eugeny/tabby-docker) - Docker 컨테이너에 연결
|
||||||
* [title-control](https://github.com/kbjr/terminus-title-control) - 접두사, 접미사 및/또는 문자열 제거를 제공하여 터미널 탭의 제목을 수정
|
* [title-control](https://github.com/kbjr/terminus-title-control) - 접두사, 접미사 및/또는 문자열 제거를 제공하여 터미널 탭의 제목을 수정
|
||||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 하나 또는 모든 터미널 탭에 신속한 명령 전송
|
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 하나 또는 모든 터미널 탭에 신속한 명령 전송
|
||||||
@@ -144,7 +144,7 @@ Pull requests and plugins are welcome!
|
|||||||
---
|
---
|
||||||
<a name="contributors"></a>
|
<a name="contributors"></a>
|
||||||
|
|
||||||
여기있는 멋진 사람들에게 진심으로 감사합니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
여기 있는 멋진 사람들에게 진심으로 감사합니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"repository": "https://github.com/Eugeny/tabby",
|
"repository": "https://github.com/Eugeny/tabby",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Eugene Pankov",
|
"name": "Tabby Developers",
|
||||||
"email": "e@ajenti.org"
|
"email": "e@ajenti.org"
|
||||||
},
|
},
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
|
@@ -6,4 +6,6 @@ export const PLUGIN_BLACKLIST = [
|
|||||||
'terminus-clickable-ips', // broken, functionality now bundled with Tabby
|
'terminus-clickable-ips', // broken, functionality now bundled with Tabby
|
||||||
'terminus-elastic-quick-commands', // broken and abandoned, fork of quick-commands
|
'terminus-elastic-quick-commands', // broken and abandoned, fork of quick-commands
|
||||||
'terminus-elastic-quick-cmds', // broken and abandoned, fork of quick-commands
|
'terminus-elastic-quick-cmds', // broken and abandoned, fork of quick-commands
|
||||||
|
'tabby-fig', // abandoned,
|
||||||
|
'tabby-plugin-fig-integration', // abandoned,
|
||||||
]
|
]
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.microphone</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
@@ -40,7 +40,7 @@
|
|||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"deep-equal": "2.0.5",
|
"deep-equal": "2.0.5",
|
||||||
"electron": "^36.3",
|
"electron": "^36.4",
|
||||||
"electron-builder": "^26.0",
|
"electron-builder": "^26.0",
|
||||||
"electron-download": "^4.1.1",
|
"electron-download": "^4.1.1",
|
||||||
"electron-installer-snap": "^5.1.0",
|
"electron-installer-snap": "^5.1.0",
|
||||||
|
@@ -31,6 +31,7 @@ export const builtinPlugins = [
|
|||||||
'tabby-electron',
|
'tabby-electron',
|
||||||
'tabby-plugin-manager',
|
'tabby-plugin-manager',
|
||||||
'tabby-linkifier',
|
'tabby-linkifier',
|
||||||
|
'tabby-auto-sudo-password',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const packagesWithDocs = [
|
export const packagesWithDocs = [
|
||||||
|
23
tabby-auto-sudo-password/package.json
Normal file
23
tabby-auto-sudo-password/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "tabby-auto-sudo-password",
|
||||||
|
"version": "1.0.197-nightly.1",
|
||||||
|
"description": "Offers to automatically paste saved sudo password in SSH sessions",
|
||||||
|
"keywords": [
|
||||||
|
"tabby-builtin-plugin"
|
||||||
|
],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "typings/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --progress --color --display-modules",
|
||||||
|
"watch": "webpack --progress --color --watch"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"typings"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"ansi-colors": "^4.1.1"
|
||||||
|
},
|
||||||
|
"author": "Tabby Developers",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
89
tabby-auto-sudo-password/src/decorator.ts
Normal file
89
tabby-auto-sudo-password/src/decorator.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import colors from 'ansi-colors'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { TerminalDecorator, BaseTerminalTabComponent, XTermFrontend, SessionMiddleware } from 'tabby-terminal'
|
||||||
|
import { SSHProfile, SSHTabComponent, PasswordStorageService } from 'tabby-ssh'
|
||||||
|
|
||||||
|
const SUDO_PROMPT_REGEX = /^\[sudo\] password for ([^:]+):\s*$/im
|
||||||
|
|
||||||
|
export class AutoSudoPasswordMiddleware extends SessionMiddleware {
|
||||||
|
private pendingPasswordToPaste: string | null = null
|
||||||
|
private pasteHint = `${colors.black.bgBlackBright(' Tabby ')} ${colors.gray('Press Enter to paste saved password')}`
|
||||||
|
private pasteHintLength = colors.stripColor(this.pasteHint).length
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private profile: SSHProfile,
|
||||||
|
private ps: PasswordStorageService,
|
||||||
|
) { super() }
|
||||||
|
|
||||||
|
feedFromSession (data: Buffer): void {
|
||||||
|
const text = data.toString('utf-8')
|
||||||
|
const match = SUDO_PROMPT_REGEX.exec(text)
|
||||||
|
if (match) {
|
||||||
|
const username = match[1]
|
||||||
|
this.handlePrompt(username)
|
||||||
|
}
|
||||||
|
this.outputToTerminal.next(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
feedFromTerminal (data: Buffer): void {
|
||||||
|
if (this.pendingPasswordToPaste) {
|
||||||
|
const backspaces = Buffer.alloc(this.pasteHintLength, 8) // backspace
|
||||||
|
const spaces = Buffer.alloc(this.pasteHintLength, 32) // space
|
||||||
|
const clear = Buffer.concat([backspaces, spaces, backspaces])
|
||||||
|
this.outputToTerminal.next(clear)
|
||||||
|
if (data.length === 1 && data[0] === 13) { // Enter key
|
||||||
|
this.outputToSession.next(Buffer.from(this.pendingPasswordToPaste + '\n'))
|
||||||
|
this.pendingPasswordToPaste = null
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
this.pendingPasswordToPaste = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.outputToSession.next(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePrompt (username: string): Promise<void> {
|
||||||
|
console.log(`Detected sudo prompt for user: ${username}`)
|
||||||
|
const pw = await this.ps.loadPassword(this.profile)
|
||||||
|
if (pw) {
|
||||||
|
this.outputToTerminal.next(Buffer.from(this.pasteHint))
|
||||||
|
this.pendingPasswordToPaste = pw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPassword (username: string): Promise<string| null> {
|
||||||
|
if (this.profile.options.user !== username) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.ps.loadPassword(this.profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AutoSudoPasswordDecorator extends TerminalDecorator {
|
||||||
|
constructor (
|
||||||
|
private ps: PasswordStorageService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachToSession (tab: SSHTabComponent) {
|
||||||
|
if (!tab.session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tab.session.middleware.unshift(new AutoSudoPasswordMiddleware(tab.profile, this.ps))
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (tab: BaseTerminalTabComponent<any>): void {
|
||||||
|
if (!(tab.frontend instanceof XTermFrontend) || !(tab instanceof SSHTabComponent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.attachToSession(tab)
|
||||||
|
this.subscribeUntilDetached(tab, tab.sessionChanged$.subscribe(() => {
|
||||||
|
this.attachToSession(tab)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
tabby-auto-sudo-password/src/index.ts
Normal file
16
tabby-auto-sudo-password/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { ToastrModule } from 'ngx-toastr'
|
||||||
|
import { TerminalDecorator } from 'tabby-terminal'
|
||||||
|
|
||||||
|
import { AutoSudoPasswordDecorator } from './decorator'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
ToastrModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TerminalDecorator, useClass: AutoSudoPasswordDecorator, multi: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export default class AutoSudoPasswordModule { }
|
7
tabby-auto-sudo-password/tsconfig.json
Normal file
7
tabby-auto-sudo-password/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
}
|
||||||
|
}
|
14
tabby-auto-sudo-password/tsconfig.typings.json
Normal file
14
tabby-auto-sudo-password/tsconfig.typings.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "typings"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationDir": "./typings",
|
||||||
|
"paths": {
|
||||||
|
"tabby-*": ["../../tabby-*"],
|
||||||
|
"*": ["../../app/node_modules/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
tabby-auto-sudo-password/webpack.config.mjs
Normal file
10
tabby-auto-sudo-password/webpack.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as path from 'path'
|
||||||
|
import * as url from 'url'
|
||||||
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||||
|
|
||||||
|
import config from '../webpack.plugin.config.mjs'
|
||||||
|
|
||||||
|
export default () => config({
|
||||||
|
name: 'auto-sudo-password',
|
||||||
|
dirname: __dirname,
|
||||||
|
})
|
8
tabby-auto-sudo-password/yarn.lock
Normal file
8
tabby-auto-sudo-password/yarn.lock
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
ansi-colors@^4.1.1:
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
|
||||||
|
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
|
@@ -14,7 +14,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/core": "^15",
|
"@angular/core": "^15",
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bootstrap": "^5.3.0-alpha.1",
|
"bootstrap": "^5.3.0-alpha.1",
|
||||||
|
@@ -10,7 +10,7 @@ export { Theme } from './theme'
|
|||||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||||
export { SelectorOption } from './selector'
|
export { SelectorOption } from './selector'
|
||||||
export { CLIHandler, CLIEvent } from './cli'
|
export { CLIHandler, CLIEvent } from './cli'
|
||||||
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload } from './platform'
|
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload, DirectoryDownload, PlatformTheme } from './platform'
|
||||||
export { MenuItemOptions } from './menu'
|
export { MenuItemOptions } from './menu'
|
||||||
export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||||
export { HostWindowService } from './hostWindow'
|
export { HostWindowService } from './hostWindow'
|
||||||
|
@@ -22,7 +22,6 @@ export interface MessageBoxResult {
|
|||||||
|
|
||||||
export abstract class FileTransfer {
|
export abstract class FileTransfer {
|
||||||
abstract getName (): string
|
abstract getName (): string
|
||||||
abstract getMode (): number
|
|
||||||
abstract getSize (): number
|
abstract getSize (): number
|
||||||
abstract close (): void
|
abstract close (): void
|
||||||
|
|
||||||
@@ -34,8 +33,16 @@ export abstract class FileTransfer {
|
|||||||
return this.completedBytes
|
return this.completedBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStatus (): string {
|
||||||
|
return this.status
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalSize (): number {
|
||||||
|
return this.totalSize
|
||||||
|
}
|
||||||
|
|
||||||
isComplete (): boolean {
|
isComplete (): boolean {
|
||||||
return this.completedBytes >= this.getSize()
|
return this.completed
|
||||||
}
|
}
|
||||||
|
|
||||||
isCancelled (): boolean {
|
isCancelled (): boolean {
|
||||||
@@ -47,6 +54,18 @@ export abstract class FileTransfer {
|
|||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatus (status: string): void {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
setTotalSize (size: number): void {
|
||||||
|
this.totalSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
setCompleted (completed: boolean): void {
|
||||||
|
this.completed = completed
|
||||||
|
}
|
||||||
|
|
||||||
protected increaseProgress (bytes: number): void {
|
protected increaseProgress (bytes: number): void {
|
||||||
if (!bytes) {
|
if (!bytes) {
|
||||||
return
|
return
|
||||||
@@ -57,16 +76,26 @@ export abstract class FileTransfer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private completedBytes = 0
|
private completedBytes = 0
|
||||||
|
private totalSize = 0
|
||||||
private lastChunkStartTime = Date.now()
|
private lastChunkStartTime = Date.now()
|
||||||
private lastChunkSpeed = 0
|
private lastChunkSpeed = 0
|
||||||
private cancelled = false
|
private cancelled = false
|
||||||
|
private completed = false
|
||||||
|
private status = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class FileDownload extends FileTransfer {
|
export abstract class FileDownload extends FileTransfer {
|
||||||
abstract write (buffer: Uint8Array): Promise<void>
|
abstract write (buffer: Uint8Array): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class DirectoryDownload extends FileTransfer {
|
||||||
|
abstract createDirectory (relativePath: string): Promise<void>
|
||||||
|
abstract createFile (relativePath: string, mode: number, size: number): Promise<FileDownload>
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class FileUpload extends FileTransfer {
|
export abstract class FileUpload extends FileTransfer {
|
||||||
|
abstract getMode (): number
|
||||||
|
|
||||||
abstract read (): Promise<Uint8Array>
|
abstract read (): Promise<Uint8Array>
|
||||||
|
|
||||||
async readAll (): Promise<Uint8Array> {
|
async readAll (): Promise<Uint8Array> {
|
||||||
@@ -127,6 +156,7 @@ export abstract class PlatformService {
|
|||||||
abstract saveConfig (content: string): Promise<void>
|
abstract saveConfig (content: string): Promise<void>
|
||||||
|
|
||||||
abstract startDownload (name: string, mode: number, size: number): Promise<FileDownload|null>
|
abstract startDownload (name: string, mode: number, size: number): Promise<FileDownload|null>
|
||||||
|
abstract startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null>
|
||||||
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
|
abstract startUpload (options?: FileUploadOptions): Promise<FileUpload[]>
|
||||||
abstract startUploadDirectory (paths?: string[]): Promise<DirectoryUpload>
|
abstract startUploadDirectory (paths?: string[]): Promise<DirectoryUpload>
|
||||||
|
|
||||||
@@ -237,7 +267,7 @@ export abstract class PlatformService {
|
|||||||
abstract setErrorHandler (handler: (_: any) => void): void
|
abstract setErrorHandler (handler: (_: any) => void): void
|
||||||
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
|
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
|
||||||
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
|
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
|
||||||
abstract pickDirectory (): Promise<string>
|
abstract pickDirectory (): Promise<string | null>
|
||||||
abstract quit (): void
|
abstract quit (): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -35,7 +35,8 @@ title-bar(
|
|||||||
[@animateTab]='{value: "in", params: {size: targetTabSize}}',
|
[@animateTab]='{value: "in", params: {size: targetTabSize}}',
|
||||||
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
|
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
|
||||||
(click)='app.selectTab(tab)',
|
(click)='app.selectTab(tab)',
|
||||||
[class.fully-draggable]='hostApp.platform !== Platform.macOS'
|
[class.fully-draggable]='hostApp.platform !== Platform.macOS',
|
||||||
|
[ngbTooltip]='tab.customTitle || tab.title'
|
||||||
)
|
)
|
||||||
|
|
||||||
.btn-group.background
|
.btn-group.background
|
||||||
@@ -43,7 +44,7 @@ title-bar(
|
|||||||
*ngFor='let button of leftToolbarButtons'
|
*ngFor='let button of leftToolbarButtons'
|
||||||
)
|
)
|
||||||
button.btn.btn-secondary.btn-tab-bar(
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
[title]='button.label',
|
[ngbTooltip]='button.label',
|
||||||
(click)='button.run && button.run()',
|
(click)='button.run && button.run()',
|
||||||
[fastHtmlBind]='button.icon'
|
[fastHtmlBind]='button.icon'
|
||||||
)
|
)
|
||||||
@@ -55,7 +56,7 @@ title-bar(
|
|||||||
)
|
)
|
||||||
button.btn.btn-secondary.btn-tab-bar(
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
[hidden]='activeTransfers.length == 0',
|
[hidden]='activeTransfers.length == 0',
|
||||||
title='File transfers',
|
[ngbTooltip]='"File transfers"|translate',
|
||||||
ngbDropdownToggle
|
ngbDropdownToggle
|
||||||
) !{require('../icons/transfers.svg')}
|
) !{require('../icons/transfers.svg')}
|
||||||
transfers-menu(
|
transfers-menu(
|
||||||
@@ -75,14 +76,14 @@ title-bar(
|
|||||||
*ngFor='let button of rightToolbarButtons'
|
*ngFor='let button of rightToolbarButtons'
|
||||||
)
|
)
|
||||||
button.btn.btn-secondary.btn-tab-bar(
|
button.btn.btn-secondary.btn-tab-bar(
|
||||||
[title]='button.label',
|
[ngbTooltip]='button.label',
|
||||||
(click)='button.run && button.run()',
|
(click)='button.run && button.run()',
|
||||||
[fastHtmlBind]='button.icon'
|
[fastHtmlBind]='button.icon'
|
||||||
)
|
)
|
||||||
|
|
||||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||||
*ngIf='updatesAvailable',
|
*ngIf='updatesAvailable',
|
||||||
title='Update available - Click to install',
|
[ngbTooltip]='"Update available - Click to install"|translate',
|
||||||
(click)='updater.update()'
|
(click)='updater.update()'
|
||||||
) !{require('../icons/gift.svg')}
|
) !{require('../icons/gift.svg')}
|
||||||
|
|
||||||
|
@@ -13,7 +13,6 @@ profile-icon(
|
|||||||
)
|
)
|
||||||
|
|
||||||
.name(
|
.name(
|
||||||
[title]='tab.customTitle || tab.title',
|
|
||||||
[class.no-hover]='config.store.terminal.hideCloseButton && config.store.terminal.hideTabOptionsButton'
|
[class.no-hover]='config.store.terminal.hideCloseButton && config.store.terminal.hideTabOptionsButton'
|
||||||
cdkDrag,
|
cdkDrag,
|
||||||
cdkDragRootElement='tab-header',
|
cdkDragRootElement='tab-header',
|
||||||
|
@@ -5,7 +5,9 @@
|
|||||||
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
||||||
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
||||||
.main
|
.main
|
||||||
label.no-wrap([title]='transfer.getName()') {{transfer.getName()}}
|
label.no-wrap([ngbTooltip]='transfer.getName()')
|
||||||
|
| {{transfer.getName()}}
|
||||||
|
span.ms-2.text-muted(*ngIf='transfer.getStatus()') ({{transfer.getStatus()}})
|
||||||
ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)')
|
ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)')
|
||||||
.metadata
|
.metadata
|
||||||
.size {{transfer.getSize()|filesize}}
|
.size {{transfer.getSize()|filesize}}
|
||||||
|
@@ -2,7 +2,7 @@ import { NgModule, ModuleWithProviders, LOCALE_ID } from '@angular/core'
|
|||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { TranslateModule, TranslateCompiler, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'
|
import { TranslateModule, TranslateCompiler, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'
|
||||||
@@ -155,6 +155,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||||||
platform: PlatformService,
|
platform: PlatformService,
|
||||||
hotkeys: HotkeysService,
|
hotkeys: HotkeysService,
|
||||||
commands: CommandService,
|
commands: CommandService,
|
||||||
|
ngbTooltipConfig: NgbTooltipConfig,
|
||||||
public locale: LocaleService,
|
public locale: LocaleService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private profilesService: ProfilesService,
|
private profilesService: ProfilesService,
|
||||||
@@ -201,6 +202,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||||||
commands.run('core:profile-selector', {})
|
commands.run('core:profile-selector', {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ngbTooltipConfig.openDelay = 750
|
||||||
|
ngbTooltipConfig.placement = 'top bottom auto'
|
||||||
|
ngbTooltipConfig.container = 'body'
|
||||||
}
|
}
|
||||||
|
|
||||||
async showSelector (provider: ProfileProvider<Profile>): Promise<void> {
|
async showSelector (provider: ProfileProvider<Profile>): Promise<void> {
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/core": "^15",
|
"@angular/core": "^15",
|
||||||
|
@@ -5,12 +5,11 @@ import * as os from 'os'
|
|||||||
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
||||||
import { execFile } from 'mz/child_process'
|
import { execFile } from 'mz/child_process'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core'
|
import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, DirectoryDownload, FileUploadOptions, wrapPromise, TranslateService, FileTransfer, PlatformTheme } from 'tabby-core'
|
||||||
import { ElectronService } from '../services/electron.service'
|
import { ElectronService } from '../services/electron.service'
|
||||||
import { ElectronHostWindow } from './hostWindow.service'
|
import { ElectronHostWindow } from './hostWindow.service'
|
||||||
import { ShellIntegrationService } from './shellIntegration.service'
|
import { ShellIntegrationService } from './shellIntegration.service'
|
||||||
import { ElectronHostAppService } from './hostApp.service'
|
import { ElectronHostAppService } from './hostApp.service'
|
||||||
import { PlatformTheme } from '../../../tabby-core/src/api/platform'
|
|
||||||
import { configPath } from '../../../app/lib/config'
|
import { configPath } from '../../../app/lib/config'
|
||||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||||
|
|
||||||
@@ -272,19 +271,48 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
return transfer
|
return transfer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null> {
|
||||||
|
const selectedFolder = await this.pickDirectory(this.translate.instant('Select destination folder for {name}', { name }), this.translate.instant('Download here'))
|
||||||
|
if (!selectedFolder) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadPath = path.join(selectedFolder, name)
|
||||||
|
let counter = 1
|
||||||
|
while (fsSync.existsSync(downloadPath)) {
|
||||||
|
downloadPath = path.join(selectedFolder, `${name} (${counter})`)
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
const transfer = new ElectronDirectoryDownload(downloadPath, name, estimatedSize ?? 0, this.electron, this.zone)
|
||||||
|
await wrapPromise(this.zone, transfer.open())
|
||||||
|
this.fileTransferStarted.next(transfer)
|
||||||
|
return transfer
|
||||||
|
}
|
||||||
|
|
||||||
|
_registerFileTransfer (transfer: FileTransfer): void {
|
||||||
|
this.fileTransferStarted.next(transfer)
|
||||||
|
}
|
||||||
|
|
||||||
setErrorHandler (handler: (_: any) => void): void {
|
setErrorHandler (handler: (_: any) => void): void {
|
||||||
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
|
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
|
||||||
handler(err)
|
handler(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickDirectory (): Promise<string> {
|
async pickDirectory (title?: string, buttonLabel?: string): Promise<string | null> {
|
||||||
return (await this.electron.dialog.showOpenDialog(
|
const result = await this.electron.dialog.showOpenDialog(
|
||||||
this.hostWindow.getWindow(),
|
this.hostWindow.getWindow(),
|
||||||
{
|
{
|
||||||
|
title,
|
||||||
|
buttonLabel,
|
||||||
properties: ['openDirectory', 'showHiddenFiles'],
|
properties: ['openDirectory', 'showHiddenFiles'],
|
||||||
},
|
},
|
||||||
)).filePaths[0]
|
)
|
||||||
|
if (result.canceled || !result.filePaths.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return result.filePaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
getTheme (): PlatformTheme {
|
getTheme (): PlatformTheme {
|
||||||
@@ -313,6 +341,7 @@ class ElectronFileUpload extends FileUpload {
|
|||||||
const stat = await fs.stat(this.filePath)
|
const stat = await fs.stat(this.filePath)
|
||||||
this.size = stat.size
|
this.size = stat.size
|
||||||
this.mode = stat.mode
|
this.mode = stat.mode
|
||||||
|
this.setTotalSize(this.size)
|
||||||
this.file = await fs.open(this.filePath, 'r')
|
this.file = await fs.open(this.filePath, 'r')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +360,9 @@ class ElectronFileUpload extends FileUpload {
|
|||||||
async read (): Promise<Uint8Array> {
|
async read (): Promise<Uint8Array> {
|
||||||
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
|
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
|
||||||
this.increaseProgress(result.bytesRead)
|
this.increaseProgress(result.bytesRead)
|
||||||
|
if (this.getCompletedBytes() >= this.getSize()) {
|
||||||
|
this.setCompleted(true)
|
||||||
|
}
|
||||||
return this.buffer.slice(0, result.bytesRead)
|
return this.buffer.slice(0, result.bytesRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +384,7 @@ class ElectronFileDownload extends FileDownload {
|
|||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||||
|
this.setTotalSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
async open (): Promise<void> {
|
async open (): Promise<void> {
|
||||||
@@ -362,10 +395,6 @@ class ElectronFileDownload extends FileDownload {
|
|||||||
return path.basename(this.filePath)
|
return path.basename(this.filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
getMode (): number {
|
|
||||||
return this.mode
|
|
||||||
}
|
|
||||||
|
|
||||||
getSize (): number {
|
getSize (): number {
|
||||||
return this.size
|
return this.size
|
||||||
}
|
}
|
||||||
@@ -377,6 +406,9 @@ class ElectronFileDownload extends FileDownload {
|
|||||||
this.increaseProgress(result.bytesWritten)
|
this.increaseProgress(result.bytesWritten)
|
||||||
pos += result.bytesWritten
|
pos += result.bytesWritten
|
||||||
}
|
}
|
||||||
|
if (this.getCompletedBytes() >= this.getSize()) {
|
||||||
|
this.setCompleted(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close (): void {
|
close (): void {
|
||||||
@@ -384,3 +416,49 @@ class ElectronFileDownload extends FileDownload {
|
|||||||
this.file.close()
|
this.file.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ElectronDirectoryDownload extends DirectoryDownload {
|
||||||
|
private powerSaveBlocker = 0
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private basePath: string,
|
||||||
|
private name: string,
|
||||||
|
estimatedSize: number,
|
||||||
|
private electron: ElectronService,
|
||||||
|
private zone: NgZone,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||||
|
this.setTotalSize(estimatedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async open (): Promise<void> {
|
||||||
|
await fs.mkdir(this.basePath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
getName (): string {
|
||||||
|
return this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
getSize (): number {
|
||||||
|
return this.getTotalSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDirectory (relativePath: string): Promise<void> {
|
||||||
|
const fullPath = path.join(this.basePath, relativePath)
|
||||||
|
await fs.mkdir(fullPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFile (relativePath: string, mode: number, size: number): Promise<FileDownload> {
|
||||||
|
const fullPath = path.join(this.basePath, relativePath)
|
||||||
|
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
||||||
|
|
||||||
|
const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron)
|
||||||
|
await wrapPromise(this.zone, fileDownload.open())
|
||||||
|
return fileDownload
|
||||||
|
}
|
||||||
|
|
||||||
|
close (): void {
|
||||||
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@xterm/addon-web-links": "^0.10.0",
|
"@xterm/addon-web-links": "^0.10.0",
|
||||||
|
@@ -13,4 +13,12 @@ export abstract class LinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract handle (uri: string, tab?: BaseTerminalTabComponent<any>): void
|
abstract handle (uri: string, tab?: BaseTerminalTabComponent<any>): void
|
||||||
|
|
||||||
|
private _fullMatchRegex: RegExp | null = null
|
||||||
|
get fullMatchRegex (): RegExp {
|
||||||
|
if (!this._fullMatchRegex) {
|
||||||
|
this._fullMatchRegex = new RegExp(`^${this.regex.source}$`)
|
||||||
|
}
|
||||||
|
return this._fullMatchRegex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ export class LinkHighlighterDecorator extends TerminalDecorator {
|
|||||||
|
|
||||||
const openLink = async uri => {
|
const openLink = async uri => {
|
||||||
for (const handler of this.handlers) {
|
for (const handler of this.handlers) {
|
||||||
if (!handler.regex.test(uri)) {
|
if (!handler.fullMatchRegex.test(uri)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!await handler.verify(await handler.convert(uri, tab), tab)) {
|
if (!await handler.verify(await handler.convert(uri, tab), tab)) {
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ansi-colors": "^4.1.1",
|
"ansi-colors": "^4.1.1",
|
||||||
|
@@ -28,6 +28,10 @@ export class LocalProfileSettingsComponent implements ProfileSettingsComponent<L
|
|||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
this.profile.options.cwd = await this.platform.pickDirectory()
|
const cwd = await this.platform.pickDirectory()
|
||||||
|
if (!cwd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.profile.options.cwd = cwd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/semver": "^7.1.0",
|
"@types/semver": "^7.1.0",
|
||||||
|
@@ -100,6 +100,7 @@ export class PluginsSettingsTabComponent {
|
|||||||
this.busy.delete(plugin.name)
|
this.busy.delete(plugin.name)
|
||||||
this.config.requestRestart()
|
this.config.requestRestart()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Error installing plugin', plugin.name, err)
|
||||||
this.erroredPlugin = plugin.name
|
this.erroredPlugin = plugin.name
|
||||||
this.errorMessage = err
|
this.errorMessage = err
|
||||||
this.busy.delete(plugin.name)
|
this.busy.delete(plugin.name)
|
||||||
@@ -114,6 +115,7 @@ export class PluginsSettingsTabComponent {
|
|||||||
this.busy.delete(plugin.name)
|
this.busy.delete(plugin.name)
|
||||||
this.config.requestRestart()
|
this.config.requestRestart()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Error uninstalling plugin', plugin.name, err)
|
||||||
this.erroredPlugin = plugin.name
|
this.erroredPlugin = plugin.name
|
||||||
this.errorMessage = err
|
this.errorMessage = err
|
||||||
this.busy.delete(plugin.name)
|
this.busy.delete(plugin.name)
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "14.14.14",
|
"@types/node": "14.14.14",
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/marked": "^5.0.1",
|
"@types/marked": "^5.0.1",
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.3.1",
|
"@types/node": "20.3.1",
|
||||||
|
@@ -6,6 +6,7 @@ export const supportedAlgorithms = {
|
|||||||
[SSHAlgorithmType.HOSTKEY]: russh.getSupportedKeyTypes().filter(x => x !== 'none'),
|
[SSHAlgorithmType.HOSTKEY]: russh.getSupportedKeyTypes().filter(x => x !== 'none'),
|
||||||
[SSHAlgorithmType.CIPHER]: russh.getSupportedCiphers().filter(x => x !== 'clear'),
|
[SSHAlgorithmType.CIPHER]: russh.getSupportedCiphers().filter(x => x !== 'clear'),
|
||||||
[SSHAlgorithmType.HMAC]: russh.getSupportedMACs().filter(x => x !== 'none'),
|
[SSHAlgorithmType.HMAC]: russh.getSupportedMACs().filter(x => x !== 'none'),
|
||||||
|
[SSHAlgorithmType.COMPRESSION]: russh.getSupportedCompressionAlgorithms().reverse(),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAlgorithms = {
|
export const defaultAlgorithms = {
|
||||||
@@ -42,4 +43,9 @@ export const defaultAlgorithms = {
|
|||||||
'hmac-sha1-etm@openssh.com',
|
'hmac-sha1-etm@openssh.com',
|
||||||
'hmac-sha1',
|
'hmac-sha1',
|
||||||
],
|
],
|
||||||
|
[SSHAlgorithmType.COMPRESSION]: [
|
||||||
|
'zlib@openssh.com',
|
||||||
|
'zlib',
|
||||||
|
'none',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,8 @@ export enum SSHAlgorithmType {
|
|||||||
KEX = 'kex',
|
KEX = 'kex',
|
||||||
CIPHER = 'cipher',
|
CIPHER = 'cipher',
|
||||||
HOSTKEY = 'serverHostKey',
|
HOSTKEY = 'serverHostKey',
|
||||||
|
COMPRESSION = 'compression',
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHProfile extends ConnectableTerminalProfile {
|
export interface SSHProfile extends ConnectableTerminalProfile {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as C from 'constants'
|
import * as C from 'constants'
|
||||||
import { posix as path } from 'path'
|
import { posix as path } from 'path'
|
||||||
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
|
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
|
||||||
import { FileUpload, DirectoryUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
|
import { FileUpload, DirectoryUpload, DirectoryDownload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
|
||||||
import { SFTPSession, SFTPFile } from '../session/sftp'
|
import { SFTPSession, SFTPFile } from '../session/sftp'
|
||||||
import { SSHSession } from '../session/ssh'
|
import { SSHSession } from '../session/ssh'
|
||||||
import { SFTPContextMenuItemProvider } from '../api'
|
import { SFTPContextMenuItemProvider } from '../api'
|
||||||
@@ -220,6 +220,68 @@ export class SFTPPanelComponent {
|
|||||||
this.sftp.download(itemPath, transfer)
|
this.sftp.download(itemPath, transfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadFolder (folder: SFTPFile): Promise<void> {
|
||||||
|
try {
|
||||||
|
const transfer = await this.platform.startDownloadDirectory(folder.name, 0)
|
||||||
|
if (!transfer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background size calculation and download simultaneously
|
||||||
|
const sizeCalculationPromise = this.calculateFolderSizeAndUpdate(folder, transfer)
|
||||||
|
const downloadPromise = this.downloadFolderRecursive(folder, transfer, '')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([sizeCalculationPromise, downloadPromise])
|
||||||
|
transfer.setStatus('')
|
||||||
|
transfer.setCompleted(true)
|
||||||
|
} catch (error) {
|
||||||
|
transfer.cancel()
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
transfer.close()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.notifications.error(`Failed to download folder: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async calculateFolderSizeAndUpdate (folder: SFTPFile, transfer: DirectoryDownload) {
|
||||||
|
let totalSize = 0
|
||||||
|
const items = await this.sftp.readdir(folder.fullPath)
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.isDirectory) {
|
||||||
|
totalSize += await this.calculateFolderSizeAndUpdate(item, transfer)
|
||||||
|
} else {
|
||||||
|
totalSize += item.size
|
||||||
|
}
|
||||||
|
transfer.setTotalSize(totalSize)
|
||||||
|
}
|
||||||
|
return totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadFolderRecursive (folder: SFTPFile, transfer: DirectoryDownload, relativePath: string): Promise<void> {
|
||||||
|
const items = await this.sftp.readdir(folder.fullPath)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (transfer.isCancelled()) {
|
||||||
|
throw new Error('Download cancelled')
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
|
||||||
|
|
||||||
|
transfer.setStatus(itemRelativePath)
|
||||||
|
if (item.isDirectory) {
|
||||||
|
await transfer.createDirectory(itemRelativePath)
|
||||||
|
await this.downloadFolderRecursive(item, transfer, itemRelativePath)
|
||||||
|
} else {
|
||||||
|
const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size)
|
||||||
|
await this.sftp.download(item.fullPath, fileDownload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getModeString (item: SFTPFile): string {
|
getModeString (item: SFTPFile): string {
|
||||||
const s = 'SGdrwxrwxrwx'
|
const s = 'SGdrwxrwxrwx'
|
||||||
const e = ' ---------'
|
const e = ' ---------'
|
||||||
|
@@ -285,7 +285,13 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
.w-75
|
.w-75
|
||||||
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
|
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
|
||||||
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
|
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
|
||||||
|
|
||||||
|
.form-line.align-items-start
|
||||||
|
.header
|
||||||
|
.title Compression
|
||||||
|
.w-75
|
||||||
|
div(*ngFor='let alg of supportedAlgorithms.compression')
|
||||||
|
checkbox([text]='alg', [(ngModel)]='algorithms.compression[alg]')
|
||||||
li(ngbNavItem)
|
li(ngbNavItem)
|
||||||
a(ngbNavLink, translate) Colors
|
a(ngbNavLink, translate) Colors
|
||||||
ng-template(ngbNavContent)
|
ng-template(ngbNavContent)
|
||||||
|
@@ -107,7 +107,7 @@ export class SSHProfileSettingsComponent {
|
|||||||
this.profile.options.algorithms![k] = Object.entries(this.algorithms[k])
|
this.profile.options.algorithms![k] = Object.entries(this.algorithms[k])
|
||||||
.filter(([_, v]) => !!v)
|
.filter(([_, v]) => !!v)
|
||||||
.map(([key, _]) => key)
|
.map(([key, _]) => key)
|
||||||
this.profile.options.algorithms![k].sort()
|
if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms![k].sort() }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.connectionMode !== 'jumpHost') {
|
if (this.connectionMode !== 'jumpHost') {
|
||||||
|
@@ -179,6 +179,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
|||||||
try {
|
try {
|
||||||
await this.initializeSessionMaybeMultiplex(false)
|
await this.initializeSessionMaybeMultiplex(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('SSH session initialization failed', e)
|
||||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -66,4 +66,5 @@ export default class SSHModule { }
|
|||||||
|
|
||||||
export * from './api'
|
export * from './api'
|
||||||
export { SFTPFile, SFTPSession } from './session/sftp'
|
export { SFTPFile, SFTPSession } from './session/sftp'
|
||||||
export { SFTPPanelComponent }
|
export { SFTPPanelComponent, SSHTabComponent }
|
||||||
|
export { PasswordStorageService } from './services/passwordStorage.service'
|
||||||
|
@@ -20,7 +20,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
|||||||
auth: null,
|
auth: null,
|
||||||
password: null,
|
password: null,
|
||||||
privateKeys: [],
|
privateKeys: [],
|
||||||
keepaliveInterval: 5000,
|
keepaliveInterval: null,
|
||||||
keepaliveCountMax: 10,
|
keepaliveCountMax: 10,
|
||||||
readyTimeout: null,
|
readyTimeout: null,
|
||||||
x11: false,
|
x11: false,
|
||||||
@@ -55,7 +55,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
|||||||
super()
|
super()
|
||||||
for (const k of Object.values(SSHAlgorithmType)) {
|
for (const k of Object.values(SSHAlgorithmType)) {
|
||||||
this.configDefaults.options.algorithms[k] = [...defaultAlgorithms[k]]
|
this.configDefaults.options.algorithms[k] = [...defaultAlgorithms[k]]
|
||||||
this.configDefaults.options.algorithms[k].sort()
|
if (k !== SSHAlgorithmType.COMPRESSION) { this.configDefaults.options.algorithms[k].sort() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -315,8 +315,9 @@ export class SSHSession {
|
|||||||
kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
|
kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
|
||||||
mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
|
mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
|
||||||
key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
|
key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
|
||||||
|
compression: this.profile.options.algorithms?.[SSHAlgorithmType.COMPRESSION]?.filter(x => supportedAlgorithms[SSHAlgorithmType.COMPRESSION].includes(x)),
|
||||||
},
|
},
|
||||||
keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000),
|
keepaliveIntervalSeconds: this.profile.options.keepaliveInterval ? Math.round(this.profile.options.keepaliveInterval / 1000) : undefined,
|
||||||
keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
||||||
connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined,
|
connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined,
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { MenuItemOptions, PlatformService, TranslateService } from 'tabby-core'
|
import { MenuItemOptions, PlatformService, TranslateService, HostAppService, Platform } from 'tabby-core'
|
||||||
import { SFTPSession, SFTPFile } from './session/sftp'
|
import { SFTPSession, SFTPFile } from './session/sftp'
|
||||||
import { SFTPContextMenuItemProvider } from './api'
|
import { SFTPContextMenuItemProvider } from './api'
|
||||||
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||||
@@ -16,37 +16,49 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
|
|||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||||
return [
|
const items: MenuItemOptions[] = [
|
||||||
{
|
{
|
||||||
click: async () => {
|
click: async () => {
|
||||||
await panel.openCreateDirectoryModal()
|
await panel.openCreateDirectoryModal()
|
||||||
},
|
},
|
||||||
label: this.translate.instant('Create directory'),
|
label: this.translate.instant('Create directory'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
click: async () => {
|
|
||||||
if ((await this.platform.showMessageBox({
|
|
||||||
type: 'warning',
|
|
||||||
message: this.translate.instant('Delete {fullPath}?', item),
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1,
|
|
||||||
buttons: [
|
|
||||||
this.translate.instant('Delete'),
|
|
||||||
this.translate.instant('Cancel'),
|
|
||||||
],
|
|
||||||
})).response === 0) {
|
|
||||||
await this.deleteItem(item, panel.sftp)
|
|
||||||
panel.navigate(panel.path)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: this.translate.instant('Delete'),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Add download folder option for directories (only in electron)
|
||||||
|
if (item.isDirectory && this.hostApp.platform !== Platform.Web) {
|
||||||
|
items.push({
|
||||||
|
click: () => panel.downloadFolder(item),
|
||||||
|
label: this.translate.instant('Download directory'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
click: async () => {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: this.translate.instant('Delete {fullPath}?', item),
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
buttons: [
|
||||||
|
this.translate.instant('Delete'),
|
||||||
|
this.translate.instant('Cancel'),
|
||||||
|
],
|
||||||
|
})).response === 0) {
|
||||||
|
await this.deleteItem(item, panel.sftp)
|
||||||
|
panel.navigate(panel.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: this.translate.instant('Delete'),
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
|
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ansi-colors": "^4.1.1",
|
"ansi-colors": "^4.1.1",
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@xterm/addon-canvas": "^0.6.0",
|
"@xterm/addon-canvas": "^0.6.0",
|
||||||
|
@@ -17,6 +17,8 @@ import { getTerminalBackgroundColor } from '../helpers'
|
|||||||
|
|
||||||
|
|
||||||
const INACTIVE_TAB_UNLOAD_DELAY = 1000 * 30
|
const INACTIVE_TAB_UNLOAD_DELAY = 1000 * 30
|
||||||
|
const OSC_FOCUS_IN = Buffer.from('\x1b[I')
|
||||||
|
const OSC_FOCUS_OUT = Buffer.from('\x1b[O')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to base your custom terminal tabs on
|
* A class to base your custom terminal tabs on
|
||||||
@@ -494,7 +496,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
|
|||||||
data = Buffer.from(data, 'utf-8')
|
data = Buffer.from(data, 'utf-8')
|
||||||
}
|
}
|
||||||
this.session?.feedFromTerminal(data)
|
this.session?.feedFromTerminal(data)
|
||||||
if (this.config.store.terminal.scrollOnInput) {
|
if (this.config.store.terminal.scrollOnInput && !data.equals(OSC_FOCUS_IN) && !data.equals(OSC_FOCUS_OUT)) {
|
||||||
this.frontend?.scrollToBottom()
|
this.frontend?.scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -542,7 +544,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.alternateScreenActive) {
|
if (!this.alternateScreenActive) {
|
||||||
if (data.includes('\r') && this.config.store.terminal.warnOnMultilinePaste) {
|
if ((data.includes('\r') || data.includes('\n')) && this.config.store.terminal.warnOnMultilinePaste) {
|
||||||
const buttons = [
|
const buttons = [
|
||||||
this.translate.instant('Paste'),
|
this.translate.instant('Paste'),
|
||||||
this.translate.instant('Cancel'),
|
this.translate.instant('Cancel'),
|
||||||
|
@@ -16,14 +16,14 @@
|
|||||||
ng-container(*ngIf='state.resultCount > 0')
|
ng-container(*ngIf='state.resultCount > 0')
|
||||||
button.btn.btn-link(
|
button.btn.btn-link(
|
||||||
(click)='findPrevious()',
|
(click)='findPrevious()',
|
||||||
ngbTooltip='Search up',
|
[ngbTooltip]='"Search up"|translate',
|
||||||
placement='bottom',
|
placement='bottom',
|
||||||
[fastHtmlBind]='icons.arrowUp'
|
[fastHtmlBind]='icons.arrowUp'
|
||||||
)
|
)
|
||||||
|
|
||||||
button.btn.btn-link(
|
button.btn.btn-link(
|
||||||
(click)='findNext()',
|
(click)='findNext()',
|
||||||
ngbTooltip='Search down',
|
[ngbTooltip]='"Search down"|translate',
|
||||||
placement='bottom',
|
placement='bottom',
|
||||||
[fastHtmlBind]='icons.arrowDown'
|
[fastHtmlBind]='icons.arrowDown'
|
||||||
)
|
)
|
||||||
@@ -34,7 +34,7 @@ button.btn(
|
|||||||
(click)='options.caseSensitive = !options.caseSensitive; saveSearchOptions()',
|
(click)='options.caseSensitive = !options.caseSensitive; saveSearchOptions()',
|
||||||
[class.btn-link]='!options.caseSensitive',
|
[class.btn-link]='!options.caseSensitive',
|
||||||
[class.btn-info]='options.caseSensitive',
|
[class.btn-info]='options.caseSensitive',
|
||||||
ngbTooltip='Case sensitivity',
|
[ngbTooltip]='"Case sensitivity"|translate',
|
||||||
placement='bottom',
|
placement='bottom',
|
||||||
[fastHtmlBind]='icons.case'
|
[fastHtmlBind]='icons.case'
|
||||||
)
|
)
|
||||||
@@ -43,7 +43,7 @@ button.btn(
|
|||||||
(click)='options.regex = !options.regex; saveSearchOptions()',
|
(click)='options.regex = !options.regex; saveSearchOptions()',
|
||||||
[class.btn-link]='!options.regex',
|
[class.btn-link]='!options.regex',
|
||||||
[class.btn-info]='options.regex',
|
[class.btn-info]='options.regex',
|
||||||
ngbTooltip='Regular expression',
|
[ngbTooltip]='"Regular expression"|translate',
|
||||||
placement='bottom',
|
placement='bottom',
|
||||||
[fastHtmlBind]='icons.regexp'
|
[fastHtmlBind]='icons.regexp'
|
||||||
)
|
)
|
||||||
@@ -52,7 +52,7 @@ button.btn(
|
|||||||
(click)='options.wholeWord = !options.wholeWord; saveSearchOptions()',
|
(click)='options.wholeWord = !options.wholeWord; saveSearchOptions()',
|
||||||
[class.btn-link]='!options.wholeWord',
|
[class.btn-link]='!options.wholeWord',
|
||||||
[class.btn-info]='options.wholeWord',
|
[class.btn-info]='options.wholeWord',
|
||||||
ngbTooltip='Whole word',
|
[ngbTooltip]='"Whole word"|translate',
|
||||||
placement='bottom',
|
placement='bottom',
|
||||||
[fastHtmlBind]='icons.wholeWord'
|
[fastHtmlBind]='icons.wholeWord'
|
||||||
)
|
)
|
||||||
|
@@ -27,7 +27,7 @@ class ZModemMiddleware extends SessionMiddleware {
|
|||||||
this.logger = log.create('zmodem')
|
this.logger = log.create('zmodem')
|
||||||
this.sentry = new ZModem.Sentry({
|
this.sentry = new ZModem.Sentry({
|
||||||
to_terminal: data => {
|
to_terminal: data => {
|
||||||
if (this.isActive) {
|
if (this.isActive && this.activeSession) {
|
||||||
this.outputToTerminal.next(Buffer.from(data))
|
this.outputToTerminal.next(Buffer.from(data))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -42,25 +42,32 @@ class ZModemMiddleware extends SessionMiddleware {
|
|||||||
},
|
},
|
||||||
on_retract: () => {
|
on_retract: () => {
|
||||||
this.showMessage('transfer cancelled')
|
this.showMessage('transfer cancelled')
|
||||||
|
this.activeSession = null
|
||||||
|
this.isActive = false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
feedFromSession (data: Buffer): void {
|
feedFromSession (data: Buffer): void {
|
||||||
const chunkSize = 1024
|
if (this.isActive || this.activeSession) {
|
||||||
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
|
|
||||||
try {
|
try {
|
||||||
this.sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
|
this.sentry.consume(data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showMessage(colors.bgRed.black(' Error ') + ' ' + e)
|
this.showMessage(colors.bgRed.black(' Error ') + ' ' + e)
|
||||||
this.logger.error('protocol error', e)
|
this.logger.error('protocol error', e)
|
||||||
this.activeSession.abort()
|
this.activeSession?.abort()
|
||||||
this.activeSession = null
|
this.activeSession = null
|
||||||
this.isActive = false
|
this.isActive = false
|
||||||
|
// Don't forward the problematic data to terminal
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if (!this.isActive) {
|
try {
|
||||||
|
this.sentry.consume(data)
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('zmodem detection error', e)
|
||||||
|
}
|
||||||
|
|
||||||
this.outputToTerminal.next(data)
|
this.outputToTerminal.next(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,25 +80,35 @@ class ZModemMiddleware extends SessionMiddleware {
|
|||||||
this.activeSession = zsession
|
this.activeSession = zsession
|
||||||
this.logger.info('new session', zsession)
|
this.logger.info('new session', zsession)
|
||||||
|
|
||||||
if (zsession.type === 'send') {
|
try {
|
||||||
const transfers = await this.platform.startUpload({ multiple: true })
|
if (zsession.type === 'send') {
|
||||||
let filesRemaining = transfers.length
|
const transfers = await this.platform.startUpload({ multiple: true })
|
||||||
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
let filesRemaining = transfers.length
|
||||||
for (const transfer of transfers) {
|
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
||||||
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
|
for (const transfer of transfers) {
|
||||||
filesRemaining--
|
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
|
||||||
sizeRemaining -= transfer.getSize()
|
filesRemaining--
|
||||||
|
sizeRemaining -= transfer.getSize()
|
||||||
|
}
|
||||||
|
await zsession.close()
|
||||||
|
} else {
|
||||||
|
zsession.on('offer', xfer => {
|
||||||
|
this.receiveFile(xfer, zsession)
|
||||||
|
})
|
||||||
|
|
||||||
|
zsession.start()
|
||||||
|
|
||||||
|
await new Promise(resolve => zsession.on('session_end', resolve))
|
||||||
}
|
}
|
||||||
this.activeSession = null
|
|
||||||
await zsession.close()
|
|
||||||
} else {
|
|
||||||
zsession.on('offer', xfer => {
|
|
||||||
this.receiveFile(xfer, zsession)
|
|
||||||
})
|
|
||||||
|
|
||||||
zsession.start()
|
this.showMessage(colors.bgBlue.black(' ZMODEM ') + ' Complete')
|
||||||
|
} catch (error) {
|
||||||
await new Promise(resolve => zsession.on('session_end', resolve))
|
this.logger.error('ZMODEM session error', error)
|
||||||
|
this.showMessage(colors.bgRed.black(' ZMODEM ') + ` Session failed: ${error.message}`)
|
||||||
|
try {
|
||||||
|
zsession.abort()
|
||||||
|
} catch { }
|
||||||
|
} finally {
|
||||||
this.activeSession = null
|
this.activeSession = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
"data",
|
"data",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/webpack-env": "^1.16.0"
|
"@types/webpack-env": "^1.16.0"
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"typings"
|
"typings"
|
||||||
],
|
],
|
||||||
"author": "Eugene Pankov",
|
"author": "Tabby Developers",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/core": "^15"
|
"@angular/core": "^15"
|
||||||
|
@@ -2,7 +2,7 @@ import '@vaadin/vaadin-context-menu'
|
|||||||
import copyToClipboard from 'copy-text-to-clipboard'
|
import copyToClipboard from 'copy-text-to-clipboard'
|
||||||
import { Injectable, Inject } from '@angular/core'
|
import { Injectable, Inject } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, HTMLFileUpload, DirectoryUpload } from 'tabby-core'
|
import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, DirectoryDownload, HTMLFileUpload, DirectoryUpload } from 'tabby-core'
|
||||||
|
|
||||||
// eslint-disable-next-line no-duplicate-imports
|
// eslint-disable-next-line no-duplicate-imports
|
||||||
import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu'
|
import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu'
|
||||||
@@ -114,6 +114,10 @@ export class WebPlatformService extends PlatformService {
|
|||||||
return transfer
|
return transfer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startDownloadDirectory (_name: string, _estimatedSize?: number): Promise<DirectoryDownload|null> {
|
||||||
|
throw new Error('Unsupported')
|
||||||
|
}
|
||||||
|
|
||||||
startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.fileSelector.onchange = () => {
|
this.fileSelector.onchange = () => {
|
||||||
|
@@ -3179,10 +3179,10 @@ electron-to-chromium@^1.4.284:
|
|||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz#0e039de59135f44ab9a8ec9025e53a9135eba11f"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz#0e039de59135f44ab9a8ec9025e53a9135eba11f"
|
||||||
integrity sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==
|
integrity sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==
|
||||||
|
|
||||||
electron@^36.3:
|
electron@^36.4:
|
||||||
version "36.3.1"
|
version "36.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.1.tgz#12a8c1b1cd9163a4bd0cb60f89816243b26ab788"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-36.7.1.tgz#73bbb460c60f529e00b9d3eff78fd135c42172ea"
|
||||||
integrity sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==
|
integrity sha512-vkih7vbmWT6O8+VWFt3a9FMLUZn0O4piR20nTX0IL/d9tz9RjpzoMvHqpI2CE1Rxew9bCzrg7FpgtcTdY6dlyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get" "^2.0.0"
|
"@electron/get" "^2.0.0"
|
||||||
"@types/node" "^22.7.7"
|
"@types/node" "^22.7.7"
|
||||||
|
Reference in New Issue
Block a user