Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
b1c4063270 build(deps): bump v8-compile-cache from 2.3.0 to 2.4.0 in /app
Bumps [v8-compile-cache](https://github.com/zertosh/v8-compile-cache) from 2.3.0 to 2.4.0.
- [Changelog](https://github.com/zertosh/v8-compile-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zertosh/v8-compile-cache/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: v8-compile-cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-15 04:07:25 +00:00
70 changed files with 740 additions and 1628 deletions

View File

@@ -1,13 +1,7 @@
settings: settings:
import/parsers:
'@typescript-eslint/parser': ['.ts']
import/resolver: import/resolver:
typescript: typescript: true
project:
- tsconfig.json
- tabby-*/tsconfig.json
node: true node: true
env: env:
browser: true browser: true
es6: true es6: true
@@ -34,7 +28,7 @@ overrides:
- plugin:import/typescript - plugin:import/typescript
plugins: plugins:
- '@typescript-eslint' - '@typescript-eslint'
- import - 'import'
rules: rules:
'@typescript-eslint/semi': '@typescript-eslint/semi':
- error - error
@@ -136,7 +130,6 @@ overrides:
'@typescript-eslint/naming-convention': off '@typescript-eslint/naming-convention': off
'@typescript-eslint/lines-between-class-members': '@typescript-eslint/lines-between-class-members':
- error - error
- always
- exceptAfterSingleLine: true - exceptAfterSingleLine: true
'@typescript-eslint/dot-notation': off '@typescript-eslint/dot-notation': off
'@typescript-eslint/no-implicit-any-catch': off '@typescript-eslint/no-implicit-any-catch': off
@@ -159,6 +152,3 @@ overrides:
'@typescript-eslint/consistent-generic-constructors': off '@typescript-eslint/consistent-generic-constructors': off
'keyword-spacing': off 'keyword-spacing': off
'@typescript-eslint/keyword-spacing': off '@typescript-eslint/keyword-spacing': off
'@typescript-eslint/class-methods-use-this': off
'@typescript-eslint/lines-around-comment': off
'@typescript-eslint/no-redundant-type-constituents': off # broken

View File

@@ -183,7 +183,7 @@ export class Application {
} }
enableTray (): void { enableTray (): void {
if (!!this.tray || process.platform === 'linux') { if (this.tray || process.platform === 'linux') {
return return
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {

View File

@@ -31,7 +31,7 @@
"npm": "6", "npm": "6",
"rxjs": "^7.5.7", "rxjs": "^7.5.7",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"v8-compile-cache": "^2.3.0", "v8-compile-cache": "^2.4.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@@ -3880,10 +3880,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
v8-compile-cache@^2.3.0: v8-compile-cache@^2.4.0:
version "2.3.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==
validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
version "3.0.4" version "3.0.4"

View File

@@ -26,8 +26,8 @@
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/webpack-env": "^1.18.0", "@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^5.54.1",
"apply-loader": "2.0.0", "apply-loader": "2.0.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
@@ -44,9 +44,9 @@
"electron-download": "^4.1.1", "electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0", "electron-installer-snap": "^5.1.0",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"eslint": "^8.48.0", "eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.27.5",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"gettext-extractor": "^3.8.0", "gettext-extractor": "^3.8.0",
"graceful-fs": "^4.2.10", "graceful-fs": "^4.2.10",

View File

@@ -16,7 +16,7 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow' export { HostWindowService } from './hostWindow'
export { HostAppService, Platform } from './hostApp' export { HostAppService, Platform } from './hostApp'
export { FileProvider } from './fileProvider' export { FileProvider } from './fileProvider'
export { ProfileProvider, ConnectableProfileProvider, QuickConnectProfileProvider, Profile, ConnectableProfile, PartialProfile, ProfileSettingsComponent, ProfileGroup, PartialProfileGroup } from './profileProvider' export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
export { PromptModalComponent } from '../components/promptModal.component' export { PromptModalComponent } from '../components/promptModal.component'
export * from './commands' export * from './commands'

View File

@@ -1,5 +1,5 @@
export interface MenuItemOptions { export interface MenuItemOptions {
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio' type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio')
label?: string label?: string
sublabel?: string sublabel?: string
enabled?: boolean enabled?: boolean

View File

@@ -21,10 +21,6 @@ export interface Profile {
isTemplate: boolean isTemplate: boolean
} }
export interface ConnectableProfile extends Profile {
clearServiceMessagesOnConnect: boolean
}
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
[K in keyof T]?: T[K] [K in keyof T]?: T[K]
}, 'options'>, 'type'>, 'name'> & { }, 'options'>, 'type'>, 'name'> & {
@@ -35,21 +31,6 @@ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
} }
} }
export interface ProfileGroup {
id: string
name: string
profiles: PartialProfile<Profile>[]
defaults: any
editable: boolean
}
export type PartialProfileGroup<T extends ProfileGroup> = Omit<Omit<{
[K in keyof T]?: T[K]
}, 'id'>, 'name'> & {
id: string
name: string
}
export interface ProfileSettingsComponent<P extends Profile> { export interface ProfileSettingsComponent<P extends Profile> {
profile: P profile: P
save?: () => void save?: () => void
@@ -58,6 +39,7 @@ export interface ProfileSettingsComponent<P extends Profile> {
export abstract class ProfileProvider<P extends Profile> { export abstract class ProfileProvider<P extends Profile> {
id: string id: string
name: string name: string
supportsQuickConnect = false
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P> settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
configDefaults = {} configDefaults = {}
@@ -71,15 +53,13 @@ export abstract class ProfileProvider<P extends Profile> {
abstract getDescription (profile: PartialProfile<P>): string abstract getDescription (profile: PartialProfile<P>): string
quickConnect (query: string): PartialProfile<P>|null {
return null
}
intoQuickConnectString (profile: P): string|null {
return null
}
deleteProfile (profile: P): void { } deleteProfile (profile: P): void { }
} }
export abstract class ConnectableProfileProvider<P extends ConnectableProfile> extends ProfileProvider<P> {}
export abstract class QuickConnectProfileProvider<P extends ConnectableProfile> extends ConnectableProfileProvider<P> {
abstract quickConnect (query: string): PartialProfile<P>|null
abstract intoQuickConnectString (profile: P): string|null
}

View File

@@ -18,7 +18,7 @@ export class CoreCommandProvider extends CommandProvider {
} }
async activate () { async activate () {
const profile = await this.profilesService.showProfileSelector().catch(() => null) const profile = await this.profilesService.showProfileSelector()
if (profile) { if (profile) {
this.profilesService.launchProfile(profile) this.profilesService.launchProfile(profile)
} }

View File

@@ -35,7 +35,8 @@ title-bar(
[@animateTab]='{value: "in", params: {size: targetTabSize}}', [@animateTab]='{value: "in", params: {size: targetTabSize}}',
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations', [@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
(click)='app.selectTab(tab)', (click)='app.selectTab(tab)',
[class.fully-draggable]='hostApp.platform != Platform.macOS' [class.fully-draggable]='hostApp.platform != Platform.macOS',
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
) )
.btn-group.background .btn-group.background
@@ -64,7 +65,7 @@ title-bar(
(transfersChange)='onTransfersChange()' (transfersChange)='onTransfersChange()'
) )
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin"') .drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
.btn-group.background .btn-group.background
.d-flex( .d-flex(

View File

@@ -75,7 +75,6 @@ export abstract class BaseTabComponent extends BaseComponent {
private titleChange = new Subject<string>() private titleChange = new Subject<string>()
private focused = new Subject<void>() private focused = new Subject<void>()
private blurred = new Subject<void>() private blurred = new Subject<void>()
private visibility = new Subject<boolean>()
private progress = new Subject<number|null>() private progress = new Subject<number|null>()
private activity = new Subject<boolean>() private activity = new Subject<boolean>()
private destroyed = new Subject<void>() private destroyed = new Subject<void>()
@@ -84,8 +83,6 @@ export abstract class BaseTabComponent extends BaseComponent {
get focused$ (): Observable<void> { return this.focused } get focused$ (): Observable<void> { return this.focused }
get blurred$ (): Observable<void> { return this.blurred } get blurred$ (): Observable<void> { return this.blurred }
/* @hidden */
get visibility$ (): Observable<boolean> { return this.visibility }
get titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) } get titleChange$ (): Observable<string> { return this.titleChange.pipe(distinctUntilChanged()) }
get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) } get progress$ (): Observable<number|null> { return this.progress.pipe(distinctUntilChanged()) }
get activity$ (): Observable<boolean> { return this.activity } get activity$ (): Observable<boolean> { return this.activity }
@@ -180,11 +177,6 @@ export abstract class BaseTabComponent extends BaseComponent {
this.blurred.next() this.blurred.next()
} }
/* @hidden */
emitVisibility (visibility: boolean): void {
this.visibility.next(visibility)
}
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> { insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any> this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
this.viewContainer = container this.viewContainer = container

View File

@@ -29,14 +29,11 @@ export class SelectorModalComponent<T> {
} }
@HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void { @HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void {
if (event.key === 'Escape') {
this.close()
} else if (this.filteredOptions.length > 0) {
if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) { if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) {
this.selectedIndex -= Math.min(10, Math.max(1, this.selectedIndex)) this.selectedIndex -= 10
event.preventDefault() event.preventDefault()
} else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) { } else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) {
this.selectedIndex += Math.min(10, Math.max(1, this.filteredOptions.length - this.selectedIndex - 1)) this.selectedIndex += 10
event.preventDefault() event.preventDefault()
} else if (event.key === 'ArrowUp') { } else if (event.key === 'ArrowUp') {
this.selectedIndex-- this.selectedIndex--
@@ -46,20 +43,21 @@ export class SelectorModalComponent<T> {
event.preventDefault() event.preventDefault()
} else if (event.key === 'Enter') { } else if (event.key === 'Enter') {
this.selectOption(this.filteredOptions[this.selectedIndex]) this.selectOption(this.filteredOptions[this.selectedIndex])
} else if (event.key === 'Backspace' && this.canEditSelected()) { } else if (event.key === 'Escape') {
this.close()
}
if (event.key === 'Backspace' && this.canEditSelected()) {
event.preventDefault() event.preventDefault()
this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent! this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
this.onFilterChange() this.onFilterChange()
} }
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({ Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest', block: 'nearest',
}) })
} }
}
onFilterChange (): void { onFilterChange (): void {
const f = this.filter.trim().toLowerCase() const f = this.filter.trim().toLowerCase()
@@ -78,7 +76,7 @@ export class SelectorModalComponent<T> {
{ sort: true }, { sort: true },
).search(f) ).search(f)
this.options.filter(x => x.freeInputPattern).sort(firstBy<SelectorOption<T>, number>(x => x.weight ?? 0)).forEach(freeOption => { this.options.filter(x => x.freeInputPattern).forEach(freeOption => {
if (!this.filteredOptions.includes(freeOption)) { if (!this.filteredOptions.includes(freeOption)) {
this.filteredOptions.push(freeOption) this.filteredOptions.push(freeOption)
} }

View File

@@ -275,7 +275,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
}) })
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred())) this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.visibility$.subscribe(visibility => this.getAllTabs().forEach(x => x.emitVisibility(visibility)))
this.tabAdded$.subscribe(() => this.updateTitle()) this.tabAdded$.subscribe(() => this.updateTitle())
this.tabRemoved$.subscribe(() => this.updateTitle()) this.tabRemoved$.subscribe(() => this.updateTitle())

View File

@@ -1,10 +1,10 @@
.container.mt-3.mb-3 .container.mt-5.mb-5
.mb-3 .mb-4
.tabby-logo .tabby-logo
h1.tabby-title Tabby h1.tabby-title Tabby
sup α sup α
.text-center.mb-3(translate) Thank you for downloading Tabby! .text-center.mb-5(translate) Thank you for downloading Tabby!
.form-line .form-line
.header .header
@@ -16,54 +16,13 @@
*ngFor='let lang of allLanguages' *ngFor='let lang of allLanguages'
) {{lang.name}} ) {{lang.name}}
.form-line
.header
.title(translate) Switch color scheme
.btn-group(role='group')
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeAuto',
[value]='"auto"'
)
label.btn.btn-secondary(
for='colorSchemeModeAuto'
)
span(translate) From system
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeDark',
[value]='"dark"'
)
label.btn.btn-secondary(
for='colorSchemeModeDark'
)
span(translate) Always dark
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeLight',
[value]='"light"'
)
label.btn.btn-secondary(
for='colorSchemeModeLight'
)
span(translate) Always light
.form-line .form-line
.header .header
.title(translate) Enable analytics .title(translate) Enable analytics
.description(translate) Help track the number of Tabby installs across the world! .description(translate) Help track the number of Tabby installs across the world!
toggle([(ngModel)]='config.store.enableAnalytics') toggle([(ngModel)]='config.store.enableAnalytics')
.form-line .form-line
.header .header
.title(translate) Enable global hotkey (Ctrl-Space) .title(translate) Enable global hotkey (Ctrl-Space)

View File

@@ -6,8 +6,3 @@
max-height: 100%; max-height: 100%;
overflow-y: auto; overflow-y: auto;
} }
.tabby-logo {
width: 60px;
height: 60px;
}

View File

@@ -9,6 +9,5 @@ export class CoreConfigProvider extends ConfigProvider {
[Platform.Linux]: require('./configDefaults.linux.yaml').default, [Platform.Linux]: require('./configDefaults.linux.yaml').default,
[Platform.Web]: require('./configDefaults.web.yaml').default, [Platform.Web]: require('./configDefaults.web.yaml').default,
} }
defaults = require('./configDefaults.yaml').default defaults = require('./configDefaults.yaml').default
} }

View File

@@ -96,3 +96,5 @@ hotkeys:
- '⌘-Shift-E' - '⌘-Shift-E'
command-selector: command-selector:
- '⌘-Shift-P' - '⌘-Shift-P'
appearance:
vibrancy: true

View File

@@ -19,7 +19,6 @@ appearance:
vibrancyType: 'blur' vibrancyType: 'blur'
lastTabClosesWindow: false lastTabClosesWindow: false
spaciness: 1 spaciness: 1
colorSchemeMode: 'dark'
terminal: terminal:
showBuiltinProfiles: true showBuiltinProfiles: true
showRecentProfiles: 3 showRecentProfiles: 3
@@ -32,7 +31,6 @@ hotkeys:
profile-selectors: profile-selectors:
__nonStructural: true __nonStructural: true
profiles: [] profiles: []
groups: []
profileDefaults: profileDefaults:
__nonStructural: true __nonStructural: true
ssh: ssh:

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core' import { TranslateService } from '@ngx-translate/core'
import { ProfilesService } from './services/profiles.service' import { ProfilesService } from './services/profiles.service'
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider' import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
import { PartialProfile, Profile } from './api'
/** @hidden */ /** @hidden */
@Injectable() @Injectable()
@@ -267,7 +268,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
return [ return [
...this.hotkeys, ...this.hotkeys,
...profiles.map(profile => ({ ...profiles.map(profile => ({
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`, id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
name: this.translate.instant('New tab: {profile}', { profile: profile.name }), name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
})), })),
...this.profilesService.getProviders().map(provider => ({ ...this.profilesService.getProviders().map(provider => ({
@@ -277,4 +278,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
] ]
} }
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
return (profile.id ?? profile.name).replace(/\./g, '-')
}
} }

View File

@@ -37,7 +37,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
import { DropZoneDirective } from './directives/dropZone.directive' import { DropZoneDirective } from './directives/dropZone.directive'
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive' import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, QuickConnectProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api' import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { ConfigService } from './services/config.service' import { ConfigService } from './services/config.service'
@@ -177,7 +177,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (hotkey.startsWith('profile.')) { if (hotkey.startsWith('profile.')) {
const id = hotkey.substring(hotkey.indexOf('.') + 1) const id = hotkey.substring(hotkey.indexOf('.') + 1)
const profiles = await profilesService.getProfiles() const profiles = await profilesService.getProfiles()
const profile = profiles.find(x => ProfilesService.getProfileHotkeyName(x) === id) const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
if (profile) { if (profile) {
profilesService.openNewTabForProfile(profile) profilesService.openNewTabForProfile(profile)
} }
@@ -188,10 +188,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (!provider) { if (!provider) {
return return
} }
this.showSelector(provider).catch(() => null) this.showSelector(provider)
} }
if (hotkey === 'command-selector') { if (hotkey === 'command-selector') {
commands.showSelector().catch(() => null) commands.showSelector()
} }
if (hotkey === 'profile-selector') { if (hotkey === 'profile-selector') {
@@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
callback: () => this.profilesService.openNewTabForProfile(p), callback: () => this.profilesService.openNewTabForProfile(p),
})) }))
if (provider instanceof QuickConnectProfileProvider) { if (provider.supportsQuickConnect) {
options.push({ options.push({
name: this.translate.instant('Quick connect'), name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'), freeInputPattern: this.translate.instant('Connect to "%s"...'),

View File

@@ -230,13 +230,11 @@ export class AppService {
if (this._activeTab) { if (this._activeTab) {
this._activeTab.clearActivity() this._activeTab.clearActivity()
this._activeTab.emitBlurred() this._activeTab.emitBlurred()
this._activeTab.emitVisibility(false)
} }
this._activeTab = tab this._activeTab = tab
this.activeTabChange.next(tab) this.activeTabChange.next(tab)
setImmediate(() => { setImmediate(() => {
this._activeTab?.emitFocused() this._activeTab?.emitFocused()
this._activeTab?.emitVisibility(true)
}) })
this.hostWindow.setTitle(this._activeTab?.title) this.hostWindow.setTitle(this._activeTab?.title)
} }

View File

@@ -101,7 +101,7 @@ export class CommandService {
context.tab = tab.getFocusedTab() ?? undefined context.tab = tab.getFocusedTab() ?? undefined
} }
const commands = await this.getCommands(context) const commands = await this.getCommands(context)
return this.selector.show( await this.selector.show(
this.translate.instant('Commands'), this.translate.instant('Commands'),
commands.map(c => ({ commands.map(c => ({
name: c.label, name: c.label,

View File

@@ -10,7 +10,6 @@ import { PlatformService } from '../api/platform'
import { HostAppService } from '../api/hostApp' import { HostAppService } from '../api/hostApp'
import { Vault, VaultService } from './vault.service' import { Vault, VaultService } from './vault.service'
import { serializeFunction } from '../utils' import { serializeFunction } from '../utils'
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
const deepmerge = require('deepmerge') const deepmerge = require('deepmerge')
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -365,47 +364,6 @@ export class ConfigService {
} }
config.version = 4 config.version = 4
} }
if (config.version < 5) {
const groups: PartialProfileGroup<ProfileGroup>[] = []
for (const p of config.profiles ?? []) {
if (!(p.group ?? '').trim()) {
continue
}
let group = groups.find(x => x.name === p.group)
if (!group) {
group = {
id: `${uuidv4()}`,
name: `${p.group}`,
}
groups.push(group)
}
p.group = group.id
}
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
for (const g of groups) {
if (profileGroupCollapsed[g.name]) {
const collapsed = profileGroupCollapsed[g.name]
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete profileGroupCollapsed[g.name]
profileGroupCollapsed[g.id] = collapsed
}
}
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
config.groups = groups
config.version = 5
}
if (config.version < 6) {
if (config.ssh?.clearServiceMessagesOnConnect === false) {
config.profileDefaults ??= {}
config.profileDefaults.ssh ??= {}
config.profileDefaults.ssh.clearServiceMessagesOnConnect = false
delete config.ssh?.clearServiceMessagesOnConnect
}
config.version = 6
}
} }
private async maybeDecryptConfig (store) { private async maybeDecryptConfig (store) {

View File

@@ -13,9 +13,8 @@ export class FileProvidersService {
) { } ) { }
async selectAndStoreFile (description: string): Promise<string> { async selectAndStoreFile (description: string): Promise<string> {
return this.selectProvider().then(p => { const p = await this.selectProvider()
return p.selectAndStoreFile(description) return p.selectAndStoreFile(description)
})
} }
async retrieveFile (key: string): Promise<Buffer> { async retrieveFile (key: string): Promise<Buffer> {

View File

@@ -2,15 +2,12 @@ import { Injectable, Inject } from '@angular/core'
import { TranslateService } from '@ngx-translate/core' import { TranslateService } from '@ngx-translate/core'
import { NewTabParameters } from './tabs.service' import { NewTabParameters } from './tabs.service'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, ProfileProvider } from '../api/profileProvider' import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
import { SelectorOption } from '../api/selector' import { SelectorOption } from '../api/selector'
import { AppService } from './app.service' import { AppService } from './app.service'
import { configMerge, ConfigProxy, ConfigService } from './config.service' import { configMerge, ConfigProxy, ConfigService } from './config.service'
import { NotificationsService } from './notifications.service' import { NotificationsService } from './notifications.service'
import { SelectorService } from './selector.service' import { SelectorService } from './selector.service'
import deepClone from 'clone-deep'
import { v4 as uuidv4 } from 'uuid'
import slugify from 'slugify'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ProfilesService { export class ProfilesService {
@@ -39,126 +36,6 @@ export class ProfilesService {
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[], @Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
) { } ) { }
/*
* Methods used to interract with ProfileProvider
*/
getProviders (): ProfileProvider<Profile>[] {
return [...this.profileProviders]
}
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
return provider as unknown as ProfileProvider<T>|null
}
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
profile = this.getConfigProxyForProfile(profile)
return this.providerForProfile(profile)?.getDescription(profile) ?? null
}
/*
* Methods used to interract with Profile
*/
/*
* Return ConfigProxy for a given Profile
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
*/
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): T {
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
return new ConfigProxy(profile, defaults) as unknown as T
}
/**
* Return an Array of Profiles
* arg: includeBuiltin (default: true) -> include BuiltinProfiles
* arg: clone (default: false) -> return deepclone Array
*/
async getProfiles (options?: { includeBuiltin?: boolean, clone?: boolean }): Promise<PartialProfile<Profile>[]> {
let list = this.config.store.profiles ?? []
if (options?.includeBuiltin ?? true) {
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
list = [
...this.config.store.profiles ?? [],
...lists.reduce((a, b) => a.concat(b), []),
]
}
const sortKey = p => `${this.resolveProfileGroupName(p.group ?? '')} / ${p.name}`
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
return options?.clone ? deepClone(list) : list
}
/**
* Insert a new Profile in config
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
*/
async newProfile (profile: PartialProfile<Profile>, options?: { genId?: boolean }): Promise<void> {
if (options?.genId ?? true) {
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
}
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
if (cProfile) {
throw new Error(`Cannot insert new Profile, duplicated Id: ${profile.id}`)
}
this.config.store.profiles.push(profile)
}
/**
* Write a Profile in config
*/
async writeProfile (profile: PartialProfile<Profile>): Promise<void> {
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
if (cProfile) {
if (!profile.group) {
delete cProfile.group
}
Object.assign(cProfile, profile)
}
}
/**
* Delete a Profile from config
*/
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
this.config.store.profiles = this.config.store.profiles.filter(p => p.id !== profile.id)
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete profileHotkeys[profileHotkeyName]
this.config.store.hotkeys.profile = profileHotkeys
}
}
/**
* Delete all Profiles from config using option filter
* arg: filter (p: PartialProfile<Profile>) => boolean -> predicate used to decide which profiles have to be deleted
*/
async bulkDeleteProfiles (filter: (p: PartialProfile<Profile>) => boolean): Promise<void> {
for (const profile of this.config.store.profiles.filter(filter)) {
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete profileHotkeys[profileHotkeyName]
this.config.store.hotkeys.profile = profileHotkeys
}
}
this.config.store.profiles = this.config.store.profiles.filter(x => !filter(x))
}
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> { async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
const params = await this.newTabParametersForProfile(profile) const params = await this.newTabParametersForProfile(profile)
if (params) { if (params) {
@@ -186,40 +63,52 @@ export class ProfilesService {
return params return params
} }
async launchProfile (profile: PartialProfile<Profile>): Promise<void> { getProviders (): ProfileProvider<Profile>[] {
await this.openNewTabForProfile(profile) return [...this.profileProviders]
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
if (this.config.store.terminal.showRecentProfiles > 0) {
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
recentProfiles.unshift(profile)
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
} else {
recentProfiles = []
}
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
} }
static getProfileHotkeyName (profile: PartialProfile<Profile>): string { async getProfiles (): Promise<PartialProfile<Profile>[]> {
return (profile.id ?? profile.name).replace(/\./g, '-') const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
let list = lists.reduce((a, b) => a.concat(b), [])
list = [
...this.config.store.profiles ?? [],
...list,
]
const sortKey = p => `${p.group ?? ''} / ${p.name}`
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
return list
} }
/* providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
* Methods used to interract with Profile Selector const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
*/ return provider as unknown as ProfileProvider<T>|null
}
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
profile = this.getConfigProxyForProfile(profile)
return this.providerForProfile(profile)?.getDescription(profile) ?? null
}
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> { selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
const fullProfile = this.getConfigProxyForProfile(profile) const fullProfile = this.getConfigProxyForProfile(profile)
const provider = this.providerForProfile(fullProfile) const provider = this.providerForProfile(fullProfile)
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
return { return {
...profile, ...profile,
group: this.resolveProfileGroupName(profile.group ?? ''), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
group: profile.group || '',
freeInputEquivalent, freeInputEquivalent,
description: provider?.getDescription(fullProfile), description: provider?.getDescription(fullProfile),
} }
} }
getRecentProfiles (): PartialProfile<Profile>[] {
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
return recentProfiles
}
showProfileSelector (): Promise<PartialProfile<Profile>|null> { showProfileSelector (): Promise<PartialProfile<Profile>|null> {
if (this.selector.active) { if (this.selector.active) {
return Promise.resolve(null) return Promise.resolve(null)
@@ -229,12 +118,12 @@ export class ProfilesService {
try { try {
const recentProfiles = this.getRecentProfiles() const recentProfiles = this.getRecentProfiles()
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({ let options: SelectorOption<void>[] = recentProfiles.map(p => ({
...this.selectorOptionForProfile(p), ...this.selectorOptionForProfile(p),
group: this.translate.instant('Recent'), group: this.translate.instant('Recent'),
icon: 'fas fa-history', icon: 'fas fa-history',
color: p.color, color: p.color,
weight: i - (recentProfiles.length + 1), weight: -2,
callback: async () => { callback: async () => {
if (p.id) { if (p.id) {
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
@@ -288,8 +177,7 @@ export class ProfilesService {
}) })
} catch { } } catch { }
this.getProviders().forEach(provider => { this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
if (provider instanceof QuickConnectProfileProvider) {
options.push({ options.push({
name: this.translate.instant('Quick connect'), name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'), freeInputPattern: this.translate.instant('Connect to "%s"...'),
@@ -301,25 +189,18 @@ export class ProfilesService {
resolve(profile) resolve(profile)
}, },
}) })
}
}) })
await this.selector.show(this.translate.instant('Select profile or enter an address'), options).catch(() => reject()) await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
} catch (err) { } catch (err) {
reject(err) reject(err)
} }
}) })
} }
getRecentProfiles (): PartialProfile<Profile>[] {
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
return recentProfiles
}
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> { async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
for (const provider of this.getProviders()) { for (const provider of this.getProviders()) {
if (provider instanceof QuickConnectProfileProvider) { if (provider.supportsQuickConnect) {
const profile = provider.quickConnect(query) const profile = provider.quickConnect(query)
if (profile) { if (profile) {
return profile return profile
@@ -330,178 +211,27 @@ export class ProfilesService {
return null return null
} }
/* getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
* Methods used to interract with Profile/ProfileGroup/Global defaults
*/
/**
* Return global defaults for a given profile provider
* Always return something, empty object if no defaults found
*/
getProviderDefaults (provider: ProfileProvider<Profile>): any {
const defaults = this.config.store.profileDefaults
return defaults[provider.id] ?? {}
}
/**
* Set global defaults for a given profile provider
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
setProviderDefaults (provider: ProfileProvider<Profile>, pdefaults: any): void {
this.config.store.profileDefaults[provider.id] = pdefaults
}
/**
* Return defaults for a given profile
* Always return something, empty object if no defaults found
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
*/
getProfileDefaults (profile: PartialProfile<Profile>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): any[] {
const provider = this.providerForProfile(profile) const provider = this.providerForProfile(profile)
const defaults = [
return [
this.profileDefaults, this.profileDefaults,
provider?.configDefaults ?? {}, provider?.configDefaults ?? {},
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {}, !provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {}, ].reduce(configMerge, {})
] return new ConfigProxy(profile, defaults) as unknown as T
} }
/* async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
* Methods used to interract with ProfileGroup await this.openNewTabForProfile(profile)
*/
/** let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
* Synchronously return an Array of the existing ProfileGroups if (this.config.store.terminal.showRecentProfiles > 0) {
* Does not return builtin groups recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
*/ recentProfiles.unshift(profile)
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] { recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
return deepClone(this.config.store.groups ?? [])
}
/**
* Return an Array of the existing ProfileGroups
* arg: includeProfiles (default: false) -> if false, does not fill up the profiles field of ProfileGroup
* arg: includeNonUserGroup (default: false) -> if false, does not add built-in and ungrouped groups
*/
async getProfileGroups (options?: { includeProfiles?: boolean, includeNonUserGroup?: boolean }): Promise<PartialProfileGroup<ProfileGroup>[]> {
let profiles: PartialProfile<Profile>[] = []
if (options?.includeProfiles) {
profiles = await this.getProfiles({ includeBuiltin: options.includeNonUserGroup, clone: true })
}
let groups: PartialProfileGroup<ProfileGroup>[] = this.getSyncProfileGroups()
groups = groups.map(x => {
x.editable = true
if (options?.includeProfiles) {
x.profiles = profiles.filter(p => p.group === x.id)
profiles = profiles.filter(p => p.group !== x.id)
}
return x
})
if (options?.includeNonUserGroup) {
const builtInGroups: PartialProfileGroup<ProfileGroup>[] = []
builtInGroups.push({
id: 'built-in',
name: this.translate.instant('Built-in'),
editable: false,
profiles: [],
})
const ungrouped: PartialProfileGroup<ProfileGroup> = {
id: 'ungrouped',
name: this.translate.instant('Ungrouped'),
editable: false,
}
if (options.includeProfiles) {
for (const profile of profiles.filter(p => p.isBuiltin)) {
let group: PartialProfileGroup<ProfileGroup> | undefined = builtInGroups.find(g => g.id === slugify(profile.group ?? 'built-in'))
if (!group) {
group = {
id: `${slugify(profile.group!)}`,
name: `${profile.group!}`,
editable: false,
profiles: [],
}
builtInGroups.push(group)
}
group.profiles!.push(profile)
}
ungrouped.profiles = profiles.filter(p => !p.isBuiltin)
}
groups = groups.concat(builtInGroups)
groups.push(ungrouped)
}
return groups
}
/**
* Insert a new ProfileGroup in config
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
*/
async newProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { genId?: boolean }): Promise<void> {
if (options?.genId ?? true) {
group.id = `${uuidv4()}`
}
const cProfileGroup = this.config.store.groups.find(p => p.id === group.id)
if (cProfileGroup) {
throw new Error(`Cannot insert new ProfileGroup, duplicated Id: ${group.id}`)
}
this.config.store.groups.push(group)
}
/**
* Write a ProfileGroup in config
*/
async writeProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
delete group.profiles
delete group.editable
const cGroup = this.config.store.groups.find(g => g.id === group.id)
if (cGroup) {
Object.assign(cGroup, group)
}
}
/**
* Delete a ProfileGroup from config
*/
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { deleteProfiles?: boolean }): Promise<void> {
this.config.store.groups = this.config.store.groups.filter(g => g.id !== group.id)
if (options?.deleteProfiles) {
await this.bulkDeleteProfiles((p) => p.group === group.id)
} else { } else {
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) { recentProfiles = []
delete profile.group }
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
} }
} }
}
/**
* Resolve and return ProfileGroup Name from ProfileGroup ID
*/
resolveProfileGroupName (groupId: string): string {
return this.config.store.groups.find(g => g.id === groupId)?.name ?? groupId
}
/**
* Return defaults for a given group ID and provider
* Always return something, empty object if no defaults found
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
*/
getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider<Profile>): any {
return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
}
}

View File

@@ -3,7 +3,7 @@ import { Subject, Observable } from 'rxjs'
import * as Color from 'color' import * as Color from 'color'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { Theme } from '../api/theme' import { Theme } from '../api/theme'
import { PlatformService, PlatformTheme } from '../api/platform' import { PlatformService } from '../api/platform'
import { NewTheme } from '../theme' import { NewTheme } from '../theme'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -194,14 +194,7 @@ export class ThemesService {
/// @hidden /// @hidden
_getActiveColorScheme (): any { _getActiveColorScheme (): any {
let theme: PlatformTheme = 'dark' if (this.platform.getTheme() === 'light') {
if (this.config.store.appearance.colorSchemeMode === 'light') {
theme = 'light'
} else if (this.config.store.appearance.colorSchemeMode === 'auto') {
theme = this.platform.getTheme()
}
if (theme === 'light') {
return this.config.store.terminal.lightColorScheme return this.config.store.terminal.lightColorScheme
} else { } else {
return this.config.store.terminal.colorScheme return this.config.store.terminal.colorScheme

View File

@@ -285,7 +285,7 @@ export class VaultFileProvider extends FileProvider {
icon: 'fas fa-file', icon: 'fas fa-file',
result: f, result: f,
})), })),
]).catch(() => null) ])
if (result) { if (result) {
return `${this.prefix}${result.key.id}` return `${this.prefix}${result.key.id}`
} }

View File

@@ -149,7 +149,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
click: async () => { click: async () => {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('Profile name') modal.componentInstance.prompt = this.translate.instant('Profile name')
const name = (await modal.result.catch(() => null))?.value const name = (await modal.result)?.value
if (!name) { if (!name) {
return return
} }
@@ -262,7 +262,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
} }
async switchTabProfile (tab: BaseTabComponent) { async switchTabProfile (tab: BaseTabComponent) {
const profile = await this.profilesService.showProfileSelector().catch(() => null) const profile = await this.profilesService.showProfileSelector()
if (!profile) { if (!profile) {
return return
} }

View File

@@ -22,6 +22,5 @@ export class ElectronConfigProvider extends ConfigProvider {
}, },
}, },
} }
defaults = {} defaults = {}
} }

View File

@@ -33,7 +33,6 @@ export class ShellIntegrationService {
command: 'paste "%V"', command: 'paste "%V"',
}, },
] ]
private constructor ( private constructor (
private electron: ElectronService, private electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,

View File

@@ -70,7 +70,6 @@ export class PluginManagerService {
map(plugins => { map(plugins => {
const mapping: Record<string, PluginInfo[]> = {} const mapping: Record<string, PluginInfo[]> = {}
for (const p of plugins) { for (const p of plugins) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
mapping[p.name] ??= [] mapping[p.name] ??= []
mapping[p.name].push(p) mapping[p.name].push(p)
} }

View File

@@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
import { LogService, NotificationsService } from 'tabby-core' import { LogService, NotificationsService } from 'tabby-core'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Injector, NgZone } from '@angular/core' import { Injector, NgZone } from '@angular/core'
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal' import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
import { SerialService } from './services/serial.service' import { SerialService } from './services/serial.service'
export interface SerialProfile extends ConnectableTerminalProfile { export interface SerialProfile extends BaseTerminalProfile {
options: SerialProfileOptions options: SerialProfileOptions
} }

View File

@@ -2,14 +2,14 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
import slugify from 'slugify' import slugify from 'slugify'
import deepClone from 'clone-deep' import deepClone from 'clone-deep'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NewTabParameters, SelectorService, HostAppService, Platform, TranslateService, ConnectableProfileProvider } from 'tabby-core' import { ProfileProvider, NewTabParameters, SelectorService, HostAppService, Platform, TranslateService } from 'tabby-core'
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component' import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
import { SerialTabComponent } from './components/serialTab.component' import { SerialTabComponent } from './components/serialTab.component'
import { SerialService } from './services/serial.service' import { SerialService } from './services/serial.service'
import { BAUD_RATES, SerialProfile } from './api' import { BAUD_RATES, SerialProfile } from './api'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class SerialProfilesService extends ConnectableProfileProvider<SerialProfile> { export class SerialProfilesService extends ProfileProvider<SerialProfile> {
id = 'serial' id = 'serial'
name = _('Serial') name = _('Serial')
settingsComponent = SerialProfileSettingsComponent settingsComponent = SerialProfileSettingsComponent
@@ -32,7 +32,6 @@ export class SerialProfilesService extends ConnectableProfileProvider<SerialProf
slowSend: false, slowSend: false,
input: { backspace: 'backspace' }, input: { backspace: 'backspace' },
}, },
clearServiceMessagesOnConnect: false,
} }
constructor ( constructor (

View File

@@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('Name for the new config') modal.componentInstance.prompt = this.translate.instant('Name for the new config')
modal.componentInstance.value = name modal.componentInstance.value = name
name = (await modal.result.catch(() => null))?.value name = (await modal.result)?.value
if (!name) { if (!name) {
return return
} }

View File

@@ -1,32 +0,0 @@
.modal-header
h3.m-0 {{group.name}}
.modal-body
.row
.col-12.col-lg-4
.mb-3
label(translate) Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='group.name',
)
.col-12.col-lg-8
.form-line.content-box
.header
.title(translate) Default profile group settings
.description(translate) These apply to all profiles of a given type in this group
.list-group.mt-3.mb-3.content-box
a.list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='editDefaults(provider)',
*ngFor='let provider of providers'
) {{provider.name|translate}}
.me-auto
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
i.fas.fa-trash-arrow-up
.modal-footer
button.btn.btn-primary((click)='save()', translate) Save
button.btn.btn-danger((click)='cancel()', translate) Cancel

View File

@@ -1,54 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigProxy, ProfileGroup, Profile, ProfileProvider, PlatformService, TranslateService } from 'tabby-core'
/** @hidden */
@Component({
templateUrl: './editProfileGroupModal.component.pug',
})
export class EditProfileGroupModalComponent<G extends ProfileGroup> {
@Input() group: G & ConfigProxy
@Input() providers: ProfileProvider<Profile>[]
constructor (
private modalInstance: NgbActiveModal,
private platform: PlatformService,
private translate: TranslateService,
) {}
save () {
this.modalInstance.close({ group: this.group })
}
cancel () {
this.modalInstance.dismiss()
}
editDefaults (provider: ProfileProvider<Profile>) {
this.modalInstance.close({ group: this.group, provider })
}
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: this.translate.instant('Restore settings to inherited defaults ?'),
buttons: [
this.translate.instant('Delete'),
this.translate.instant('Keep'),
],
defaultId: 1,
cancelId: 1,
},
)).response === 0) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.group.defaults?.[provider.id]
}
}
}
export interface EditProfileGroupModalComponentResult<G extends ProfileGroup> {
group: G
provider?: ProfileProvider<Profile>
}

View File

@@ -1,7 +1,7 @@
.modal-header(*ngIf='defaultsMode === "disabled"') .modal-header(*ngIf='!defaultsMode')
h3.m-0 {{profile.name}} h3.m-0 {{profile.name}}
.modal-header(*ngIf='defaultsMode !== "disabled"') .modal-header(*ngIf='defaultsMode')
h3.m-0( h3.m-0(
translate='Defaults for {type}', translate='Defaults for {type}',
[translateParams]='{type: profileProvider.name}' [translateParams]='{type: profileProvider.name}'
@@ -10,7 +10,7 @@
.modal-body .modal-body
.row .row
.col-12.col-lg-4 .col-12.col-lg-4
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Name label(translate) Name
input.form-control( input.form-control(
type='text', type='text',
@@ -18,20 +18,17 @@
[(ngModel)]='profile.name', [(ngModel)]='profile.name',
) )
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Group label(translate) Group
input.form-control( input.form-control(
type='text', type='text',
alwaysVisibleTypeahead, alwaysVisibleTypeahead,
placeholder='Ungrouped', placeholder='Ungrouped',
[(ngModel)]='profileGroup', [(ngModel)]='profile.group',
[ngbTypeahead]='groupTypeahead', [ngbTypeahead]='groupTypeahead',
[inputFormatter]="groupFormatter",
[resultFormatter]="groupFormatter",
[editable]="false"
) )
.mb-3(*ngIf='defaultsMode === "disabled"') .mb-3(*ngIf='!defaultsMode')
label(translate) Icon label(translate) Icon
.input-group .input-group
input.form-control( input.form-control(
@@ -77,15 +74,9 @@
) )
option(ngValue='auto', translate) Auto option(ngValue='auto', translate) Auto
option(ngValue='keep', translate) Keep option(ngValue='keep', translate) Keep
option(*ngIf='isConnectable()', ngValue='reconnect', translate) Reconnect option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect
option(ngValue='close', translate) Close option(ngValue='close', translate) Close
.form-line(*ngIf='isConnectable()')
.header
.title(translate) Clear terminal after connection
toggle(
[(ngModel)]='profile.clearServiceMessagesOnConnect',
)
.mb-4 .mb-4
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent') .col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')

View File

@@ -2,7 +2,7 @@
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs' import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core' import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigProxy, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider } from 'tabby-core' import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
const iconsData = require('../../../tabby-core/src/icons.json') const iconsData = require('../../../tabby-core/src/icons.json')
const iconsClassList = Object.keys(iconsData).map( const iconsClassList = Object.keys(iconsData).map(
@@ -19,9 +19,8 @@ export class EditProfileModalComponent<P extends Profile> {
@Input() profile: P & ConfigProxy @Input() profile: P & ConfigProxy
@Input() profileProvider: ProfileProvider<P> @Input() profileProvider: ProfileProvider<P>
@Input() settingsComponent: new () => ProfileSettingsComponent<P> @Input() settingsComponent: new () => ProfileSettingsComponent<P>
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled' @Input() defaultsMode = false
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined groupNames: string[]
groups: PartialProfileGroup<ProfileGroup>[]
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
private _profile: Profile private _profile: Profile
@@ -31,14 +30,14 @@ export class EditProfileModalComponent<P extends Profile> {
private injector: Injector, private injector: Injector,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private profilesService: ProfilesService, private profilesService: ProfilesService,
config: ConfigService,
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
) { ) {
if (this.defaultsMode === 'disabled') { this.groupNames = [...new Set(
this.profilesService.getProfileGroups().then(groups => { (config.store.profiles as Profile[])
this.groups = groups .map(x => x.group)
this.profileGroup = groups.find(g => g.id === this.profile.group) .filter(x => !!x),
}) )].sort() as string[]
}
} }
colorsAutocomplete = text$ => text$.pipe( colorsAutocomplete = text$ => text$.pipe(
@@ -57,7 +56,7 @@ export class EditProfileModalComponent<P extends Profile> {
ngOnInit () { ngOnInit () {
this._profile = this.profile this._profile = this.profile
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' }) this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode)
} }
ngAfterViewInit () { ngAfterViewInit () {
@@ -73,15 +72,13 @@ export class EditProfileModalComponent<P extends Profile> {
} }
} }
groupTypeahead: OperatorFunction<string, readonly PartialProfileGroup<ProfileGroup>[]> = (text$: Observable<string>) => groupTypeahead = (text$: Observable<string>) =>
text$.pipe( text$.pipe(
debounceTime(200), debounceTime(200),
distinctUntilChanged(), distinctUntilChanged(),
map(q => this.groups.filter(g => !q || g.name.toLowerCase().includes(q.toLowerCase()))), map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))),
) )
groupFormatter = (g: PartialProfileGroup<ProfileGroup>) => g.name
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) => iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
text$.pipe( text$.pipe(
debounceTime(200), debounceTime(200),
@@ -89,12 +86,7 @@ export class EditProfileModalComponent<P extends Profile> {
) )
save () { save () {
if (!this.profileGroup) { this.profile.group ||= undefined
this.profile.group = undefined
} else {
this.profile.group = this.profileGroup.id
}
this.settingsComponentInstance?.save?.() this.settingsComponentInstance?.save?.()
this.profile.__cleanup() this.profile.__cleanup()
this.modalInstance.close(this._profile) this.modalInstance.close(this._profile)
@@ -103,9 +95,4 @@ export class EditProfileModalComponent<P extends Profile> {
cancel () { cancel () {
this.modalInstance.dismiss() this.modalInstance.dismiss()
} }
isConnectable (): boolean {
return this.profileProvider instanceof ConnectableProfileProvider
}
} }

View File

@@ -27,17 +27,9 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
i.fas.fa-fw.fa-search i.fas.fa-fw.fa-search
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter') input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3 button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
button.btn.btn-primary(ngbDropdownToggle)
i.fas.fa-fw.fa-plus
span(translate) New
div(ngbDropdownMenu)
button(ngbDropdownItem, (click)='newProfile()')
i.fas.fa-fw.fa-plus i.fas.fa-fw.fa-plus
span(translate) New profile span(translate) New profile
button(ngbDropdownItem, (click)='newProfileGroup()')
i.fas.fa-fw.fa-plus
span(translate) New profile Group
.list-group.mt-3.mb-3 .list-group.mt-3.mb-3
ng-container(*ngFor='let group of profileGroups') ng-container(*ngFor='let group of profileGroups')
@@ -45,17 +37,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.list-group-item.list-group-item-action.d-flex.align-items-center( .list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='toggleGroupCollapse(group)' (click)='toggleGroupCollapse(group)'
) )
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0') .fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0') .fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}} span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
button.btn.btn-sm.btn-link.hover-reveal.ms-2( button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name', *ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); editProfileGroup(group)' (click)='$event.stopPropagation(); editGroup(group)'
) )
i.fas.fa-pencil-alt i.fas.fa-pencil-alt
button.btn.btn-sm.btn-link.hover-reveal.ms-2( button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name', *ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); deleteProfileGroup(group)' (click)='$event.stopPropagation(); deleteGroup(group)'
) )
i.fas.fa-trash-alt i.fas.fa-trash-alt
ng-container(*ngIf='!group.collapsed') ng-container(*ngIf='!group.collapsed')
@@ -75,7 +67,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.me-auto .me-auto
button.btn.btn-link.hover-reveal.ms-1(*ngIf='!profile.isTemplate', (click)='$event.stopPropagation(); launchProfile(profile)') button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); launchProfile(profile)')
i.fas.fa-play i.fas.fa-play
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto') .ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
@@ -177,12 +169,9 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.description(translate) These apply to all profiles of a given type .description(translate) These apply to all profiles of a given type
.list-group.mt-3.mb-3.content-box .list-group.mt-3.mb-3.content-box
a.list-group-item.list-group-item-action.d-flex.align-items-center( a.list-group-item.list-group-item-action(
(click)='editDefaults(provider)', (click)='editDefaults(provider)',
*ngFor='let provider of profileProviders' *ngFor='let provider of profileProviders'
) {{provider.name|translate}} ) {{provider.name|translate}}
.me-auto
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
i.fas.fa-trash-arrow-up
div([ngbNavOutlet]='nav') div([ngbNavOutlet]='nav')

View File

@@ -1,27 +1,32 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker' import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
import { v4 as uuidv4 } from 'uuid'
import slugify from 'slugify'
import deepClone from 'clone-deep' import deepClone from 'clone-deep'
import { Component, Inject } from '@angular/core' import { Component, Inject } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup, QuickConnectProfileProvider } from 'tabby-core' import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
import { EditProfileModalComponent } from './editProfileModal.component' import { EditProfileModalComponent } from './editProfileModal.component'
import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
interface ProfileGroup {
name?: string
profiles: PartialProfile<Profile>[]
editable: boolean
collapsed: boolean
}
_('Filter') _('Filter')
_('Ungrouped') _('Ungrouped')
interface CollapsableProfileGroup extends ProfileGroup {
collapsed: boolean
}
/** @hidden */ /** @hidden */
@Component({ @Component({
templateUrl: './profilesSettingsTab.component.pug', templateUrl: './profilesSettingsTab.component.pug',
styleUrls: ['./profilesSettingsTab.component.scss'], styleUrls: ['./profilesSettingsTab.component.scss'],
}) })
export class ProfilesSettingsTabComponent extends BaseComponent { export class ProfilesSettingsTabComponent extends BaseComponent {
profiles: PartialProfile<Profile>[] = []
builtinProfiles: PartialProfile<Profile>[] = [] builtinProfiles: PartialProfile<Profile>[] = []
templateProfiles: PartialProfile<Profile>[] = [] templateProfiles: PartialProfile<Profile>[] = []
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[] profileGroups: ProfileGroup[]
filter = '' filter = ''
Platform = Platform Platform = Platform
@@ -54,7 +59,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
async newProfile (base?: PartialProfile<Profile>): Promise<void> { async newProfile (base?: PartialProfile<Profile>): Promise<void> {
if (!base) { if (!base) {
let profiles = await this.profilesService.getProfiles() let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
profiles = profiles.filter(x => !this.isProfileBlacklisted(x)) profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0)) profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
base = await this.selector.show( base = await this.selector.show(
@@ -62,13 +67,10 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
profiles.map(p => ({ profiles.map(p => ({
icon: p.icon, icon: p.icon,
description: this.profilesService.getDescription(p) ?? undefined, description: this.profilesService.getDescription(p) ?? undefined,
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name, name: p.group ? `${p.group} / ${p.name}` : p.name,
result: p, result: p,
})), })),
).catch(() => undefined) )
if (!base) {
return
}
} }
const profile: PartialProfile<Profile> = deepClone(base) const profile: PartialProfile<Profile> = deepClone(base)
delete profile.id delete profile.id
@@ -88,7 +90,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile) const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base) profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
} }
await this.profilesService.newProfile(profile) profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
this.config.store.profiles = [profile, ...this.config.store.profiles]
await this.config.save() await this.config.save()
} }
@@ -98,7 +101,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
return return
} }
Object.assign(profile, result) Object.assign(profile, result)
await this.profilesService.writeProfile(profile)
await this.config.save() await this.config.save()
} }
@@ -142,80 +144,69 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1, cancelId: 1,
}, },
)).response === 0) { )).response === 0) {
await this.profilesService.deleteProfile(profile) this.profilesService.providerForProfile(profile)?.deleteProfile(
await this.config.save() this.profilesService.getConfigProxyForProfile(profile))
} this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
} const profileHotkeyName = AppHotkeyProvider.getProfileHotkeyName(profile)
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
async newProfileGroup (): Promise<void> { const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('New group name')
const result = await modal.result.catch(() => null)
if (result?.value.trim()) {
await this.profilesService.newProfileGroup({ id: '', name: result.value })
await this.config.save()
}
}
async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> {
const result = await this.showProfileGroupEditModal(group)
if (!result) {
return
}
Object.assign(group, result)
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group))
await this.config.save()
}
async showProfileGroupEditModal (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
const modal = this.ngbModal.open(
EditProfileGroupModalComponent,
{ size: 'lg' },
)
modal.componentInstance.group = deepClone(group)
modal.componentInstance.providers = this.profileProviders
const result: EditProfileGroupModalComponentResult<CollapsableProfileGroup> | null = await modal.result.catch(() => null)
if (!result) {
return null
}
if (result.provider) {
return this.editProfileGroupDefaults(result.group, result.provider)
}
return result.group
}
private async editProfileGroupDefaults (group: PartialProfileGroup<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
const modal = this.ngbModal.open(
EditProfileModalComponent,
{ size: 'lg' },
)
const model = group.defaults?.[provider.id] ?? {}
model.type = provider.id
modal.componentInstance.profile = Object.assign({}, model)
modal.componentInstance.profileProvider = provider
modal.componentInstance.defaultsMode = 'group'
const result = await modal.result.catch(() => null)
if (result) {
// Fully replace the config
for (const k in model) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete model[k] delete profileHotkeys[profileHotkeyName]
this.config.store.hotkeys.profile = profileHotkeys
} }
Object.assign(model, result) await this.config.save()
if (!group.defaults) {
group.defaults = {}
} }
group.defaults[provider.id] = model
}
return this.showProfileGroupEditModal(group)
} }
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> { refresh (): void {
this.profiles = this.config.store.profiles
this.profileGroups = []
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
for (const profile of this.profiles) {
// Group null, undefined and empty together
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let group = this.profileGroups.find(x => x.name === (profile.group || ''))
if (!group) {
group = {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: profile.group || '',
profiles: [],
editable: true,
collapsed: profileGroupCollapsed[profile.group ?? ''] ?? false,
}
this.profileGroups.push(group)
}
group.profiles.push(profile)
}
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
const builtIn = {
name: this.translate.instant('Built-in'),
profiles: this.builtinProfiles,
editable: false,
collapsed: false,
}
builtIn.collapsed = profileGroupCollapsed[builtIn.name ?? ''] ?? false
this.profileGroups.push(builtIn)
}
async editGroup (group: ProfileGroup): Promise<void> {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('New name')
modal.componentInstance.value = group.name
const result = await modal.result
if (result) {
for (const profile of this.profiles.filter(x => x.group === group.name)) {
profile.group = result.value
}
this.config.store.profiles = this.profiles
await this.config.save()
}
}
async deleteGroup (group: ProfileGroup): Promise<void> {
if ((await this.platform.showMessageBox( if ((await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
@@ -228,8 +219,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1, cancelId: 1,
}, },
)).response === 0) { )).response === 0) {
let deleteProfiles = false if ((await this.platform.showMessageBox(
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
message: this.translate.instant('Delete the group\'s profiles?'), message: this.translate.instant('Delete the group\'s profiles?'),
@@ -240,26 +230,19 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
defaultId: 0, defaultId: 0,
cancelId: 0, cancelId: 0,
}, },
)).response !== 0) { )).response === 0) {
deleteProfiles = true for (const profile of this.profiles.filter(x => x.group === group.name)) {
delete profile.group
}
} else {
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
} }
await this.profilesService.deleteProfileGroup(group, { deleteProfiles })
await this.config.save() await this.config.save()
} }
} }
async refresh (): Promise<void> { isGroupVisible (group: ProfileGroup): boolean {
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}') return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
const groups = await this.profilesService.getProfileGroups({ includeNonUserGroup: true, includeProfiles: true })
groups.sort((a, b) => a.name.localeCompare(b.name))
groups.sort((a, b) => (a.id === 'built-in' || !a.editable ? 1 : 0) - (b.id === 'built-in' || !b.editable ? 1 : 0))
groups.sort((a, b) => (a.id === 'ungrouped' ? 0 : 1) - (b.id === 'ungrouped' ? 0 : 1))
this.profileGroups = groups.map(g => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false))
}
isGroupVisible (group: PartialProfileGroup<ProfileGroup>): boolean {
return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x))
} }
isProfileVisible (profile: PartialProfile<Profile>): boolean { isProfileVisible (profile: PartialProfile<Profile>): boolean {
@@ -287,12 +270,11 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning' }[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
} }
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void { toggleGroupCollapse (group: ProfileGroup): void {
if (group.profiles?.length === 0) {
return
}
group.collapsed = !group.collapsed group.collapsed = !group.collapsed
this.saveProfileGroupCollapse(group) const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
profileGroupCollapsed[group.name ?? ''] = group.collapsed
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
} }
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> { async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
@@ -300,41 +282,22 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
EditProfileModalComponent, EditProfileModalComponent,
{ size: 'lg' }, { size: 'lg' },
) )
const model = this.profilesService.getProviderDefaults(provider) const model = this.config.store.profileDefaults[provider.id] ?? {}
model.type = provider.id model.type = provider.id
modal.componentInstance.profile = Object.assign({}, model) modal.componentInstance.profile = Object.assign({}, model)
modal.componentInstance.profileProvider = provider modal.componentInstance.profileProvider = provider
modal.componentInstance.defaultsMode = 'enabled' modal.componentInstance.defaultsMode = true
const result = await modal.result.catch(() => null) const result = await modal.result
if (result) {
// Fully replace the config // Fully replace the config
for (const k in model) { for (const k in model) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete model[k] delete model[k]
} }
Object.assign(model, result) Object.assign(model, result)
this.profilesService.setProviderDefaults(provider, model) this.config.store.profileDefaults[provider.id] = model
await this.config.save() await this.config.save()
} }
}
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: this.translate.instant('Restore settings to defaults ?'),
buttons: [
this.translate.instant('Delete'),
this.translate.instant('Keep'),
],
defaultId: 1,
cancelId: 1,
},
)).response === 0) {
this.profilesService.setProviderDefaults(provider, {})
await this.config.save()
}
}
blacklistProfile (profile: PartialProfile<Profile>): void { blacklistProfile (profile: PartialProfile<Profile>): void {
this.config.store.profileBlacklist = [...this.config.store.profileBlacklist, profile.id] this.config.store.profileBlacklist = [...this.config.store.profileBlacklist, profile.id]
@@ -351,29 +314,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
} }
getQuickConnectProviders (): ProfileProvider<Profile>[] { getQuickConnectProviders (): ProfileProvider<Profile>[] {
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider) return this.profileProviders.filter(x => x.supportsQuickConnect)
}
/**
* Save ProfileGroup collapse state in localStorage
*/
private saveProfileGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
profileGroupCollapsed[group.id] = group.collapsed
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
}
private static collapsableIntoPartialProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): PartialProfileGroup<ProfileGroup> {
const g: any = { ...group }
delete g.collapsed
return g
}
private static intoPartialCollapsableProfileGroup (group: PartialProfileGroup<ProfileGroup>, collapsed: boolean): PartialProfileGroup<CollapsableProfileGroup> {
const collapsableGroup = {
...group,
collapsed,
}
return collapsableGroup
} }
} }

View File

@@ -35,12 +35,10 @@ export class VaultSettingsTabComponent extends BaseComponent {
async enableVault () { async enableVault () {
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent) const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
const newPassphrase = await modal.result.catch(() => null) const newPassphrase = await modal.result
if (newPassphrase) {
await this.vault.setEnabled(true, newPassphrase) await this.vault.setEnabled(true, newPassphrase)
this.vaultContents = await this.vault.load(newPassphrase) this.vaultContents = await this.vault.load(newPassphrase)
} }
}
async disableVault () { async disableVault () {
if ((await this.platform.showMessageBox( if ((await this.platform.showMessageBox(
@@ -67,11 +65,9 @@ export class VaultSettingsTabComponent extends BaseComponent {
return return
} }
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent) const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
const newPassphrase = await modal.result.catch(() => null) const newPassphrase = await modal.result
if (newPassphrase) {
this.vault.save(this.vaultContents, newPassphrase) this.vault.save(this.vaultContents, newPassphrase)
} }
}
async toggleConfigEncrypted () { async toggleConfigEncrypted () {
this.config.store.encrypted = !this.config.store.encrypted this.config.store.encrypted = !this.config.store.encrypted
@@ -122,7 +118,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
modal.componentInstance.prompt = this.translate.instant('New name') modal.componentInstance.prompt = this.translate.instant('New name')
modal.componentInstance.value = secret.key.description modal.componentInstance.value = secret.key.description
const description = (await modal.result.catch(() => null))?.value const description = (await modal.result)?.value
if (!description) { if (!description) {
return return
} }

View File

@@ -20,7 +20,6 @@ export class SettingsConfigProvider extends ConfigProvider {
}, },
}, },
} }
platformDefaults = { platformDefaults = {
[Platform.macOS]: { [Platform.macOS]: {
hotkeys: { hotkeys: {

View File

@@ -7,7 +7,6 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core' import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
import { EditProfileModalComponent } from './components/editProfileModal.component' import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component' import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component' import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
@@ -49,7 +48,6 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
], ],
declarations: [ declarations: [
EditProfileModalComponent, EditProfileModalComponent,
EditProfileGroupModalComponent,
HotkeyInputModalComponent, HotkeyInputModalComponent,
HotkeySettingsTabComponent, HotkeySettingsTabComponent,
MultiHotkeyInputComponent, MultiHotkeyInputComponent,

View File

@@ -25,7 +25,6 @@
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/ssh2": "^0.5.46", "@types/ssh2": "^0.5.46",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"diffie-hellman": "^5.0.3",
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136", "sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
"strip-ansi": "^7.0.0" "strip-ansi": "^7.0.0"
}, },

View File

@@ -1,4 +1,4 @@
import { ConnectableTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal' import { BaseTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
export enum SSHAlgorithmType { export enum SSHAlgorithmType {
HMAC = 'hmac', HMAC = 'hmac',
@@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
HOSTKEY = 'serverHostKey', HOSTKEY = 'serverHostKey',
} }
export interface SSHProfile extends ConnectableTerminalProfile { export interface SSHProfile extends BaseTerminalProfile {
options: SSHProfileOptions options: SSHProfileOptions
} }

View File

@@ -18,7 +18,6 @@ export class SFTPCreateDirectoryModalComponent extends BaseComponent {
create (): void { create (): void {
this.modalInstance.close(this.directoryName) this.modalInstance.close(this.directoryName)
} }
cancel (): void { cancel (): void {
this.modalInstance.close('') this.modalInstance.close('')
} }

View File

@@ -113,8 +113,8 @@ export class SFTPPanelComponent {
async openCreateDirectoryModal (): Promise<void> { async openCreateDirectoryModal (): Promise<void> {
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent) const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
const directoryName = await modal.result.catch(() => null) const directoryName = await modal.result
if (directoryName?.trim()) { if (directoryName !== '') {
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => { this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
this.notifications.notice('The directory was created successfully') this.notifications.notice('The directory was created successfully')
this.navigate(path.join(this.path, directoryName)) this.navigate(path.join(this.path, directoryName))

View File

@@ -75,7 +75,7 @@ export class SSHProfileSettingsComponent {
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}` modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
modal.componentInstance.password = true modal.componentInstance.password = true
try { try {
const result = await modal.result.catch(() => null) const result = await modal.result
if (result?.value) { if (result?.value) {
this.passwordStorage.savePassword(this.profile, result.value) this.passwordStorage.savePassword(this.profile, result.value)
this.hasSavedPassword = true this.hasSavedPassword = true
@@ -89,14 +89,12 @@ export class SSHProfileSettingsComponent {
} }
async addPrivateKey () { async addPrivateKey () {
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null) const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`)
if (ref) {
this.profile.options.privateKeys = [ this.profile.options.privateKeys = [
...this.profile.options.privateKeys!, ...this.profile.options.privateKeys!,
ref, ref,
] ]
} }
}
removePrivateKey (path: string) { removePrivateKey (path: string) {
this.profile.options.privateKeys = this.profile.options.privateKeys?.filter(x => x !== path) this.profile.options.privateKeys = this.profile.options.privateKeys?.filter(x => x !== path)

View File

@@ -61,4 +61,12 @@ h3 SSH
(ngModelChange)='config.save()' (ngModelChange)='config.save()'
) )
.form-line
.header
.title(translate) Clear terminal after connection
toggle(
[(ngModel)]='config.store.ssh.clearServiceMessagesOnConnect',
(ngModelChange)='config.save()',
)
.alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab .alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab

View File

@@ -83,7 +83,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
const jumpSession = await this.setupOneSession( const jumpSession = await this.setupOneSession(
this.injector, this.injector,
this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection), this.profilesService.getConfigProxyForProfile(jumpConnection),
) )
jumpSession.ref() jumpSession.ref()
@@ -163,6 +163,10 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
await session.start() await session.start()
if (this.config.store.ssh.clearServiceMessagesOnConnect) {
this.frontend?.clear()
}
this.session?.resize(this.size.columns, this.size.rows) this.session?.resize(this.size.columns, this.size.rows)
} }

View File

@@ -11,6 +11,7 @@ export class SSHConfigProvider extends ConfigProvider {
x11Display: null, x11Display: null,
knownHosts: [], knownHosts: [],
verifyHostKeys: true, verifyHostKeys: true,
clearServiceMessagesOnConnect: true,
}, },
hotkeys: { hotkeys: {
'restart-ssh-session': [], 'restart-ssh-session': [],

View File

@@ -1,5 +1,3 @@
import './polyfills'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'

View File

@@ -1,4 +0,0 @@
const nodeCrypto = require('crypto')
const browserDH = require('diffie-hellman/browser')
nodeCrypto.createDiffieHellmanGroup = browserDH.createDiffieHellmanGroup
nodeCrypto.createDiffieHellman = browserDH.createDiffieHellman

View File

@@ -1,5 +1,5 @@
import { Injectable, InjectFlags, Injector } from '@angular/core' import { Injectable, InjectFlags, Injector } from '@angular/core'
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core' import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants' import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component' import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
import { SSHTabComponent } from './components/sshTab.component' import { SSHTabComponent } from './components/sshTab.component'
@@ -8,9 +8,10 @@ import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
import { SSHProfileImporter } from './api/importer' import { SSHProfileImporter } from './api/importer'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> { export class SSHProfilesService extends ProfileProvider<SSHProfile> {
id = 'ssh' id = 'ssh'
name = 'SSH' name = 'SSH'
supportsQuickConnect = true
settingsComponent = SSHProfileSettingsComponent settingsComponent = SSHProfileSettingsComponent
configDefaults = { configDefaults = {
options: { options: {
@@ -44,7 +45,6 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
reuseSession: true, reuseSession: true,
input: { backspace: 'backspace' }, input: { backspace: 'backspace' },
}, },
clearServiceMessagesOnConnect: true,
} }
constructor ( constructor (

View File

@@ -34,7 +34,7 @@ export class SSHMultiplexerService {
if (!jumpConnection) { if (!jumpConnection) {
return key return key
} }
const jumpProfile = this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection) const jumpProfile = this.profilesService.getConfigProxyForProfile(jumpConnection)
key += '$' + await this.getMultiplexerKey(jumpProfile) key += '$' + await this.getMultiplexerKey(jumpProfile)
} }
return key return key

View File

@@ -1,4 +1,5 @@
import * as C from 'constants' import * as C from 'constants'
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { posix as posixPath } from 'path' import { posix as posixPath } from 'path'
import { Injector, NgZone } from '@angular/core' import { Injector, NgZone } from '@angular/core'

View File

@@ -1,5 +1,6 @@
import * as fs from 'mz/fs' import * as fs from 'mz/fs'
import * as crypto from 'crypto' import * as crypto from 'crypto'
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
import * as sshpk from 'sshpk' import * as sshpk from 'sshpk'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
@@ -209,6 +210,7 @@ export class SSHSession {
if (!await this.verifyHostKey(handshake)) { if (!await this.verifyHostKey(handshake)) {
this.ssh.end() this.ssh.end()
reject(new Error('Host key verification failed')) reject(new Error('Host key verification failed'))
return
} }
this.logger.info('Handshake complete:', handshake) this.logger.info('Handshake complete:', handshake)
resolve() resolve()
@@ -298,7 +300,7 @@ export class SSHSession {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Username for ${this.profile.options.host}` modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
try { try {
const result = await modal.result.catch(() => null) const result = await modal.result
this.authUsername = result?.value ?? null this.authUsername = result?.value ?? null
} catch { } catch {
this.authUsername = 'root' this.authUsername = 'root'
@@ -426,7 +428,11 @@ export class SSHSession {
const modal = this.ngbModal.open(HostKeyPromptModalComponent) const modal = this.ngbModal.open(HostKeyPromptModalComponent)
modal.componentInstance.selector = selector modal.componentInstance.selector = selector
modal.componentInstance.digest = this.hostKeyDigest modal.componentInstance.digest = this.hostKeyDigest
return modal.result.catch(() => false) try {
return await modal.result
} catch {
return false
}
} }
return true return true
} }
@@ -489,7 +495,7 @@ export class SSHSession {
modal.componentInstance.showRememberCheckbox = true modal.componentInstance.showRememberCheckbox = true
try { try {
const result = await modal.result.catch(() => null) const result = await modal.result
if (result) { if (result) {
if (result.remember) { if (result.remember) {
this.savedPassword = result.value this.savedPassword = result.value

View File

@@ -7,7 +7,7 @@ export class X11Socket {
static resolveDisplaySpec (spec?: string|null): SocketConnectOpts { static resolveDisplaySpec (spec?: string|null): SocketConnectOpts {
// eslint-disable-next-line prefer-const, @typescript-eslint/no-unused-vars // eslint-disable-next-line prefer-const, @typescript-eslint/no-unused-vars
let [_, xHost, xDisplay] = /^(.+):(\d+)(?:.(\d+))$/.exec(spec ?? process.env.DISPLAY ?? 'localhost:0') ?? [undefined, undefined, undefined] let [_, xHost, xDisplay] = /^(.+):(\d+)(?:.(\d+))$/.exec(spec ?? process.env.DISPLAY ?? 'localhost:0') ?? []
if (process.platform === 'win32') { if (process.platform === 'win32') {
xHost ??= 'localhost' xHost ??= 'localhost'
} else { } else {
@@ -18,7 +18,7 @@ export class X11Socket {
xHost = spec xHost = spec
} }
const display = parseInt(xDisplay ?? '0') const display = parseInt(xDisplay || '0')
const port = display < 100 ? display + 6000 : display const port = display < 100 ? display + 6000 : display
if (xHost === 'unix') { if (xHost === 'unix') {

View File

@@ -53,6 +53,6 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
const modal = this.ngbModal.open(SFTPDeleteModalComponent) const modal = this.ngbModal.open(SFTPDeleteModalComponent)
modal.componentInstance.item = item modal.componentInstance.item = item
modal.componentInstance.sftp = session modal.componentInstance.sftp = session
await modal.result.catch(() => null) await modal.result
} }
} }

View File

@@ -68,11 +68,6 @@ bcrypt-pbkdf@^1.0.0:
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
bn.js@^4.0.0, bn.js@^4.1.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -81,11 +76,6 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
cli@0.4.x: cli@0.4.x:
version "0.4.5" version "0.4.5"
resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61" resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61"
@@ -129,15 +119,6 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
diffie-hellman@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
dependencies:
bn.js "^4.1.0"
miller-rabin "^4.0.0"
randombytes "^2.0.0"
ecc-jsbn@~0.1.1: ecc-jsbn@~0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -219,14 +200,6 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
dependencies:
bn.js "^4.0.0"
brorand "^1.0.1"
minimatch@^3.0.4: minimatch@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -258,13 +231,6 @@ pkginfo@0.3.x:
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
randombytes@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
dependencies:
safe-buffer "^5.1.0"
rimraf@^3.0.0: rimraf@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -277,11 +243,6 @@ run-script-os@^1.1.3:
resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347" resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347"
integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw== integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
safe-buffer@^5.1.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core' import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component' import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
import { TelnetTabComponent } from './components/telnetTab.component' import { TelnetTabComponent } from './components/telnetTab.component'
import { TelnetProfile } from './session' import { TelnetProfile } from './session'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetProfile> { export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
id = 'telnet' id = 'telnet'
name = 'Telnet' name = 'Telnet'
supportsQuickConnect = true supportsQuickConnect = true
@@ -21,7 +21,6 @@ export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetPro
scripts: [], scripts: [],
input: { backspace: 'backspace' }, input: { backspace: 'backspace' },
}, },
clearServiceMessagesOnConnect: false,
} }
constructor (private translate: TranslateService) { super() } constructor (private translate: TranslateService) { super() }
@@ -96,12 +95,4 @@ export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetPro
}, },
} }
} }
intoQuickConnectString (profile: TelnetProfile): string | null {
let s = profile.options.host
if (profile.options.port !== 23) {
s = `${s}:${profile.options.port}`
}
return s
}
} }

View File

@@ -1,14 +1,13 @@
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
import { Socket } from 'net' import { Socket } from 'net'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import { Injector } from '@angular/core' import { Injector } from '@angular/core'
import { LogService } from 'tabby-core' import { LogService } from 'tabby-core'
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
export interface TelnetProfile extends ConnectableTerminalProfile { export interface TelnetProfile extends BaseTerminalProfile {
options: TelnetProfileOptions options: TelnetProfileOptions
} }

View File

@@ -1,4 +1,4 @@
import { Observable, Subject, first, auditTime, debounce, interval } from 'rxjs' import { Observable, Subject, first, auditTime } from 'rxjs'
import { Spinner } from 'cli-spinner' import { Spinner } from 'cli-spinner'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags, Component } from '@angular/core' import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags, Component } from '@angular/core'
@@ -14,9 +14,6 @@ import { TerminalDecorator } from './decorator'
import { SearchPanelComponent } from '../components/searchPanel.component' import { SearchPanelComponent } from '../components/searchPanel.component'
import { MultifocusService } from '../services/multifocus.service' import { MultifocusService } from '../services/multifocus.service'
const INACTIVE_TAB_UNLOAD_DELAY = 1000 * 30
/** /**
* A class to base your custom terminal tabs on * A class to base your custom terminal tabs on
*/ */
@@ -150,13 +147,11 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
}, },
}, },
}) })
private spinnerActive = false private spinnerActive = false
private spinnerPaused = false private spinnerPaused = false
private toolbarRevealTimeout = new ResettableTimeout(() => { private toolbarRevealTimeout = new ResettableTimeout(() => {
this.revealToolbar = false this.revealToolbar = false
}, 1000) }, 1000)
private frontendWriteLock = Promise.resolve() private frontendWriteLock = Promise.resolve()
get input$ (): Observable<Buffer> { get input$ (): Observable<Buffer> {
@@ -413,24 +408,6 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
this.blurred$.subscribe(() => { this.blurred$.subscribe(() => {
this.multifocus.cancel() this.multifocus.cancel()
}) })
this.visibility$
.pipe(debounce(visibility => interval(visibility ? 0 : INACTIVE_TAB_UNLOAD_DELAY)))
.subscribe(visibility => {
if (this.frontend instanceof XTermFrontend) {
if (visibility) {
// this.frontend.resizeHandler()
const term = this.frontend.xterm as any
term._core._renderService.clear()
term._core._renderService.handleResize(term.cols, term.rows)
} else {
this.frontend.xterm.element?.querySelectorAll('canvas').forEach(c => {
c.height = c.width = 0
c.style.height = c.style.width = '0px'
})
}
}
})
} }
protected onFrontendReady (): void { protected onFrontendReady (): void {

View File

@@ -4,7 +4,7 @@ import { Injector, Component } from '@angular/core'
import { first } from 'rxjs' import { first } from 'rxjs'
import { ConnectableTerminalProfile } from './interfaces' import { BaseTerminalProfile } from './interfaces'
import { BaseTerminalTabComponent } from './baseTerminalTab.component' import { BaseTerminalTabComponent } from './baseTerminalTab.component'
import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core' import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
@@ -13,7 +13,7 @@ import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
* A class to base your custom connectable terminal tabs on * A class to base your custom connectable terminal tabs on
*/ */
@Component({ template: '' }) @Component({ template: '' })
export abstract class ConnectableTerminalTabComponent<P extends ConnectableTerminalProfile> extends BaseTerminalTabComponent<P> { export abstract class ConnectableTerminalTabComponent<P extends BaseTerminalProfile> extends BaseTerminalTabComponent<P> {
protected reconnectOffered = false protected reconnectOffered = false
protected isDisconnectedByHand = false protected isDisconnectedByHand = false
@@ -57,9 +57,6 @@ export abstract class ConnectableTerminalTabComponent<P extends ConnectableTermi
async initializeSession (): Promise<void> { async initializeSession (): Promise<void> {
this.reconnectOffered = false this.reconnectOffered = false
this.isDisconnectedByHand = false this.isDisconnectedByHand = false
if (this.profile.clearServiceMessagesOnConnect) {
this.frontend?.clear()
}
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { ConnectableProfile, Profile } from 'tabby-core' import { Profile } from 'tabby-core'
export interface ResizeEvent { export interface ResizeEvent {
columns: number columns: number
@@ -19,5 +19,3 @@ export interface TerminalColorScheme {
export interface BaseTerminalProfile extends Profile { export interface BaseTerminalProfile extends Profile {
terminalColorScheme?: TerminalColorScheme terminalColorScheme?: TerminalColorScheme
} }
export interface ConnectableTerminalProfile extends BaseTerminalProfile, ConnectableProfile {}

View File

@@ -1,47 +1,5 @@
h3.mb-3(translate) Color schemes h3.mb-3(translate) Color schemes
.form-line
.header
.title(translate) Switch color scheme
.btn-group(role='group')
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeAuto',
[value]='"auto"'
)
label.btn.btn-secondary(
for='colorSchemeModeAuto'
)
span(translate) From system
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeDark',
[value]='"dark"'
)
label.btn.btn-secondary(
for='colorSchemeModeDark'
)
span(translate) Always dark
input.btn-check(
type='radio',
name='colorSchemeMode',
[(ngModel)]='config.store.appearance.colorSchemeMode',
(ngModelChange)='config.save()',
id='colorSchemeModeLight',
[value]='"light"'
)
label.btn.btn-secondary(
for='colorSchemeModeLight'
)
span(translate) Always light
ul.nav-tabs(ngbNav, #nav='ngbNav', [activeId]='defaultTab') ul.nav-tabs(ngbNav, #nav='ngbNav', [activeId]='defaultTab')
li(ngbNavItem='dark') li(ngbNavItem='dark')
a(ngbNavLink, translate) Dark mode a(ngbNavLink, translate) Dark mode

View File

@@ -33,7 +33,6 @@ export class StreamProcessingSettingsComponent {
description: _('Send bytes by typing in hex values'), description: _('Send bytes by typing in hex values'),
}, },
] ]
outputModes = [ outputModes = [
{ {
key: null, key: null,
@@ -46,7 +45,6 @@ export class StreamProcessingSettingsComponent {
description: _('Output is shown as a hexdump'), description: _('Output is shown as a hexdump'),
}, },
] ]
newlineModes = [ newlineModes = [
{ key: null, name: _('Keep') }, { key: null, name: _('Keep') },
{ key: 'strip', name: _('Strip') }, { key: 'strip', name: _('Strip') },

View File

@@ -106,7 +106,6 @@ export class MultifocusService {
return return
} }
const tabs = currentTab.getAllTabs().filter(t => t instanceof BaseTerminalTabComponent) const tabs = currentTab.getAllTabs().filter(t => t instanceof BaseTerminalTabComponent)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
this.start(pane, tabs as any) this.start(pane, tabs as any)
} }
} }

View File

@@ -175,7 +175,7 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
const modal = this.ngbModal.open(PromptModalComponent) const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('New profile name') modal.componentInstance.prompt = this.translate.instant('New profile name')
modal.componentInstance.value = tab.profile.name modal.componentInstance.value = tab.profile.name
const name = (await modal.result.catch(() => null))?.value const name = (await modal.result)?.value
if (!name) { if (!name) {
return return
} }

999
yarn.lock

File diff suppressed because it is too large Load Diff