mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-26 18:16:04 +00:00
Compare commits
11 Commits
b31c2a5c11
...
all-contri
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4b632b1cee | ||
![]() |
0dec25c9e8 | ||
![]() |
fe53b71ef5 | ||
![]() |
2ed342563b | ||
![]() |
24575842d4 | ||
![]() |
f5ea98d9a8 | ||
![]() |
8db0b3bd05 | ||
![]() |
2bfac0528e | ||
![]() |
e692b598d2 | ||
![]() |
6b89b81509 | ||
![]() |
3b30dfa47c |
@@ -1355,6 +1355,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yyjdelete",
|
||||
"name": "SilverFox",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1819074?v=4",
|
||||
"profile": "https://github.com/yyjdelete",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@@ -60,7 +60,7 @@ tabby
|
||||
| ├─ src # Electron renderer code
|
||||
| └─ main.js # Electron main entry point
|
||||
├─ build
|
||||
├─ clink # Clink distribution, for Windows
|
||||
├─ clink # Clink distributive, for Windows
|
||||
├─ scripts # Maintenance scripts
|
||||
├─ tabby-community-color-schemes # Plugin that provides color schemes
|
||||
├─ tabby-core # Plugin that provides base UI and tab management
|
||||
|
@@ -349,6 +349,7 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -351,6 +351,7 @@ Gracias a estas maravillosas personas ([emoji key](https://allcontributors.org/d
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -347,6 +347,7 @@ Terima kasih kepada mereka yang telah membantu ([emoji key](https://allcontribut
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -343,6 +343,7 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -358,6 +358,7 @@ Windows上では、`Tabby.exe`がある場所と同じ場所に`data`フォル
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -107,7 +107,7 @@ This README is also available in: <a href="./README.md">:gb: English</a> · <a
|
||||
|
||||
플러그인과 테마는 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 컨테이너에 연결
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - 접두사, 접미사 및/또는 문자열 제거를 제공하여 터미널 탭의 제목을 수정
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 하나 또는 모든 터미널 탭에 신속한 명령 전송
|
||||
@@ -144,7 +144,7 @@ Pull requests and plugins are welcome!
|
||||
---
|
||||
<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 -->
|
||||
<!-- prettier-ignore-start -->
|
||||
@@ -342,6 +342,7 @@ Pull requests and plugins are welcome!
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -366,6 +366,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -351,6 +351,7 @@ Obrigado vai para essas pessoas maravilhosas ([emoji key](https://allcontributor
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -343,6 +343,7 @@ Pull-запросы и плагины приветствуются!
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -342,6 +342,7 @@
|
||||
</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/yyjdelete"><img src="https://avatars.githubusercontent.com/u/1819074?v=4?s=100" width="100px;" alt="SilverFox"/><br /><sub><b>SilverFox</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=yyjdelete" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -40,7 +40,7 @@
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "^6.7.3",
|
||||
"deep-equal": "2.0.5",
|
||||
"electron": "^36.4",
|
||||
"electron": "^36.3",
|
||||
"electron-builder": "^26.0",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-installer-snap": "^5.1.0",
|
||||
|
@@ -10,7 +10,7 @@ export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
export { SelectorOption } from './selector'
|
||||
export { CLIHandler, CLIEvent } from './cli'
|
||||
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, 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 { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||
export { HostWindowService } from './hostWindow'
|
||||
|
@@ -22,6 +22,7 @@ export interface MessageBoxResult {
|
||||
|
||||
export abstract class FileTransfer {
|
||||
abstract getName (): string
|
||||
abstract getMode (): number
|
||||
abstract getSize (): number
|
||||
abstract close (): void
|
||||
|
||||
@@ -33,16 +34,8 @@ export abstract class FileTransfer {
|
||||
return this.completedBytes
|
||||
}
|
||||
|
||||
getStatus (): string {
|
||||
return this.status
|
||||
}
|
||||
|
||||
getTotalSize (): number {
|
||||
return this.totalSize
|
||||
}
|
||||
|
||||
isComplete (): boolean {
|
||||
return this.completed
|
||||
return this.completedBytes >= this.getSize()
|
||||
}
|
||||
|
||||
isCancelled (): boolean {
|
||||
@@ -54,18 +47,6 @@ export abstract class FileTransfer {
|
||||
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 {
|
||||
if (!bytes) {
|
||||
return
|
||||
@@ -76,26 +57,16 @@ export abstract class FileTransfer {
|
||||
}
|
||||
|
||||
private completedBytes = 0
|
||||
private totalSize = 0
|
||||
private lastChunkStartTime = Date.now()
|
||||
private lastChunkSpeed = 0
|
||||
private cancelled = false
|
||||
private completed = false
|
||||
private status = ''
|
||||
}
|
||||
|
||||
export abstract class FileDownload extends FileTransfer {
|
||||
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 {
|
||||
abstract getMode (): number
|
||||
|
||||
abstract read (): Promise<Uint8Array>
|
||||
|
||||
async readAll (): Promise<Uint8Array> {
|
||||
@@ -156,7 +127,6 @@ export abstract class PlatformService {
|
||||
abstract saveConfig (content: string): Promise<void>
|
||||
|
||||
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 startUploadDirectory (paths?: string[]): Promise<DirectoryUpload>
|
||||
|
||||
@@ -267,7 +237,7 @@ export abstract class PlatformService {
|
||||
abstract setErrorHandler (handler: (_: any) => void): void
|
||||
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
|
||||
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
|
||||
abstract pickDirectory (): Promise<string | null>
|
||||
abstract pickDirectory (): Promise<string>
|
||||
abstract quit (): void
|
||||
}
|
||||
|
||||
|
@@ -5,9 +5,7 @@
|
||||
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
||||
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
||||
.main
|
||||
label.no-wrap([ngbTooltip]='transfer.getName()')
|
||||
| {{transfer.getName()}}
|
||||
span.ms-2.text-muted(*ngIf='transfer.getStatus()') ({{transfer.getStatus()}})
|
||||
label.no-wrap([ngbTooltip]='transfer.getName()') {{transfer.getName()}}
|
||||
ngb-progressbar([type]='transfer.isComplete() ? "success" : transfer.isCancelled() ? "danger" : "info"', [value]='getProgress(transfer)')
|
||||
.metadata
|
||||
.size {{transfer.getSize()|filesize}}
|
||||
|
@@ -5,11 +5,12 @@ import * as os from 'os'
|
||||
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
||||
import { execFile } from 'mz/child_process'
|
||||
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 { ElectronHostWindow } from './hostWindow.service'
|
||||
import { ShellIntegrationService } from './shellIntegration.service'
|
||||
import { ElectronHostAppService } from './hostApp.service'
|
||||
import { PlatformTheme } from '../../../tabby-core/src/api/platform'
|
||||
import { configPath } from '../../../app/lib/config'
|
||||
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
||||
|
||||
@@ -271,48 +272,19 @@ export class ElectronPlatformService extends PlatformService {
|
||||
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 {
|
||||
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
|
||||
handler(err)
|
||||
})
|
||||
}
|
||||
|
||||
async pickDirectory (title?: string, buttonLabel?: string): Promise<string | null> {
|
||||
const result = await this.electron.dialog.showOpenDialog(
|
||||
async pickDirectory (): Promise<string> {
|
||||
return (await this.electron.dialog.showOpenDialog(
|
||||
this.hostWindow.getWindow(),
|
||||
{
|
||||
title,
|
||||
buttonLabel,
|
||||
properties: ['openDirectory', 'showHiddenFiles'],
|
||||
},
|
||||
)
|
||||
if (result.canceled || !result.filePaths.length) {
|
||||
return null
|
||||
}
|
||||
return result.filePaths[0]
|
||||
)).filePaths[0]
|
||||
}
|
||||
|
||||
getTheme (): PlatformTheme {
|
||||
@@ -341,7 +313,6 @@ class ElectronFileUpload extends FileUpload {
|
||||
const stat = await fs.stat(this.filePath)
|
||||
this.size = stat.size
|
||||
this.mode = stat.mode
|
||||
this.setTotalSize(this.size)
|
||||
this.file = await fs.open(this.filePath, 'r')
|
||||
}
|
||||
|
||||
@@ -360,9 +331,6 @@ class ElectronFileUpload extends FileUpload {
|
||||
async read (): Promise<Uint8Array> {
|
||||
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
|
||||
this.increaseProgress(result.bytesRead)
|
||||
if (this.getCompletedBytes() >= this.getSize()) {
|
||||
this.setCompleted(true)
|
||||
}
|
||||
return this.buffer.slice(0, result.bytesRead)
|
||||
}
|
||||
|
||||
@@ -384,7 +352,6 @@ class ElectronFileDownload extends FileDownload {
|
||||
) {
|
||||
super()
|
||||
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||
this.setTotalSize(size)
|
||||
}
|
||||
|
||||
async open (): Promise<void> {
|
||||
@@ -395,6 +362,10 @@ class ElectronFileDownload extends FileDownload {
|
||||
return path.basename(this.filePath)
|
||||
}
|
||||
|
||||
getMode (): number {
|
||||
return this.mode
|
||||
}
|
||||
|
||||
getSize (): number {
|
||||
return this.size
|
||||
}
|
||||
@@ -406,9 +377,6 @@ class ElectronFileDownload extends FileDownload {
|
||||
this.increaseProgress(result.bytesWritten)
|
||||
pos += result.bytesWritten
|
||||
}
|
||||
if (this.getCompletedBytes() >= this.getSize()) {
|
||||
this.setCompleted(true)
|
||||
}
|
||||
}
|
||||
|
||||
close (): void {
|
||||
@@ -416,49 +384,3 @@ class ElectronFileDownload extends FileDownload {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -28,10 +28,6 @@ export class LocalProfileSettingsComponent implements ProfileSettingsComponent<L
|
||||
// return
|
||||
// }
|
||||
|
||||
const cwd = await this.platform.pickDirectory()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
this.profile.options.cwd = cwd
|
||||
this.profile.options.cwd = await this.platform.pickDirectory()
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as C from 'constants'
|
||||
import { posix as path } from 'path'
|
||||
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 { SSHSession } from '../session/ssh'
|
||||
import { SFTPContextMenuItemProvider } from '../api'
|
||||
@@ -220,68 +220,6 @@ export class SFTPPanelComponent {
|
||||
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 {
|
||||
const s = 'SGdrwxrwxrwx'
|
||||
const e = ' ---------'
|
||||
|
@@ -20,7 +20,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
||||
auth: null,
|
||||
password: null,
|
||||
privateKeys: [],
|
||||
keepaliveInterval: null,
|
||||
keepaliveInterval: 5000,
|
||||
keepaliveCountMax: 10,
|
||||
readyTimeout: null,
|
||||
x11: false,
|
||||
|
@@ -317,7 +317,7 @@ export class SSHSession {
|
||||
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: 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,
|
||||
connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined,
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
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 { SFTPContextMenuItemProvider } from './api'
|
||||
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||
@@ -16,49 +16,37 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||
private platform: PlatformService,
|
||||
private ngbModal: NgbModal,
|
||||
private translate: TranslateService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||
const items: MenuItemOptions[] = [
|
||||
return [
|
||||
{
|
||||
click: async () => {
|
||||
await panel.openCreateDirectoryModal()
|
||||
},
|
||||
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 () => {
|
||||
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)
|
||||
}
|
||||
{
|
||||
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'),
|
||||
},
|
||||
label: this.translate.instant('Delete'),
|
||||
})
|
||||
|
||||
return items
|
||||
]
|
||||
}
|
||||
|
||||
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
|
||||
|
@@ -27,7 +27,7 @@ class ZModemMiddleware extends SessionMiddleware {
|
||||
this.logger = log.create('zmodem')
|
||||
this.sentry = new ZModem.Sentry({
|
||||
to_terminal: data => {
|
||||
if (this.isActive && this.activeSession) {
|
||||
if (this.isActive) {
|
||||
this.outputToTerminal.next(Buffer.from(data))
|
||||
}
|
||||
},
|
||||
@@ -42,32 +42,25 @@ class ZModemMiddleware extends SessionMiddleware {
|
||||
},
|
||||
on_retract: () => {
|
||||
this.showMessage('transfer cancelled')
|
||||
this.activeSession = null
|
||||
this.isActive = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
feedFromSession (data: Buffer): void {
|
||||
if (this.isActive || this.activeSession) {
|
||||
const chunkSize = 1024
|
||||
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
|
||||
try {
|
||||
this.sentry.consume(data)
|
||||
this.sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
|
||||
} catch (e) {
|
||||
this.showMessage(colors.bgRed.black(' Error ') + ' ' + e)
|
||||
this.logger.error('protocol error', e)
|
||||
this.activeSession?.abort()
|
||||
this.activeSession.abort()
|
||||
this.activeSession = null
|
||||
this.isActive = false
|
||||
// Don't forward the problematic data to terminal
|
||||
return
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.sentry.consume(data)
|
||||
} catch (e) {
|
||||
this.logger.error('zmodem detection error', e)
|
||||
}
|
||||
|
||||
}
|
||||
if (!this.isActive) {
|
||||
this.outputToTerminal.next(data)
|
||||
}
|
||||
}
|
||||
@@ -80,35 +73,25 @@ class ZModemMiddleware extends SessionMiddleware {
|
||||
this.activeSession = zsession
|
||||
this.logger.info('new session', zsession)
|
||||
|
||||
try {
|
||||
if (zsession.type === 'send') {
|
||||
const transfers = await this.platform.startUpload({ multiple: true })
|
||||
let filesRemaining = transfers.length
|
||||
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
||||
for (const transfer of transfers) {
|
||||
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
|
||||
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))
|
||||
if (zsession.type === 'send') {
|
||||
const transfers = await this.platform.startUpload({ multiple: true })
|
||||
let filesRemaining = transfers.length
|
||||
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
|
||||
for (const transfer of transfers) {
|
||||
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
|
||||
filesRemaining--
|
||||
sizeRemaining -= transfer.getSize()
|
||||
}
|
||||
this.activeSession = null
|
||||
await zsession.close()
|
||||
} else {
|
||||
zsession.on('offer', xfer => {
|
||||
this.receiveFile(xfer, zsession)
|
||||
})
|
||||
|
||||
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 {
|
||||
zsession.start()
|
||||
|
||||
await new Promise(resolve => zsession.on('session_end', resolve))
|
||||
this.activeSession = null
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import '@vaadin/vaadin-context-menu'
|
||||
import copyToClipboard from 'copy-text-to-clipboard'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
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
|
||||
import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu'
|
||||
@@ -114,10 +114,6 @@ export class WebPlatformService extends PlatformService {
|
||||
return transfer
|
||||
}
|
||||
|
||||
async startDownloadDirectory (_name: string, _estimatedSize?: number): Promise<DirectoryDownload|null> {
|
||||
throw new Error('Unsupported')
|
||||
}
|
||||
|
||||
startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
||||
return new Promise(resolve => {
|
||||
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"
|
||||
integrity sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==
|
||||
|
||||
electron@^36.4:
|
||||
version "36.7.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.7.1.tgz#73bbb460c60f529e00b9d3eff78fd135c42172ea"
|
||||
integrity sha512-vkih7vbmWT6O8+VWFt3a9FMLUZn0O4piR20nTX0IL/d9tz9RjpzoMvHqpI2CE1Rxew9bCzrg7FpgtcTdY6dlyw==
|
||||
electron@^36.3:
|
||||
version "36.3.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.1.tgz#12a8c1b1cd9163a4bd0cb60f89816243b26ab788"
|
||||
integrity sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^22.7.7"
|
||||
|
Reference in New Issue
Block a user