diff --git a/.all-contributorsrc b/.all-contributorsrc
index 021fcdad..63be7acf 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1319,6 +1319,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "marko1616",
+ "name": "marko1616",
+ "avatar_url": "https://avatars.githubusercontent.com/u/45327989?v=4",
+ "profile": "https://github.com/marko1616",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a5f94f25..3c9fbfd0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -243,13 +243,15 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
- name: Upload packages to packagecloud.io
- uses: Eugeny/packagecloud-action@main
+ uses: TykTechnologies/packagecloud-action@main
if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
env:
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
with:
repo: 'eugeny/tabby'
dir: 'dist'
+ rpmvers: 'el/9 el/8 ol/6 ol/7'
+ debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble debian/jessie debian/stretch debian/buster'
- uses: actions/upload-artifact@master
name: Upload AppImage (${{matrix.arch}})
diff --git a/README.de-DE.md b/README.de-DE.md
index cb222f0a..457fbed6 100644
--- a/README.de-DE.md
+++ b/README.de-DE.md
@@ -341,6 +341,7 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.es-ES.md b/README.es-ES.md
index 416cab43..45b0a7ba 100644
--- a/README.es-ES.md
+++ b/README.es-ES.md
@@ -343,6 +343,7 @@ Gracias a estas maravillosas personas ([emoji key](https://allcontributors.org/d
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.id-ID.md b/README.id-ID.md
index b33e1980..07604021 100644
--- a/README.id-ID.md
+++ b/README.id-ID.md
@@ -340,6 +340,7 @@ Terima kasih kepada mereka yang telah membantu ([emoji key](https://allcontribut
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.it-IT.md b/README.it-IT.md
index 065a5340..c527aaba 100644
--- a/README.it-IT.md
+++ b/README.it-IT.md
@@ -336,6 +336,7 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.ja-JP.md b/README.ja-JP.md
index 8c161d60..2d8330d4 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -351,6 +351,7 @@ WindowsδΈγ§γ―γ`Tabby.exe`γγγε ΄ζγ¨εγε ΄ζγ«`data`γγ©γ«
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.ko-KR.md b/README.ko-KR.md
index f56bae66..9a8ed25b 100644
--- a/README.ko-KR.md
+++ b/README.ko-KR.md
@@ -335,6 +335,7 @@ Pull requests and plugins are welcome!
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.md b/README.md
index 7a34d9ea..e714a33c 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,6 @@
> π Managing remote environments? Check out [Warpgate, my smart SSH/HTTP/MySQL bastion server](https://github.com/warp-tech/warpgate), it works great with Tabby, you'll love it.
-> π [Tabby-web](https://github.com/Eugeny/tabby-web) is looking for sponsors. As I can't afford to host it myself any longer, I'm looking for a sponsor to cover the hosting costs. If you're interested, please [get in touch](https://twitter.com/eugeeeeny)!
-
-
----
### Downloads:
@@ -155,6 +152,11 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
[**keygen**](https://keygen.sh/?via=eugene) has provided free release & auto-update hosting
+
+
+[**IQ Hive**](https://iqhive.com) is providing financial support for the project development
+
+
# Contributing
@@ -357,6 +359,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.pt-BR.md b/README.pt-BR.md
index e920e49d..703b0eaf 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -344,6 +344,7 @@ Obrigado vai para essas pessoas maravilhosas ([emoji key](https://allcontributor
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.ru-RU.md b/README.ru-RU.md
index bf71870d..a96a3505 100644
--- a/README.ru-RU.md
+++ b/README.ru-RU.md
@@ -336,6 +336,7 @@ Pull-Π·Π°ΠΏΡΠΎΡΡ ΠΈ ΠΏΠ»Π°Π³ΠΈΠ½Ρ ΠΏΡΠΈΠ²Π΅ΡΡΡΠ²ΡΡΡΡΡ!
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 162512ad..1e7b693f 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -335,6 +335,7 @@
 Charles Buffington π» |
 Yu Qin π» |
 fireblue π» |
+  marko1616 π» |
diff --git a/build/mac/entitlements.plist b/build/mac/entitlements.plist
index 350aa189..25c9ccd5 100644
--- a/build/mac/entitlements.plist
+++ b/build/mac/entitlements.plist
@@ -2,6 +2,8 @@
+ com.apple.security.automation.apple-events
+
com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
diff --git a/electron-builder.yml b/electron-builder.yml
index 8e6262d2..e83658b0 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -51,6 +51,7 @@ mac:
entitlements: "./build/mac/entitlements.plist"
entitlementsInherit: "./build/mac/entitlements.plist"
extendInfo:
+ ElectronTeamID: ${teamId}
NSRequiresAquaSystemAppearance: false
NSCameraUsageDescription: "A subprocess requests access to the device's camera."
NSMicrophoneUsageDescription: "A subprocess requests access to the device's microphone."
@@ -60,6 +61,7 @@ mac:
NSDownloadsFolderUsageDescription: "A subprocess requests access to the user's Downloads folder."
NSNetworkVolumesUsageDescription: 'A subprocess requests access to files on a network volume.'
NSRemovableVolumesUsageDescription: 'A subprocess requests access to files on a removable volume.'
+ NSAppleEventsUsageDescription: 'A subprocess requests permission to send AppleScript events to another application.'
linux:
category: "Utility;TerminalEmulator;System"
diff --git a/scripts/build-macos.mjs b/scripts/build-macos.mjs
index 1084621b..015f60d2 100755
--- a/scripts/build-macos.mjs
+++ b/scripts/build-macos.mjs
@@ -24,6 +24,7 @@ builder({
config: {
extraMetadata: {
version: vars.version,
+ teamId: process.env.APPLE_TEAM_ID,
},
mac: {
identity: !process.env.CI || process.env.CSC_LINK ? undefined : null,
diff --git a/tabby-core/src/api/index.ts b/tabby-core/src/api/index.ts
index cc467f96..b55f7e76 100644
--- a/tabby-core/src/api/index.ts
+++ b/tabby-core/src/api/index.ts
@@ -10,7 +10,7 @@ export { Theme } from './theme'
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
export { SelectorOption } from './selector'
export { CLIHandler, CLIEvent } from './cli'
-export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } 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'
diff --git a/tabby-core/src/api/platform.ts b/tabby-core/src/api/platform.ts
index df89abe3..2cb33810 100644
--- a/tabby-core/src/api/platform.ts
+++ b/tabby-core/src/api/platform.ts
@@ -88,6 +88,26 @@ export interface FileUploadOptions {
multiple: boolean
}
+export class DirectoryUpload {
+ private childrens: (FileUpload|DirectoryUpload)[] = []
+
+ constructor (private name = '') {
+ // Just set name for now.
+ }
+
+ getName (): string {
+ return this.name
+ }
+
+ getChildrens (): (FileUpload|DirectoryUpload)[] {
+ return this.childrens
+ }
+
+ pushChildren (item: FileUpload|DirectoryUpload): void {
+ this.childrens.push(item)
+ }
+}
+
export type PlatformTheme = 'light'|'dark'
export abstract class PlatformService {
@@ -108,23 +128,54 @@ export abstract class PlatformService {
abstract startDownload (name: string, mode: number, size: number): Promise
abstract startUpload (options?: FileUploadOptions): Promise
+ abstract startUploadDirectory (paths?: string[]): Promise
+
+ async startUploadFromDragEvent (event: DragEvent, multiple = false): Promise {
+ const result = new DirectoryUpload()
- startUploadFromDragEvent (event: DragEvent, multiple = false): FileUpload[] {
- const result: FileUpload[] = []
if (!event.dataTransfer) {
- return []
+ return Promise.resolve(result)
}
+
+ const traverseFileTree = (item: any, root: DirectoryUpload = result): Promise => {
+ return new Promise((resolve) => {
+ if (item.isFile) {
+ item.file((file: File) => {
+ const transfer = new HTMLFileUpload(file)
+ this.fileTransferStarted.next(transfer)
+ root.pushChildren(transfer)
+ resolve()
+ })
+ } else if (item.isDirectory) {
+ const dirReader = item.createReader()
+ const childrenFolder = new DirectoryUpload(item.name)
+ dirReader.readEntries(async (entries: any[]) => {
+ for (const entry of entries) {
+ await traverseFileTree(entry, childrenFolder)
+ }
+ resolve()
+ })
+ root.pushChildren(childrenFolder)
+ } else {
+ resolve()
+ }
+ })
+ }
+
+ const promises: Promise[] = []
+
+ const items = event.dataTransfer.items
// eslint-disable-next-line @typescript-eslint/prefer-for-of
- for (let i = 0; i < event.dataTransfer.files.length; i++) {
- const file = event.dataTransfer.files[i]
- const transfer = new HTMLFileUpload(file)
- this.fileTransferStarted.next(transfer)
- result.push(transfer)
- if (!multiple) {
- break
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i].webkitGetAsEntry()
+ if (item) {
+ promises.push(traverseFileTree(item))
+ if (!multiple) {
+ break
+ }
}
}
- return result
+ return Promise.all(promises).then(() => result)
}
getConfigPath (): string|null {
diff --git a/tabby-core/src/directives/dropZone.directive.ts b/tabby-core/src/directives/dropZone.directive.ts
index 1da823eb..64632fbc 100644
--- a/tabby-core/src/directives/dropZone.directive.ts
+++ b/tabby-core/src/directives/dropZone.directive.ts
@@ -1,5 +1,5 @@
import { Directive, Output, ElementRef, EventEmitter, AfterViewInit } from '@angular/core'
-import { FileUpload, PlatformService } from '../api/platform'
+import { DirectoryUpload, PlatformService } from '../api/platform'
import './dropZone.directive.scss'
/** @hidden */
@@ -7,7 +7,7 @@ import './dropZone.directive.scss'
selector: '[dropZone]',
})
export class DropZoneDirective implements AfterViewInit {
- @Output() transfer = new EventEmitter()
+ @Output() transfer = new EventEmitter()
private dropHint?: HTMLElement
constructor (
@@ -27,11 +27,9 @@ export class DropZoneDirective implements AfterViewInit {
})
}
})
- this.el.nativeElement.addEventListener('drop', (event: DragEvent) => {
+ this.el.nativeElement.addEventListener('drop', async (event: DragEvent) => {
this.removeHint()
- for (const transfer of this.platform.startUploadFromDragEvent(event, true)) {
- this.transfer.emit(transfer)
- }
+ this.transfer.emit(await this.platform.startUploadFromDragEvent(event, true))
})
this.el.nativeElement.addEventListener('dragleave', () => {
this.removeHint()
diff --git a/tabby-core/src/services/homeBase.service.ts b/tabby-core/src/services/homeBase.service.ts
index d64d5217..b508aa88 100644
--- a/tabby-core/src/services/homeBase.service.ts
+++ b/tabby-core/src/services/homeBase.service.ts
@@ -27,12 +27,8 @@ export class HomeBaseService {
this.platform.openExternal('https://github.com/Eugeny/tabby')
}
- openDiscussions (): void {
- this.platform.openExternal('https://github.com/Eugeny/tabby/discussions')
- }
-
- openTwitter (): void {
- this.platform.openExternal('https://twitter.com/eugeeeeny')
+ openDiscord (): void {
+ this.platform.openExternal('https://discord.gg/4c5EVTBhtp')
}
openTranslations (): void {
diff --git a/tabby-electron/src/services/platform.service.ts b/tabby-electron/src/services/platform.service.ts
index 9db5aa0b..d08f07ef 100644
--- a/tabby-electron/src/services/platform.service.ts
+++ b/tabby-electron/src/services/platform.service.ts
@@ -5,7 +5,7 @@ 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, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } 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'
@@ -48,6 +48,21 @@ export class ElectronPlatformService extends PlatformService {
})
}
+ async getAllFiles (dir: string, root: DirectoryUpload): Promise {
+ const items = await fs.readdir(dir, { withFileTypes: true })
+ for (const item of items) {
+ if (item.isDirectory()) {
+ root.pushChildren(await this.getAllFiles(path.join(dir, item.name), new DirectoryUpload(item.name)))
+ } else {
+ const file = new ElectronFileUpload(path.join(dir, item.name), this.electron)
+ root.pushChildren(file)
+ await wrapPromise(this.zone, file.open())
+ this.fileTransferStarted.next(file)
+ }
+ }
+ return root
+ }
+
readClipboard (): string {
return this.electron.clipboard.readText()
}
@@ -216,6 +231,28 @@ export class ElectronPlatformService extends PlatformService {
}))
}
+ async startUploadDirectory (paths?: string[]): Promise {
+ const properties: any[] = ['openFile', 'treatPackageAsDirectory', 'openDirectory']
+
+ if (!paths) {
+ const result = await this.electron.dialog.showOpenDialog(
+ this.hostWindow.getWindow(),
+ {
+ buttonLabel: this.translate.instant('Select'),
+ properties,
+ },
+ )
+ if (result.canceled) {
+ return new DirectoryUpload()
+ }
+ paths = result.filePaths
+ }
+
+ const root = new DirectoryUpload()
+ root.pushChildren(await this.getAllFiles(paths[0].split(path.sep).join(path.posix.sep), new DirectoryUpload(path.basename(paths[0]))))
+ return root
+ }
+
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise {
if (!filePath) {
const result = await this.electron.dialog.showSaveDialog(
diff --git a/tabby-settings/src/components/settingsTab.component.pug b/tabby-settings/src/components/settingsTab.component.pug
index e923d29f..1b2e8e95 100644
--- a/tabby-settings/src/components/settingsTab.component.pug
+++ b/tabby-settings/src/components/settingsTab.component.pug
@@ -40,11 +40,11 @@
div(translate) Report a problem
small.text-muted(translate) Generate a pre-filled GitHub issue
- button.list-group-item.list-group-item-action.link-card((click)='homeBase.openDiscussions()')
- i.fas.fa-fw.fa-comments
+ button.list-group-item.list-group-item-action.link-card((click)='homeBase.openDiscord()')
+ i.fab.fa-fw.fa-discord
div
- div(translate) Ask a question
- small.text-muted(translate) On GitHub Discussions
+ div(translate) Community
+ small.text-muted(translate) On Discord
button.list-group-item.list-group-item-action.link-card((click)='homeBase.openGitHub()')
i.fab.fa-fw.fa-github
@@ -58,12 +58,6 @@
div(translate) What's new
small.text-muted(translate) Show release notes
- button.list-group-item.list-group-item-action.link-card((click)='homeBase.openTwitter()')
- i.fab.fa-fw.fa-twitter
- div
- div(translate) Subscribe to updates
- small.text-muted(translate) Tabby news and updates on Twitter
-
h3(translate) Application settings
.form-line
diff --git a/tabby-ssh/src/components/sftpPanel.component.pug b/tabby-ssh/src/components/sftpPanel.component.pug
index 57e96941..67e4f202 100644
--- a/tabby-ssh/src/components/sftpPanel.component.pug
+++ b/tabby-ssh/src/components/sftpPanel.component.pug
@@ -23,11 +23,15 @@
button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='upload()')
i.fas.fa-upload.me-1
- div(translate) Upload
+ div(translate) Upload files
+
+ button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='uploadFolder()')
+ i.fas.fa-upload.me-1
+ div(translate) Upload folder
button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')}
-.body(dropZone, (transfer)='uploadOne($event)')
+.body(dropZone, (transfer)='uploadOneFolder($event)')
a.alert.alert-info.d-flex.align-items-center(
*ngIf='shouldShowCWDTip && !cwdDetectionAvailable',
(click)='platform.openExternal("https://tabby.sh/go/cwd-detection")'
diff --git a/tabby-ssh/src/components/sftpPanel.component.ts b/tabby-ssh/src/components/sftpPanel.component.ts
index e9662443..38625c0f 100644
--- a/tabby-ssh/src/components/sftpPanel.component.ts
+++ b/tabby-ssh/src/components/sftpPanel.component.ts
@@ -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, 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'
@@ -180,6 +180,30 @@ export class SFTPPanelComponent {
await Promise.all(transfers.map(t => this.uploadOne(t)))
}
+ async uploadFolder (): Promise {
+ const transfer = await this.platform.startUploadDirectory()
+ await this.uploadOneFolder(transfer)
+ }
+
+ async uploadOneFolder (transfer: DirectoryUpload, accumPath = ''): Promise {
+ const savedPath = this.path
+ for(const t of transfer.getChildrens()) {
+ if (t instanceof DirectoryUpload) {
+ try {
+ await this.sftp.mkdir(path.posix.join(this.path, accumPath, t.getName()))
+ } catch {
+ // Intentionally ignoring errors from making duplicate dirs.
+ }
+ await this.uploadOneFolder(t, path.posix.join(accumPath, t.getName()))
+ } else {
+ await this.sftp.upload(path.posix.join(this.path, accumPath, t.getName()), t)
+ }
+ }
+ if (this.path === savedPath) {
+ await this.navigate(this.path)
+ }
+ }
+
async uploadOne (transfer: FileUpload): Promise {
const savedPath = this.path
await this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
diff --git a/tabby-web/src/platform.ts b/tabby-web/src/platform.ts
index d64b50f4..54a7c6db 100644
--- a/tabby-web/src/platform.ts
+++ b/tabby-web/src/platform.ts
@@ -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, HTMLFileUpload } 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'
@@ -135,6 +135,10 @@ export class WebPlatformService extends PlatformService {
})
}
+ async startUploadDirectory (_paths?: string[]): Promise {
+ return new DirectoryUpload()
+ }
+
setErrorHandler (handler: (_: any) => void): void {
window.addEventListener('error', handler)
}