Compare commits

..

12 Commits

Author SHA1 Message Date
Eugene
4efcf1bbd6 Update build.yml 2025-01-22 23:07:24 +01:00
Eugene
abea964d94 Update build.yml 2025-01-22 22:59:28 +01:00
Eugene
2e7b66ac60 native arm64 build 2025-01-22 22:40:11 +01:00
Eugene
c4a514fc4a fixed agent login regression - fixes #10248, fixes #10174 2025-01-22 22:33:26 +01:00
Eugene
d525374061 fixed missing app theme at the initial vault password prompt 2025-01-22 22:16:17 +01:00
Eugene
6db08b765f lint 2025-01-16 22:30:36 +01:00
Eugene
d8d346c507 bump russh for agent fix 2025-01-16 22:27:11 +01:00
Eugene
6ffeb61c9c fixed dropping files into the terminal not inserting the path - fixes #10221, fixes #10206 2025-01-16 22:24:24 +01:00
Eugene
92c729dada bump russh for keyboard-interactive fixes and lock race fix 2025-01-16 22:14:29 +01:00
PytatoDuck
302f88058c Added Sponsors Logos in README.md (#10200) 2025-01-13 20:46:29 +01:00
fireblue
66c173b1b5 Keep the translucency effect even when the window loses focus on macOS. (#10196) 2025-01-13 19:06:09 +01:00
fireblue
f9dadf0816 Set the application's dark mode to follow the app settings on macOS. (#10186) 2025-01-10 22:07:17 +01:00
10 changed files with 123 additions and 48 deletions

View File

@@ -130,7 +130,7 @@ jobs:
path: artifact-zip path: artifact-zip
Linux-Build: Linux-Build:
runs-on: ubuntu-24.04 runs-on: ${{matrix.os}}
needs: Lint needs: Lint
strategy: strategy:
matrix: matrix:
@@ -138,14 +138,17 @@ jobs:
- build-arch: x64 - build-arch: x64
arch: amd64 arch: amd64
rust_triple: x86_64-unknown-linux-gnu rust_triple: x86_64-unknown-linux-gnu
os: ubuntu-24.04
- build-arch: arm64 - build-arch: arm64
arch: arm64 arch: arm64
rust_triple: aarch64-unknown-linux-gnu rust_triple: aarch64-unknown-linux-gnu
triplet: aarch64-linux-gnu- triplet: aarch64-linux-gnu-
os: ubuntu-24.04-arm
- build-arch: arm - build-arch: arm
arch: armhf arch: armhf
rust_triple: arm-unknown-linux-gnueabihf rust_triple: arm-unknown-linux-gnueabihf
triplet: arm-linux-gnueabihf- triplet: arm-linux-gnueabihf-
os: ubuntu-24.04
fail-fast: false fail-fast: false
env: env:
@@ -167,6 +170,10 @@ jobs:
with: with:
node-version: 22 node-version: 22
- name: Install FPM
run: |
sudo gem install fpm
- run: rustup target add ${{matrix.rust_triple}} - run: rustup target add ${{matrix.rust_triple}}
- name: Install dependencies - name: Install dependencies
@@ -176,12 +183,12 @@ jobs:
- name: Setup tar to run as root - name: Setup tar to run as root
run: sudo chmod u+s "$(command -v tar)" run: sudo chmod u+s "$(command -v tar)"
if: matrix.build-arch != 'x64' if: matrix.build-arch == 'arm'
- name: Download cached sysroot - name: Download cached sysroot
uses: actions/cache@v3 uses: actions/cache@v3
id: dl-cached-sysroot id: dl-cached-sysroot
if: matrix.build-arch !='x64' if: matrix.build-arch == 'arm'
with: with:
key: sysroot-${{matrix.build-arch}} key: sysroot-${{matrix.build-arch}}
path: /${{matrix.build-arch}}-sysroot path: /${{matrix.build-arch}}-sysroot
@@ -191,7 +198,7 @@ jobs:
sudo apt-get update -y && sudo apt-get install debootstrap qemu-user-static binfmt-support -y sudo apt-get update -y && sudo apt-get install debootstrap qemu-user-static binfmt-support -y
sudo qemu-debootstrap --include=libfontconfig1-dev,libsecret-1-dev,libnss3,libatk1.0-0,libatk-bridge2.0-0,libgdk-pixbuf2.0-0,libgtk-3-0,libgbm1 --variant=buildd --exclude=snapd --components=main,restricted,universe,multiverse --extractor=dpkg-deb --arch ${{matrix.arch}} bionic /${{matrix.build-arch}}-sysroot/ http://ports.ubuntu.com/ubuntu-ports/ sudo qemu-debootstrap --include=libfontconfig1-dev,libsecret-1-dev,libnss3,libatk1.0-0,libatk-bridge2.0-0,libgdk-pixbuf2.0-0,libgtk-3-0,libgbm1 --variant=buildd --exclude=snapd --components=main,restricted,universe,multiverse --extractor=dpkg-deb --arch ${{matrix.arch}} bionic /${{matrix.build-arch}}-sysroot/ http://ports.ubuntu.com/ubuntu-ports/
sudo find /${{matrix.build-arch}}-sysroot -type l -lname '/*' -exec sh -c 'file="$0"; dir=$(dirname "$file"); target=$(readlink "$0"); prefix=$(dirname "$dir" | sed 's@[^/]*@\.\.@g'); newtarget="$prefix$target"; ln -snf $newtarget $file' {} \; ; sudo find /${{matrix.build-arch}}-sysroot -type l -lname '/*' -exec sh -c 'file="$0"; dir=$(dirname "$file"); target=$(readlink "$0"); prefix=$(dirname "$dir" | sed 's@[^/]*@\.\.@g'); newtarget="$prefix$target"; ln -snf $newtarget $file' {} \; ;
if: matrix.build-arch != 'x64' && steps.dl-cached-sysroot.outputs.cache-hit != 'true' if: matrix.build-arch == 'arm' && steps.dl-cached-sysroot.outputs.cache-hit != 'true'
- name: Setup env to use ${{matrix.build-arch}} sysroot - name: Setup env to use ${{matrix.build-arch}} sysroot
run: | run: |
@@ -206,9 +213,9 @@ jobs:
elif [[ ${{matrix.arch}} == 'arm64' ]]; then elif [[ ${{matrix.arch}} == 'arm64' ]]; then
echo "PKG_CONFIG_PATH=/${{matrix.build-arch}}-sysroot/usr/lib/pkgconfig/:/${{matrix.build-arch}}-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig/" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=/${{matrix.build-arch}}-sysroot/usr/lib/pkgconfig/:/${{matrix.build-arch}}-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig/" >> $GITHUB_ENV
fi fi
if: matrix.build-arch != 'x64' if: matrix.build-arch == 'arm'
- name: Install npm_modules (amd64) - name: Install npm_modules (native)
run: | run: |
npm i -g yarn node-gyp npm i -g yarn node-gyp
yarn --network-timeout 1000000 --arch=${{matrix.build-arch}} --target-arch=${{matrix.build-arch}} yarn --network-timeout 1000000 --arch=${{matrix.build-arch}} --target-arch=${{matrix.build-arch}}
@@ -225,6 +232,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KEYGEN_TOKEN: ${{ secrets.KEYGEN_TOKEN }} KEYGEN_TOKEN: ${{ secrets.KEYGEN_TOKEN }}
USE_HARD_LINKS: false USE_HARD_LINKS: false
USE_SYSTEM_FPM: true
# DEBUG: electron-builder,electron-builder:* # DEBUG: electron-builder,electron-builder:*
- name: Build web resources (amd64 only) - name: Build web resources (amd64 only)

View File

@@ -144,7 +144,7 @@ Plugins and themes can be installed directly from the Settings view inside Tabby
# Sponsors <!-- omit in toc --> # Sponsors <!-- omit in toc -->
[![](https://assets-production.packagecloud.io/assets/packagecloud-logo-light-scaled-26ce8e96060fddf74afbd4445e63ba35590d4aaa56edc98495bb390ef3cae0ae.png)](https://packagecloud.io) <a href="https://packagecloud.io"><img src="https://assets-production.packagecloud.io/assets/logo_v1-d5895e7b89b2dee19030e85515fd0f91d8f3b37c82d218a6531fc89c2b1b613c.png" width="200"></a>
[**packagecloud**](https://packagecloud.io) has provided free Debian/RPM repository hosting [**packagecloud**](https://packagecloud.io) has provided free Debian/RPM repository hosting
@@ -152,7 +152,7 @@ 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 [**keygen**](https://keygen.sh/?via=eugene) has provided free release & auto-update hosting
<a href="https://iqhive.com/"><img src="https://private-user-images.githubusercontent.com/161476/361584584-ed292436-1d50-46bc-b479-78222c83ed22.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ3MDg3NjgsIm5iZiI6MTcyNDcwODQ2OCwicGF0aCI6Ii8xNjE0NzYvMzYxNTg0NTg0LWVkMjkyNDM2LTFkNTAtNDZiYy1iNDc5LTc4MjIyYzgzZWQyMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODI2JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyNlQyMTQxMDhaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1iYzNlZjIxN2JiYzBkYTU5NGE4YmZlZDJiNmIxZWE1ZTAyOTNhYjJlZTRhOGZjYTk4N2E4MzMzZjg0ZTNkZWQ0JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.pQzR2d71YV4TIxOH3Lg20HpNKrm_r2D-xfkEM_F2DTs" width="100"></a> <a href="https://iqhive.com/"><img src="https://iqhive.com/img/icons/logo.svg" width="200"></a>
[**IQ Hive**](https://iqhive.com) is providing financial support for the project development [**IQ Hive**](https://iqhive.com) is providing financial support for the project development

View File

@@ -1,7 +1,7 @@
import * as glasstron from 'glasstron' import * as glasstron from 'glasstron'
import { autoUpdater } from 'electron-updater' import { autoUpdater } from 'electron-updater'
import { Subject, Observable, debounceTime } from 'rxjs' import { Subject, Observable, debounceTime } from 'rxjs'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents } from 'electron' import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents, nativeTheme } from 'electron'
import ElectronConfig = require('electron-config') import ElectronConfig = require('electron-config')
import { enable as enableRemote } from '@electron/remote/main' import { enable as enableRemote } from '@electron/remote/main'
import * as os from 'os' import * as os from 'os'
@@ -100,6 +100,10 @@ export class Window {
} }
} }
if (process.platform === 'darwin') {
bwOptions.visualEffectState = 'active'
}
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
this.window = new BrowserWindow(bwOptions) as GlasstronWindow this.window = new BrowserWindow(bwOptions) as GlasstronWindow
} else { } else {
@@ -115,6 +119,8 @@ export class Window {
this.setVibrancy(true) this.setVibrancy(true)
} }
this.setDarkMode(this.configStore.appearance?.colorSchemeMode ?? 'dark')
if (!options.hidden) { if (!options.hidden) {
if (maximized) { if (maximized) {
this.window.maximize() this.window.maximize()
@@ -201,6 +207,18 @@ export class Window {
} }
} }
setDarkMode (mode: string): void {
if (process.platform === 'darwin') {
if ('light' === mode ) {
nativeTheme.themeSource = 'light'
} else if ('auto' === mode) {
nativeTheme.themeSource = 'system'
} else {
nativeTheme.themeSource = 'dark'
}
}
}
focus (): void { focus (): void {
this.window.focus() this.window.focus()
} }
@@ -373,6 +391,10 @@ export class Window {
this.setVibrancy(enabled, type) this.setVibrancy(enabled, type)
}) })
this.on('window-set-dark-mode', (_, mode) => {
this.setDarkMode(mode)
})
this.on('window-set-window-controls-color', (_, theme) => { this.on('window-set-window-controls-color', (_, theme) => {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const symbolColor: string = theme.foreground const symbolColor: string = theme.foreground

View File

@@ -30,7 +30,7 @@
"native-process-working-directory": "^1.0.2", "native-process-working-directory": "^1.0.2",
"npm": "6", "npm": "6",
"rxjs": "^7.5.7", "rxjs": "^7.5.7",
"russh": "0.1.11", "russh": "0.1.14",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"v8-compile-cache": "^2.3.0", "v8-compile-cache": "^2.3.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"

View File

@@ -3628,10 +3628,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies: dependencies:
aproba "^1.1.1" aproba "^1.1.1"
russh@0.1.11: russh@0.1.14:
version "0.1.11" version "0.1.14"
resolved "https://registry.yarnpkg.com/russh/-/russh-0.1.11.tgz#22e74f93ec1cb045930c85f8ba1daf2e5efcae4b" resolved "https://registry.yarnpkg.com/russh/-/russh-0.1.14.tgz#d97a6435795f97693414c55c93b823bbbbe34465"
integrity sha512-3CuI+rMoGpnnFDJxsEmcHYRSHInf3bz3fbgeyPnX8n1wgsX6wdbyI1DKL188oQlsrFWEjO3/7ebbYliCi0Qz7w== integrity sha512-x8n5P/QVm4yuRqRScxbjTt3RRLVLwUGC87OBXrZBOTEjPikGwyy/kcBGf2PjlV8iJ3M8JOro4b1xHj1JM8Khfg==
dependencies: dependencies:
"@napi-rs/cli" "^2.18.3" "@napi-rs/cli" "^2.18.3"

View File

@@ -23,6 +23,7 @@ export class ThemesService {
) { ) {
this.rootElementStyleBackup = document.documentElement.style.cssText this.rootElementStyleBackup = document.documentElement.style.cssText
this.applyTheme(standardTheme) this.applyTheme(standardTheme)
this.applyThemeVariables()
config.ready$.toPromise().then(() => { config.ready$.toPromise().then(() => {
this.applyCurrentTheme() this.applyCurrentTheme()
this.applyThemeVariables() this.applyThemeVariables()
@@ -37,6 +38,11 @@ export class ThemesService {
}) })
} }
private getConfigStoreOrDefaults (): any {
/// Theme service is active before the vault is unlocked and config is available
return this.config.store ?? this.config.getDefaults()
}
private applyThemeVariables () { private applyThemeVariables () {
if (!this.findCurrentTheme().followsColorScheme) { if (!this.findCurrentTheme().followsColorScheme) {
document.documentElement.style.cssText = this.rootElementStyleBackup document.documentElement.style.cssText = this.rootElementStyleBackup
@@ -60,7 +66,7 @@ export class ThemesService {
} }
let background = Color(theme.background) let background = Color(theme.background)
if (this.config.store?.appearance.vibrancy) { if (this.getConfigStoreOrDefaults().appearance.vibrancy) {
background = background.fade(0.6) background = background.fade(0.6)
} }
// const background = theme.background // const background = theme.background
@@ -148,13 +154,13 @@ export class ThemesService {
vars['--bs-form-switch-bg'] = `url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3e%3ccircle r=%273%27 fill=%27${switchBackground}%27/%3e%3c/svg%3e")` vars['--bs-form-switch-bg'] = `url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3e%3ccircle r=%273%27 fill=%27${switchBackground}%27/%3e%3c/svg%3e")`
} }
vars['--spaciness'] = this.config.store.appearance.spaciness vars['--spaciness'] = this.getConfigStoreOrDefaults().appearance.spaciness
for (const [bg, fg] of contrastPairs) { for (const [bg, fg] of contrastPairs) {
const colorBg = Color(vars[bg]).hsl() const colorBg = Color(vars[bg]).hsl()
const colorFg = Color(vars[fg]).hsl() const colorFg = Color(vars[fg]).hsl()
const bgContrast = colorBg.contrast(colorFg) const bgContrast = colorBg.contrast(colorFg)
if (bgContrast < this.config.store.terminal.minimumContrastRatio) { if (bgContrast < this.getConfigStoreOrDefaults().terminal.minimumContrastRatio) {
vars[fg] = this.ensureContrast(colorFg, colorBg).string() vars[fg] = this.ensureContrast(colorFg, colorBg).string()
} }
} }
@@ -163,7 +169,7 @@ export class ThemesService {
document.documentElement.style.setProperty(key, value) document.documentElement.style.setProperty(key, value)
} }
document.body.classList.toggle('no-animations', !this.config.store.accessibility.animations) document.body.classList.toggle('no-animations', !this.getConfigStoreOrDefaults().accessibility.animations)
} }
private ensureContrast (color: Color, against: Color): Color { private ensureContrast (color: Color, against: Color): Color {
@@ -178,7 +184,7 @@ export class ThemesService {
while ( while (
(step < 1 && color.color[2] > 1 || (step < 1 && color.color[2] > 1 ||
step > 1 && color.color[2] < 99) && step > 1 && color.color[2] < 99) &&
color.contrast(against) < this.config.store.terminal.minimumContrastRatio) { color.contrast(against) < this.getConfigStoreOrDefaults().terminal.minimumContrastRatio) {
color.color[2] *= step color.color[2] *= step
} }
return color return color
@@ -189,22 +195,22 @@ export class ThemesService {
} }
findCurrentTheme (): Theme { findCurrentTheme (): Theme {
return this.findTheme(this.config.store.appearance.theme) ?? this.standardTheme return this.findTheme(this.getConfigStoreOrDefaults().appearance.theme) ?? this.standardTheme
} }
/// @hidden /// @hidden
_getActiveColorScheme (): any { _getActiveColorScheme (): any {
let theme: PlatformTheme = 'dark' let theme: PlatformTheme = 'dark'
if (this.config.store.appearance.colorSchemeMode === 'light') { if (this.getConfigStoreOrDefaults().appearance.colorSchemeMode === 'light') {
theme = 'light' theme = 'light'
} else if (this.config.store.appearance.colorSchemeMode === 'auto') { } else if (this.getConfigStoreOrDefaults().appearance.colorSchemeMode === 'auto') {
theme = this.platform.getTheme() theme = this.platform.getTheme()
} }
if (theme === 'light') { if (theme === 'light') {
return this.config.store.terminal.lightColorScheme return this.getConfigStoreOrDefaults().terminal.lightColorScheme
} else { } else {
return this.config.store.terminal.colorScheme return this.getConfigStoreOrDefaults().terminal.colorScheme
} }
} }
@@ -215,7 +221,7 @@ export class ThemesService {
document.querySelector('head')!.appendChild(this.styleElement) document.querySelector('head')!.appendChild(this.styleElement)
} }
this.styleElement.textContent = theme.css this.styleElement.textContent = theme.css
document.querySelector('style#custom-css')!.innerHTML = this.config.store?.appearance?.css document.querySelector('style#custom-css')!.innerHTML = this.getConfigStoreOrDefaults().appearance.css
this.themeChanged.next(theme) this.themeChanged.next(theme)
} }

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core' import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
import { TerminalColorSchemeProvider } from 'tabby-terminal' import { TerminalColorSchemeProvider, TerminalDecorator } from 'tabby-terminal'
import { SFTPContextMenuItemProvider, SSHProfileImporter, AutoPrivateKeyLocator } from 'tabby-ssh' import { SFTPContextMenuItemProvider, SSHProfileImporter, AutoPrivateKeyLocator } from 'tabby-ssh'
import { PTYInterface, ShellProvider, UACService } from 'tabby-local' import { PTYInterface, ShellProvider, UACService } from 'tabby-local'
import { auditTime } from 'rxjs' import { auditTime } from 'rxjs'
@@ -23,6 +23,7 @@ import { ElectronConfigProvider } from './config'
import { EditSFTPContextMenu } from './sftpContextMenu' import { EditSFTPContextMenu } from './sftpContextMenu'
import { OpenSSHImporter, PrivateKeyLocator, StaticFileImporter } from './sshImporters' import { OpenSSHImporter, PrivateKeyLocator, StaticFileImporter } from './sshImporters'
import { ElectronPTYInterface } from './pty' import { ElectronPTYInterface } from './pty'
import { PathDropDecorator } from './pathDrop'
import { CmderShellProvider } from './shells/cmder' import { CmderShellProvider } from './shells/cmder'
import { Cygwin32ShellProvider } from './shells/cygwin32' import { Cygwin32ShellProvider } from './shells/cygwin32'
@@ -73,6 +74,8 @@ import { VSDevToolsProvider } from './shells/vs'
{ provide: PTYInterface, useClass: ElectronPTYInterface }, { provide: PTYInterface, useClass: ElectronPTYInterface },
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
// For WindowsDefaultShellProvider // For WindowsDefaultShellProvider
PowerShellCoreShellProvider, PowerShellCoreShellProvider,
WSLShellProvider, WSLShellProvider,
@@ -130,7 +133,10 @@ export default class ElectronModule {
}) })
}) })
config.changed$.subscribe(() => this.updateVibrancy()) config.changed$.subscribe(() => {
this.updateVibrancy()
this.updateDarkMode()
})
config.changed$.subscribe(() => this.updateWindowControlsColor()) config.changed$.subscribe(() => this.updateWindowControlsColor())
@@ -173,6 +179,11 @@ export default class ElectronModule {
this.hostWindow.setOpacity(this.config.store.appearance.opacity) this.hostWindow.setOpacity(this.config.store.appearance.opacity)
} }
private updateDarkMode () {
const colorSchemeMode = this.config.store.appearance.colorSchemeMode
this.electron.ipcRenderer.send('window-set-dark-mode', colorSchemeMode)
}
private updateWindowControlsColor () { private updateWindowControlsColor () {
// if windows and not using native frame, WCO does not exist, return. // if windows and not using native frame, WCO does not exist, return.
if (this.hostApp.platform === Platform.Windows && this.config.store.appearance.frame === 'native') { if (this.hostApp.platform === Platform.Windows && this.config.store.appearance.frame === 'native') {

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { TerminalDecorator } from '../api/decorator' import { TerminalDecorator, BaseTerminalTabComponent } from 'tabby-terminal'
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' import { webUtils } from 'electron'
/** @hidden */ /** @hidden */
@Injectable() @Injectable()
@@ -11,8 +11,8 @@ export class PathDropDecorator extends TerminalDecorator {
event.preventDefault() event.preventDefault()
})) }))
this.subscribeUntilDetached(terminal, terminal.frontend?.drop$.subscribe((event: DragEvent) => { this.subscribeUntilDetached(terminal, terminal.frontend?.drop$.subscribe((event: DragEvent) => {
for (const file of event.dataTransfer!.files as any) { for (const file of event.dataTransfer!.files as unknown as Iterable<File>) {
this.injectPath(terminal, file.path) this.injectPath(terminal, webUtils.getPathForFile(file))
} }
event.preventDefault() event.preventDefault()
})) }))

View File

@@ -50,6 +50,18 @@ type AuthMethod = {
kind: 'pageant', kind: 'pageant',
} }
function sshAuthTypeForMethod (m: AuthMethod): string {
switch (m.type) {
case 'none': return 'none'
case 'hostbased': return 'hostbased'
case 'prompt-password': return 'password'
case 'saved-password': return 'password'
case 'keyboard-interactive': return 'keyboard-interactive'
case 'publickey': return 'publickey'
case 'agent': return 'publickey'
}
}
export class KeyboardInteractivePrompt { export class KeyboardInteractivePrompt {
readonly responses: string[] = [] readonly responses: string[] = []
@@ -181,6 +193,13 @@ export class SSHSession {
}) })
} }
} }
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
}
this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
}
if (!this.profile.options.auth || this.profile.options.auth === 'password') { if (!this.profile.options.auth || this.profile.options.auth === 'password') {
if (this.profile.options.password) { if (this.profile.options.password) {
this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password }) this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
@@ -191,13 +210,6 @@ export class SSHSession {
} }
this.remainingAuthMethods.push({ type: 'prompt-password' }) this.remainingAuthMethods.push({ type: 'prompt-password' })
} }
if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
if (savedPassword) {
this.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
}
this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
}
this.remainingAuthMethods.push({ type: 'hostbased' }) this.remainingAuthMethods.push({ type: 'hostbased' })
} }
@@ -495,7 +507,7 @@ export class SSHSession {
this.keyboardInteractivePrompt.next(prompt) this.keyboardInteractivePrompt.next(prompt)
} }
async handleAuth (methodsLeft?: string[] | null): Promise<russh.AuthenticatedSSHClient|null> { async handleAuth (): Promise<russh.AuthenticatedSSHClient|null> {
this.activePrivateKey = null this.activePrivateKey = null
if (!(this.ssh instanceof russh.SSHClient)) { if (!(this.ssh instanceof russh.SSHClient)) {
@@ -506,22 +518,36 @@ export class SSHSession {
throw new Error('No username') throw new Error('No username')
} }
const noneResult = await this.ssh.authenticateNone(this.authUsername)
if (noneResult instanceof russh.AuthenticatedSSHClient) {
return noneResult
}
let methodsLeft = noneResult.remainingMethods
function maybeSetRemainingMethods (r: russh.AuthFailure) {
if (r.remainingMethods.length) {
methodsLeft = r.remainingMethods
}
}
while (true) { while (true) {
const method = this.remainingAuthMethods.shift() const m = methodsLeft
const method = this.remainingAuthMethods.find(x => m.length === 0 || m.includes(sshAuthTypeForMethod(x)))
if (!method) { if (!method) {
return null return null
} }
if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
// Agent can still be used even if not in methodsLeft this.remainingAuthMethods = this.remainingAuthMethods.filter(x => x !== method)
this.logger.info('Server does not support auth method', method.type)
continue
}
if (method.type === 'saved-password') { if (method.type === 'saved-password') {
this.emitServiceMessage(this.translate.instant('Using saved password')) this.emitServiceMessage(this.translate.instant('Using saved password'))
const result = await this.ssh.authenticateWithPassword(this.authUsername, method.password) const result = await this.ssh.authenticateWithPassword(this.authUsername, method.password)
if (result instanceof russh.AuthenticatedSSHClient) { if (result instanceof russh.AuthenticatedSSHClient) {
return result return result
} }
maybeSetRemainingMethods(result)
} }
if (method.type === 'prompt-password') { if (method.type === 'prompt-password') {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
@@ -539,6 +565,7 @@ export class SSHSession {
if (result instanceof russh.AuthenticatedSSHClient) { if (result instanceof russh.AuthenticatedSSHClient) {
return result return result
} }
maybeSetRemainingMethods(result)
} else { } else {
continue continue
} }
@@ -556,6 +583,7 @@ export class SSHSession {
if (result instanceof russh.AuthenticatedSSHClient) { if (result instanceof russh.AuthenticatedSSHClient) {
return result return result
} }
maybeSetRemainingMethods(result)
} }
} catch (e) { } catch (e) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`) this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
@@ -567,6 +595,7 @@ export class SSHSession {
while (true) { while (true) {
if (state.state === 'failure') { if (state.state === 'failure') {
maybeSetRemainingMethods(state)
break break
} }
@@ -602,7 +631,7 @@ export class SSHSession {
} }
} }
state = await this.ssh .continueKeyboardInteractiveAuthentication(responses) state = await this.ssh.continueKeyboardInteractiveAuthentication(responses)
if (state instanceof russh.AuthenticatedSSHClient) { if (state instanceof russh.AuthenticatedSSHClient) {
return state return state
@@ -615,6 +644,7 @@ export class SSHSession {
if (result instanceof russh.AuthenticatedSSHClient) { if (result instanceof russh.AuthenticatedSSHClient) {
return result return result
} }
maybeSetRemainingMethods(result)
} catch (e) { } catch (e) {
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to authenticate using agent: ${e}`) this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to authenticate using agent: ${e}`)
continue continue

View File

@@ -26,7 +26,6 @@ import { TerminalContextMenuItemProvider } from './api/contextMenuProvider'
import { TerminalColorSchemeProvider } from './api/colorSchemeProvider' import { TerminalColorSchemeProvider } from './api/colorSchemeProvider'
import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ColorSchemeSettingsTabProvider } from './settings' import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ColorSchemeSettingsTabProvider } from './settings'
import { DebugDecorator } from './features/debug' import { DebugDecorator } from './features/debug'
import { PathDropDecorator } from './features/pathDrop'
import { ZModemDecorator } from './features/zmodem' import { ZModemDecorator } from './features/zmodem'
import { TerminalConfigProvider } from './config' import { TerminalConfigProvider } from './config'
import { TerminalHotkeyProvider } from './hotkeys' import { TerminalHotkeyProvider } from './hotkeys'
@@ -54,7 +53,6 @@ import { DefaultColorSchemes } from './colorSchemes'
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true }, { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
{ provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true }, { provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
{ provide: TerminalDecorator, useClass: ZModemDecorator, multi: true }, { provide: TerminalDecorator, useClass: ZModemDecorator, multi: true },
{ provide: TerminalDecorator, useClass: DebugDecorator, multi: true }, { provide: TerminalDecorator, useClass: DebugDecorator, multi: true },