Compare commits

..

6 Commits

Author SHA1 Message Date
allcontributors[bot]
8571dffd8d update README.de-DE.md [skip ci] 2025-05-25 06:48:29 +00:00
allcontributors[bot]
243f57108e update README.it-IT.md [skip ci] 2025-05-25 06:48:28 +00:00
allcontributors[bot]
a7352aad7f update README.ko-KR.md [skip ci] 2025-05-25 06:48:27 +00:00
allcontributors[bot]
ae6c7d0d82 update README.ru-RU.md [skip ci] 2025-05-25 06:48:26 +00:00
allcontributors[bot]
79c713aa1f update README.zh-CN.md [skip ci] 2025-05-25 06:48:25 +00:00
allcontributors[bot]
4a0d452e87 update README.md [skip ci] 2025-05-25 06:48:24 +00:00
54 changed files with 109 additions and 550 deletions

View File

@@ -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 distribution, for Windows ├─ clink # Clink distributive, 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

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Tabby Developers Copyright (c) 2017 Eugene Pankov
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

View File

@@ -349,6 +349,7 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -343,6 +343,7 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -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) - 터미널의 경로 및 URL을 클릭 가능하게 * [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - m터미널의 경로 및 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 -->
@@ -342,6 +342,7 @@ Pull requests and plugins are welcome!
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -366,6 +366,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -343,6 +343,7 @@ Pull-запросы и плагины приветствуются!
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -342,6 +342,7 @@
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geodic"><img src="https://avatars.githubusercontent.com/u/64704703?v=4?s=100" width="100px;" alt="geodic"/><br /><sub><b>geodic</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=geodic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loopx9"><img src="https://avatars.githubusercontent.com/u/46422475?v=4?s=100" width="100px;" alt="loopx9"/><br /><sub><b>loopx9</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=loopx9" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"repository": "https://github.com/Eugeny/tabby", "repository": "https://github.com/Eugeny/tabby",
"author": { "author": {
"name": "Tabby Developers", "name": "Eugene Pankov",
"email": "e@ajenti.org" "email": "e@ajenti.org"
}, },
"main": "dist/main.js", "main": "dist/main.js",

View File

@@ -6,6 +6,4 @@ 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,
] ]

View File

@@ -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.4", "electron": "^36.3",
"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",

View File

@@ -31,7 +31,6 @@ 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 = [

View File

@@ -1,23 +0,0 @@
{
"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"
}

View File

@@ -1,89 +0,0 @@
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)
}))
})
}
}

View File

@@ -1,16 +0,0 @@
/* 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 { }

View File

@@ -1,7 +0,0 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "src",
}
}

View File

@@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"compilerOptions": {
"baseUrl": "src",
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./typings",
"paths": {
"tabby-*": ["../../tabby-*"],
"*": ["../../app/node_modules/*"]
}
}
}

View File

@@ -1,10 +0,0 @@
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,
})

View File

@@ -1,8 +0,0 @@
# 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==

View File

@@ -14,7 +14,7 @@
"files": [ "files": [
"dist" "dist"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@angular/core": "^15", "@angular/core": "^15",

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"bootstrap": "^5.3.0-alpha.1", "bootstrap": "^5.3.0-alpha.1",

View File

@@ -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, DirectoryDownload, PlatformTheme } from './platform' export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload } 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'

View File

@@ -22,6 +22,7 @@ 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
@@ -33,16 +34,8 @@ 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.completed return this.completedBytes >= this.getSize()
} }
isCancelled (): boolean { isCancelled (): boolean {
@@ -54,18 +47,6 @@ 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
@@ -76,26 +57,16 @@ 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> {
@@ -156,7 +127,6 @@ 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>
@@ -267,7 +237,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 | null> abstract pickDirectory (): Promise<string>
abstract quit (): void abstract quit (): void
} }

View File

@@ -35,8 +35,7 @@ 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
@@ -44,7 +43,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(
[ngbTooltip]='button.label', [title]='button.label',
(click)='button.run && button.run()', (click)='button.run && button.run()',
[fastHtmlBind]='button.icon' [fastHtmlBind]='button.icon'
) )
@@ -56,7 +55,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',
[ngbTooltip]='"File transfers"|translate', title='File transfers',
ngbDropdownToggle ngbDropdownToggle
) !{require('../icons/transfers.svg')} ) !{require('../icons/transfers.svg')}
transfers-menu( transfers-menu(
@@ -76,14 +75,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(
[ngbTooltip]='button.label', [title]='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',
[ngbTooltip]='"Update available - Click to install"|translate', title='Update available - Click to install',
(click)='updater.update()' (click)='updater.update()'
) !{require('../icons/gift.svg')} ) !{require('../icons/gift.svg')}

View File

@@ -13,6 +13,7 @@ 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',

View File

@@ -5,9 +5,7 @@
.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([ngbTooltip]='transfer.getName()') label.no-wrap([title]='transfer.getName()') {{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}}

View File

@@ -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, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } 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,7 +155,6 @@ 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,
@@ -202,10 +201,6 @@ 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> {

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@angular/core": "^15", "@angular/core": "^15",

View File

@@ -5,11 +5,12 @@ 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, DirectoryDownload, FileUploadOptions, wrapPromise, TranslateService, FileTransfer, PlatformTheme } from 'tabby-core' import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } 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
@@ -271,48 +272,19 @@ 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 (title?: string, buttonLabel?: string): Promise<string | null> { async pickDirectory (): Promise<string> {
const result = await this.electron.dialog.showOpenDialog( return (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 {
@@ -341,7 +313,6 @@ 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')
} }
@@ -360,9 +331,6 @@ 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)
} }
@@ -384,7 +352,6 @@ 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> {
@@ -395,6 +362,10 @@ 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
} }
@@ -406,9 +377,6 @@ 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 {
@@ -416,49 +384,3 @@ 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)
}
}

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@xterm/addon-web-links": "^0.10.0", "@xterm/addon-web-links": "^0.10.0",

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",

View File

@@ -28,10 +28,6 @@ export class LocalProfileSettingsComponent implements ProfileSettingsComponent<L
// return // return
// } // }
const cwd = await this.platform.pickDirectory() this.profile.options.cwd = await this.platform.pickDirectory()
if (!cwd) {
return
}
this.profile.options.cwd = cwd
} }
} }

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",

View File

@@ -100,7 +100,6 @@ 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)
@@ -115,7 +114,6 @@ 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)

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "14.14.14", "@types/node": "14.14.14",

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/marked": "^5.0.1", "@types/marked": "^5.0.1",

View File

@@ -17,7 +17,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "20.3.1", "@types/node": "20.3.1",

View File

@@ -17,10 +17,6 @@
.breadcrumb-spacer.flex-grow-1.h-100((dblclick)='editPath()') .breadcrumb-spacer.flex-grow-1.h-100((dblclick)='editPath()')
button.btn.btn-link.btn-sm.flex-shrink-0.d-flex(*ngIf='!showFilter', (click)='showFilter = true')
i.fas.fa-filter.me-1
div(translate) Filter
button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='openCreateDirectoryModal()') button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='openCreateDirectoryModal()')
i.fas.fa-plus.me-1 i.fas.fa-plus.me-1
div(translate) Create directory div(translate) Create directory
@@ -35,19 +31,6 @@
button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')} button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')}
.filter-bar.px-3.py-2.border-bottom(*ngIf='showFilter')
.input-group
input.form-control(
type='text',
placeholder='Filter...',
autofocus,
[(ngModel)]='filterText',
(input)='onFilterChange()',
(keydown.escape)='clearFilter()'
)
button.btn.btn-secondary((click)='clearFilter()')
i.fas.fa-times
.body(dropZone, (transfer)='uploadOneFolder($event)') .body(dropZone, (transfer)='uploadOneFolder($event)')
a.alert.alert-info.d-flex.align-items-center( a.alert.alert-info.d-flex.align-items-center(
*ngIf='shouldShowCWDTip && !cwdDetectionAvailable', *ngIf='shouldShowCWDTip && !cwdDetectionAvailable',
@@ -64,13 +47,13 @@
div(*ngIf='fileList === null', translate) Loading div(*ngIf='fileList === null', translate) Loading
.list-group.list-group-light(*ngIf='fileList !== null') .list-group.list-group-light(*ngIf='fileList !== null')
.list-group-item.list-group-item-action.d-flex.align-items-center( .list-group-item.list-group-item-action.d-flex.align-items-center(
*ngIf='path !== "/" && (!showFilter || filterText.trim() === "")', *ngIf='path !== "/"',
(click)='goUp()' (click)='goUp()'
) )
i.fas.fa-fw.fa-level-up-alt i.fas.fa-fw.fa-level-up-alt
div(translate) Go up div(translate) Go up
.list-group-item.list-group-item-action.d-flex.align-items-center( .list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let item of filteredFileList', *ngFor='let item of fileList',
(contextmenu)='showContextMenu(item, $event)', (contextmenu)='showContextMenu(item, $event)',
(click)='open(item)' (click)='open(item)'
) )
@@ -80,6 +63,3 @@
.size(*ngIf='!item.isDirectory') {{item.size|filesize}} .size(*ngIf='!item.isDirectory') {{item.size|filesize}}
.date {{item.modified|tabbyDate}} .date {{item.modified|tabbyDate}}
.mode {{getModeString(item)}} .mode {{getModeString(item)}}
.alert.alert-info.text-center.mt-3(*ngIf='fileList !== null && filteredFileList.length === 0 && showFilter && filterText.trim() !== ""')
i.fas.fa-search.me-2
span(translate) No files match the filter "{{filterText}}"

View File

@@ -9,10 +9,6 @@
flex: none; flex: none;
} }
> .filter-bar {
flex: none;
}
> .body { > .body {
padding: 10px 20px; padding: 10px 20px;
flex: 1 1 0; flex: 1 1 0;

View File

@@ -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, DirectoryDownload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core' import { FileUpload, DirectoryUpload, 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'
@@ -23,14 +23,11 @@ export class SFTPPanelComponent {
@Output() closed = new EventEmitter<void>() @Output() closed = new EventEmitter<void>()
sftp: SFTPSession sftp: SFTPSession
fileList: SFTPFile[]|null = null fileList: SFTPFile[]|null = null
filteredFileList: SFTPFile[] = []
@Input() path = '/' @Input() path = '/'
@Output() pathChange = new EventEmitter<string>() @Output() pathChange = new EventEmitter<string>()
pathSegments: PathSegment[] = [] pathSegments: PathSegment[] = []
@Input() cwdDetectionAvailable = false @Input() cwdDetectionAvailable = false
editingPath: string|null = null editingPath: string|null = null
showFilter = false
filterText = ''
constructor ( constructor (
private ngbModal: NgbModal, private ngbModal: NgbModal,
@@ -57,8 +54,6 @@ export class SFTPPanelComponent {
this.path = newPath this.path = newPath
this.pathChange.next(this.path) this.pathChange.next(this.path)
this.clearFilter()
let p = newPath let p = newPath
this.pathSegments = [] this.pathSegments = []
while (p !== '/') { while (p !== '/') {
@@ -70,7 +65,6 @@ export class SFTPPanelComponent {
} }
this.fileList = null this.fileList = null
this.filteredFileList = []
try { try {
this.fileList = await this.sftp.readdir(this.path) this.fileList = await this.sftp.readdir(this.path)
} catch (error) { } catch (error) {
@@ -85,8 +79,6 @@ export class SFTPPanelComponent {
this.fileList.sort((a, b) => this.fileList.sort((a, b) =>
dirKey(b) - dirKey(a) || dirKey(b) - dirKey(a) ||
a.name.localeCompare(b.name)) a.name.localeCompare(b.name))
this.updateFilteredList()
} }
getFileType (fileExtension: string): string { getFileType (fileExtension: string): string {
@@ -228,68 +220,6 @@ 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 = ' ---------'
@@ -344,29 +274,4 @@ export class SFTPPanelComponent {
this.closed.emit() this.closed.emit()
} }
clearFilter (): void {
this.showFilter = false
this.filterText = ''
this.updateFilteredList()
}
onFilterChange (): void {
this.updateFilteredList()
}
private updateFilteredList (): void {
if (!this.fileList) {
this.filteredFileList = []
return
}
if (!this.showFilter || this.filterText.trim() === '') {
this.filteredFileList = this.fileList
return
}
this.filteredFileList = this.fileList.filter(item =>
item.name.toLowerCase().includes(this.filterText.toLowerCase()),
)
}
} }

View File

@@ -179,7 +179,6 @@ 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
} }

View File

@@ -66,5 +66,4 @@ export default class SSHModule { }
export * from './api' export * from './api'
export { SFTPFile, SFTPSession } from './session/sftp' export { SFTPFile, SFTPSession } from './session/sftp'
export { SFTPPanelComponent, SSHTabComponent } export { SFTPPanelComponent }
export { PasswordStorageService } from './services/passwordStorage.service'

View File

@@ -20,7 +20,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
auth: null, auth: null,
password: null, password: null,
privateKeys: [], privateKeys: [],
keepaliveInterval: null, keepaliveInterval: 5000,
keepaliveCountMax: 10, keepaliveCountMax: 10,
readyTimeout: null, readyTimeout: null,
x11: false, x11: false,

View File

@@ -317,7 +317,7 @@ export class SSHSession {
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)), compression: this.profile.options.algorithms?.[SSHAlgorithmType.COMPRESSION]?.filter(x => supportedAlgorithms[SSHAlgorithmType.COMPRESSION].includes(x)),
}, },
keepaliveIntervalSeconds: this.profile.options.keepaliveInterval ? Math.round(this.profile.options.keepaliveInterval / 1000) : undefined, keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000),
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,
}, },

View File

@@ -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, HostAppService, Platform } from 'tabby-core' import { MenuItemOptions, PlatformService, TranslateService } 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,30 +16,19 @@ 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[]> {
const items: MenuItemOptions[] = [ return [
{ {
click: async () => { click: async () => {
await panel.openCreateDirectoryModal() await panel.openCreateDirectoryModal()
}, },
label: this.translate.instant('Create directory'), label: this.translate.instant('Create directory'),
}, },
] {
// 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 () => { click: async () => {
if ((await this.platform.showMessageBox({ if ((await this.platform.showMessageBox({
type: 'warning', type: 'warning',
@@ -56,9 +45,8 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
} }
}, },
label: this.translate.instant('Delete'), label: this.translate.instant('Delete'),
}) },
]
return items
} }
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> { async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",

View File

@@ -16,7 +16,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@xterm/addon-canvas": "^0.6.0", "@xterm/addon-canvas": "^0.6.0",

View File

@@ -17,8 +17,6 @@ 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
@@ -496,7 +494,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 && !data.equals(OSC_FOCUS_IN) && !data.equals(OSC_FOCUS_OUT)) { if (this.config.store.terminal.scrollOnInput) {
this.frontend?.scrollToBottom() this.frontend?.scrollToBottom()
} }
} }
@@ -544,7 +542,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
} }
if (!this.alternateScreenActive) { if (!this.alternateScreenActive) {
if ((data.includes('\r') || data.includes('\n')) && this.config.store.terminal.warnOnMultilinePaste) { if (data.includes('\r') && 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'),

View File

@@ -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"|translate', ngbTooltip='Search up',
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"|translate', ngbTooltip='Search down',
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"|translate', ngbTooltip='Case sensitivity',
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"|translate', ngbTooltip='Regular expression',
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"|translate', ngbTooltip='Whole word',
placement='bottom', placement='bottom',
[fastHtmlBind]='icons.wholeWord' [fastHtmlBind]='icons.wholeWord'
) )

View File

@@ -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 && this.activeSession) { if (this.isActive) {
this.outputToTerminal.next(Buffer.from(data)) this.outputToTerminal.next(Buffer.from(data))
} }
}, },
@@ -42,32 +42,25 @@ 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 {
if (this.isActive || this.activeSession) { const chunkSize = 1024
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
try { try {
this.sentry.consume(data) this.sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
} 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 {
try {
this.sentry.consume(data)
} catch (e) {
this.logger.error('zmodem detection error', e)
} }
if (!this.isActive) {
this.outputToTerminal.next(data) this.outputToTerminal.next(data)
} }
} }
@@ -80,7 +73,6 @@ class ZModemMiddleware extends SessionMiddleware {
this.activeSession = zsession this.activeSession = zsession
this.logger.info('new session', zsession) this.logger.info('new session', zsession)
try {
if (zsession.type === 'send') { if (zsession.type === 'send') {
const transfers = await this.platform.startUpload({ multiple: true }) const transfers = await this.platform.startUpload({ multiple: true })
let filesRemaining = transfers.length let filesRemaining = transfers.length
@@ -90,6 +82,7 @@ class ZModemMiddleware extends SessionMiddleware {
filesRemaining-- filesRemaining--
sizeRemaining -= transfer.getSize() sizeRemaining -= transfer.getSize()
} }
this.activeSession = null
await zsession.close() await zsession.close()
} else { } else {
zsession.on('offer', xfer => { zsession.on('offer', xfer => {
@@ -99,16 +92,6 @@ class ZModemMiddleware extends SessionMiddleware {
zsession.start() zsession.start()
await new Promise(resolve => zsession.on('session_end', resolve)) await new Promise(resolve => zsession.on('session_end', resolve))
}
this.showMessage(colors.bgBlue.black(' ZMODEM ') + ' Complete')
} catch (error) {
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
} }
} }

View File

@@ -11,7 +11,7 @@
"data", "data",
"dist" "dist"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/webpack-env": "^1.16.0" "@types/webpack-env": "^1.16.0"

View File

@@ -15,7 +15,7 @@
"dist", "dist",
"typings" "typings"
], ],
"author": "Tabby Developers", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@angular/core": "^15" "@angular/core": "^15"

View File

@@ -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, DirectoryDownload, HTMLFileUpload, DirectoryUpload } from 'tabby-core' import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, 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,10 +114,6 @@ 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 = () => {

View File

@@ -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.4: electron@^36.3:
version "36.7.1" version "36.3.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-36.7.1.tgz#73bbb460c60f529e00b9d3eff78fd135c42172ea" resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.1.tgz#12a8c1b1cd9163a4bd0cb60f89816243b26ab788"
integrity sha512-vkih7vbmWT6O8+VWFt3a9FMLUZn0O4piR20nTX0IL/d9tz9RjpzoMvHqpI2CE1Rxew9bCzrg7FpgtcTdY6dlyw== integrity sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==
dependencies: dependencies:
"@electron/get" "^2.0.0" "@electron/get" "^2.0.0"
"@types/node" "^22.7.7" "@types/node" "^22.7.7"