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:
import/parsers:
'@typescript-eslint/parser': ['.ts']
import/resolver:
typescript:
project:
- tsconfig.json
- tabby-*/tsconfig.json
typescript: true
node: true
env:
browser: true
es6: true
@@ -34,7 +28,7 @@ overrides:
- plugin:import/typescript
plugins:
- '@typescript-eslint'
- import
- 'import'
rules:
'@typescript-eslint/semi':
- error
@@ -136,7 +130,6 @@ overrides:
'@typescript-eslint/naming-convention': off
'@typescript-eslint/lines-between-class-members':
- error
- always
- exceptAfterSingleLine: true
'@typescript-eslint/dot-notation': off
'@typescript-eslint/no-implicit-any-catch': off
@@ -159,6 +152,3 @@ overrides:
'@typescript-eslint/consistent-generic-constructors': off
'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 {
if (!!this.tray || process.platform === 'linux') {
if (this.tray || process.platform === 'linux') {
return
}
if (process.platform === 'darwin') {

View File

@@ -31,7 +31,7 @@
"npm": "6",
"rxjs": "^7.5.7",
"source-map-support": "^0.5.20",
"v8-compile-cache": "^2.3.0",
"v8-compile-cache": "^2.4.0",
"yargs": "^17.7.2"
},
"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"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
v8-compile-cache@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
v8-compile-cache@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128"
integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==
validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
version "3.0.4"

View File

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

View File

@@ -16,7 +16,7 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow'
export { HostAppService, Platform } from './hostApp'
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 * from './commands'

View File

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

View File

@@ -21,10 +21,6 @@ export interface Profile {
isTemplate: boolean
}
export interface ConnectableProfile extends Profile {
clearServiceMessagesOnConnect: boolean
}
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
[K in keyof T]?: T[K]
}, '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> {
profile: P
save?: () => void
@@ -58,6 +39,7 @@ export interface ProfileSettingsComponent<P extends Profile> {
export abstract class ProfileProvider<P extends Profile> {
id: string
name: string
supportsQuickConnect = false
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
configDefaults = {}
@@ -71,15 +53,13 @@ export abstract class ProfileProvider<P extends Profile> {
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 { }
}
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 () {
const profile = await this.profilesService.showProfileSelector().catch(() => null)
const profile = await this.profilesService.showProfileSelector()
if (profile) {
this.profilesService.launchProfile(profile)
}

View File

@@ -35,7 +35,8 @@ title-bar(
[@animateTab]='{value: "in", params: {size: targetTabSize}}',
[@.disabled]='hasVerticalTabs() || !config.store.accessibility.animations',
(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
@@ -64,7 +65,7 @@ title-bar(
(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
.d-flex(

View File

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

View File

@@ -29,36 +29,34 @@ export class SelectorModalComponent<T> {
}
@HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void {
if (event.key === 'Escape') {
if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) {
this.selectedIndex -= 10
event.preventDefault()
} else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) {
this.selectedIndex += 10
event.preventDefault()
} else if (event.key === 'ArrowUp') {
this.selectedIndex--
event.preventDefault()
} else if (event.key === 'ArrowDown') {
this.selectedIndex++
event.preventDefault()
} else if (event.key === 'Enter') {
this.selectOption(this.filteredOptions[this.selectedIndex])
} else if (event.key === 'Escape') {
this.close()
} else if (this.filteredOptions.length > 0) {
if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) {
this.selectedIndex -= Math.min(10, Math.max(1, this.selectedIndex))
event.preventDefault()
} else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) {
this.selectedIndex += Math.min(10, Math.max(1, this.filteredOptions.length - this.selectedIndex - 1))
event.preventDefault()
} else if (event.key === 'ArrowUp') {
this.selectedIndex--
event.preventDefault()
} else if (event.key === 'ArrowDown') {
this.selectedIndex++
event.preventDefault()
} else if (event.key === 'Enter') {
this.selectOption(this.filteredOptions[this.selectedIndex])
} else if (event.key === 'Backspace' && this.canEditSelected()) {
event.preventDefault()
this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
this.onFilterChange()
}
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
if (event.key === 'Backspace' && this.canEditSelected()) {
event.preventDefault()
this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent!
this.onFilterChange()
}
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
onFilterChange (): void {
@@ -78,7 +76,7 @@ export class SelectorModalComponent<T> {
{ sort: true },
).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)) {
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.visibility$.subscribe(visibility => this.getAllTabs().forEach(x => x.emitVisibility(visibility)))
this.tabAdded$.subscribe(() => this.updateTitle())
this.tabRemoved$.subscribe(() => this.updateTitle())

View File

@@ -1,10 +1,10 @@
.container.mt-3.mb-3
.mb-3
.container.mt-5.mb-5
.mb-4
.tabby-logo
h1.tabby-title Tabby
sup α
.text-center.mb-3(translate) Thank you for downloading Tabby!
.text-center.mb-5(translate) Thank you for downloading Tabby!
.form-line
.header
@@ -16,54 +16,13 @@
*ngFor='let lang of allLanguages'
) {{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
.header
.title(translate) Enable analytics
.description(translate) Help track the number of Tabby installs across the world!
toggle([(ngModel)]='config.store.enableAnalytics')
.form-line
.header
.title(translate) Enable global hotkey (Ctrl-Space)

View File

@@ -6,8 +6,3 @@
max-height: 100%;
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.Web]: require('./configDefaults.web.yaml').default,
}
defaults = require('./configDefaults.yaml').default
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { ProfilesService } from './services/profiles.service'
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
import { PartialProfile, Profile } from './api'
/** @hidden */
@Injectable()
@@ -267,7 +268,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
return [
...this.hotkeys,
...profiles.map(profile => ({
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`,
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
})),
...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 { 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 { ConfigService } from './services/config.service'
@@ -177,7 +177,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (hotkey.startsWith('profile.')) {
const id = hotkey.substring(hotkey.indexOf('.') + 1)
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) {
profilesService.openNewTabForProfile(profile)
}
@@ -188,10 +188,10 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (!provider) {
return
}
this.showSelector(provider).catch(() => null)
this.showSelector(provider)
}
if (hotkey === 'command-selector') {
commands.showSelector().catch(() => null)
commands.showSelector()
}
if (hotkey === 'profile-selector') {
@@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
callback: () => this.profilesService.openNewTabForProfile(p),
}))
if (provider instanceof QuickConnectProfileProvider) {
if (provider.supportsQuickConnect) {
options.push({
name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'),

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ import { PlatformService } from '../api/platform'
import { HostAppService } from '../api/hostApp'
import { Vault, VaultService } from './vault.service'
import { serializeFunction } from '../utils'
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
const deepmerge = require('deepmerge')
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -365,47 +364,6 @@ export class ConfigService {
}
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) {

View File

@@ -13,9 +13,8 @@ export class FileProvidersService {
) { }
async selectAndStoreFile (description: string): Promise<string> {
return this.selectProvider().then(p => {
return p.selectAndStoreFile(description)
})
const p = await this.selectProvider()
return p.selectAndStoreFile(description)
}
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 { NewTabParameters } from './tabs.service'
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 { AppService } from './app.service'
import { configMerge, ConfigProxy, ConfigService } from './config.service'
import { NotificationsService } from './notifications.service'
import { SelectorService } from './selector.service'
import deepClone from 'clone-deep'
import { v4 as uuidv4 } from 'uuid'
import slugify from 'slugify'
@Injectable({ providedIn: 'root' })
export class ProfilesService {
@@ -39,126 +36,6 @@ export class ProfilesService {
@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> {
const params = await this.newTabParametersForProfile(profile)
if (params) {
@@ -186,40 +63,52 @@ export class ProfilesService {
return params
}
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
await this.openNewTabForProfile(profile)
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)
getProviders (): ProfileProvider<Profile>[] {
return [...this.profileProviders]
}
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
return (profile.id ?? profile.name).replace(/\./g, '-')
async getProfiles (): Promise<PartialProfile<Profile>[]> {
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
}
/*
* Methods used to interract with Profile Selector
*/
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
}
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
const fullProfile = this.getConfigProxyForProfile(profile)
const provider = this.providerForProfile(fullProfile)
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
return {
...profile,
group: this.resolveProfileGroupName(profile.group ?? ''),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
group: profile.group || '',
freeInputEquivalent,
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> {
if (this.selector.active) {
return Promise.resolve(null)
@@ -229,12 +118,12 @@ export class ProfilesService {
try {
const recentProfiles = this.getRecentProfiles()
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
...this.selectorOptionForProfile(p),
group: this.translate.instant('Recent'),
icon: 'fas fa-history',
color: p.color,
weight: i - (recentProfiles.length + 1),
weight: -2,
callback: async () => {
if (p.id) {
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
@@ -288,38 +177,30 @@ export class ProfilesService {
})
} catch { }
this.getProviders().forEach(provider => {
if (provider instanceof QuickConnectProfileProvider) {
options.push({
name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'),
description: `(${provider.name.toUpperCase()})`,
icon: 'fas fa-arrow-right',
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
callback: query => {
const profile = provider.quickConnect(query)
resolve(profile)
},
})
}
this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
options.push({
name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'),
description: `(${provider.name.toUpperCase()})`,
icon: 'fas fa-arrow-right',
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
callback: query => {
const profile = provider.quickConnect(query)
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) {
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> {
for (const provider of this.getProviders()) {
if (provider instanceof QuickConnectProfileProvider) {
if (provider.supportsQuickConnect) {
const profile = provider.quickConnect(query)
if (profile) {
return profile
@@ -330,178 +211,27 @@ export class ProfilesService {
return null
}
/*
* 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[] {
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
const provider = this.providerForProfile(profile)
return [
const defaults = [
this.profileDefaults,
provider?.configDefaults ?? {},
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {},
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {},
]
!provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
].reduce(configMerge, {})
return new ConfigProxy(profile, defaults) as unknown as T
}
/*
* Methods used to interract with ProfileGroup
*/
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
await this.openNewTabForProfile(profile)
/**
* Synchronously return an Array of the existing ProfileGroups
* Does not return builtin groups
*/
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] {
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)
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 {
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) {
delete profile.group
}
recentProfiles = []
}
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 { ConfigService } from '../services/config.service'
import { Theme } from '../api/theme'
import { PlatformService, PlatformTheme } from '../api/platform'
import { PlatformService } from '../api/platform'
import { NewTheme } from '../theme'
@Injectable({ providedIn: 'root' })
@@ -194,14 +194,7 @@ export class ThemesService {
/// @hidden
_getActiveColorScheme (): any {
let theme: PlatformTheme = 'dark'
if (this.config.store.appearance.colorSchemeMode === 'light') {
theme = 'light'
} else if (this.config.store.appearance.colorSchemeMode === 'auto') {
theme = this.platform.getTheme()
}
if (theme === 'light') {
if (this.platform.getTheme() === 'light') {
return this.config.store.terminal.lightColorScheme
} else {
return this.config.store.terminal.colorScheme

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
import { LogService, NotificationsService } from 'tabby-core'
import { Subject, Observable } from 'rxjs'
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'
export interface SerialProfile extends ConnectableTerminalProfile {
export interface SerialProfile extends BaseTerminalProfile {
options: SerialProfileOptions
}

View File

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

View File

@@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = this.translate.instant('Name for the new config')
modal.componentInstance.value = name
name = (await modal.result.catch(() => null))?.value
name = (await modal.result)?.value
if (!name) {
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}}
.modal-header(*ngIf='defaultsMode !== "disabled"')
.modal-header(*ngIf='defaultsMode')
h3.m-0(
translate='Defaults for {type}',
[translateParams]='{type: profileProvider.name}'
@@ -10,7 +10,7 @@
.modal-body
.row
.col-12.col-lg-4
.mb-3(*ngIf='defaultsMode === "disabled"')
.mb-3(*ngIf='!defaultsMode')
label(translate) Name
input.form-control(
type='text',
@@ -18,20 +18,17 @@
[(ngModel)]='profile.name',
)
.mb-3(*ngIf='defaultsMode === "disabled"')
.mb-3(*ngIf='!defaultsMode')
label(translate) Group
input.form-control(
type='text',
alwaysVisibleTypeahead,
placeholder='Ungrouped',
[(ngModel)]='profileGroup',
[(ngModel)]='profile.group',
[ngbTypeahead]='groupTypeahead',
[inputFormatter]="groupFormatter",
[resultFormatter]="groupFormatter",
[editable]="false"
)
.mb-3(*ngIf='defaultsMode === "disabled"')
.mb-3(*ngIf='!defaultsMode')
label(translate) Icon
.input-group
input.form-control(
@@ -77,15 +74,9 @@
)
option(ngValue='auto', translate) Auto
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
.form-line(*ngIf='isConnectable()')
.header
.title(translate) Clear terminal after connection
toggle(
[(ngModel)]='profile.clearServiceMessagesOnConnect',
)
.mb-4
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')

View File

@@ -2,7 +2,7 @@
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
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 iconsClassList = Object.keys(iconsData).map(
@@ -19,9 +19,8 @@ export class EditProfileModalComponent<P extends Profile> {
@Input() profile: P & ConfigProxy
@Input() profileProvider: ProfileProvider<P>
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
groups: PartialProfileGroup<ProfileGroup>[]
@Input() defaultsMode = false
groupNames: string[]
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
private _profile: Profile
@@ -31,14 +30,14 @@ export class EditProfileModalComponent<P extends Profile> {
private injector: Injector,
private componentFactoryResolver: ComponentFactoryResolver,
private profilesService: ProfilesService,
config: ConfigService,
private modalInstance: NgbActiveModal,
) {
if (this.defaultsMode === 'disabled') {
this.profilesService.getProfileGroups().then(groups => {
this.groups = groups
this.profileGroup = groups.find(g => g.id === this.profile.group)
})
}
this.groupNames = [...new Set(
(config.store.profiles as Profile[])
.map(x => x.group)
.filter(x => !!x),
)].sort() as string[]
}
colorsAutocomplete = text$ => text$.pipe(
@@ -57,7 +56,7 @@ export class EditProfileModalComponent<P extends Profile> {
ngOnInit () {
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 () {
@@ -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(
debounceTime(200),
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>) =>
text$.pipe(
debounceTime(200),
@@ -89,12 +86,7 @@ export class EditProfileModalComponent<P extends Profile> {
)
save () {
if (!this.profileGroup) {
this.profile.group = undefined
} else {
this.profile.group = this.profileGroup.id
}
this.profile.group ||= undefined
this.settingsComponentInstance?.save?.()
this.profile.__cleanup()
this.modalInstance.close(this._profile)
@@ -103,9 +95,4 @@ export class EditProfileModalComponent<P extends Profile> {
cancel () {
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
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3
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
span(translate) New profile
button(ngbDropdownItem, (click)='newProfileGroup()')
i.fas.fa-fw.fa-plus
span(translate) New profile Group
button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
i.fas.fa-fw.fa-plus
span(translate) New profile
.list-group.mt-3.mb-3
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(
(click)='toggleGroupCollapse(group)'
)
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0')
.fa.fa-fw.fa-chevron-down(*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')
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); editProfileGroup(group)'
(click)='$event.stopPropagation(); editGroup(group)'
)
i.fas.fa-pencil-alt
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
*ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); deleteProfileGroup(group)'
(click)='$event.stopPropagation(); deleteGroup(group)'
)
i.fas.fa-trash-alt
ng-container(*ngIf='!group.collapsed')
@@ -75,7 +67,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.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
.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
.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)',
*ngFor='let provider of profileProviders'
) {{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')

View File

@@ -1,27 +1,32 @@
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 { Component, Inject } from '@angular/core'
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 { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
interface ProfileGroup {
name?: string
profiles: PartialProfile<Profile>[]
editable: boolean
collapsed: boolean
}
_('Filter')
_('Ungrouped')
interface CollapsableProfileGroup extends ProfileGroup {
collapsed: boolean
}
/** @hidden */
@Component({
templateUrl: './profilesSettingsTab.component.pug',
styleUrls: ['./profilesSettingsTab.component.scss'],
})
export class ProfilesSettingsTabComponent extends BaseComponent {
profiles: PartialProfile<Profile>[] = []
builtinProfiles: PartialProfile<Profile>[] = []
templateProfiles: PartialProfile<Profile>[] = []
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[]
profileGroups: ProfileGroup[]
filter = ''
Platform = Platform
@@ -54,7 +59,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
if (!base) {
let profiles = await this.profilesService.getProfiles()
let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
base = await this.selector.show(
@@ -62,13 +67,10 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
profiles.map(p => ({
icon: p.icon,
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,
})),
).catch(() => undefined)
if (!base) {
return
}
)
}
const profile: PartialProfile<Profile> = deepClone(base)
delete profile.id
@@ -88,7 +90,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
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()
}
@@ -98,7 +101,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
return
}
Object.assign(profile, result)
await this.profilesService.writeProfile(profile)
await this.config.save()
}
@@ -142,80 +144,69 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1,
},
)).response === 0) {
await this.profilesService.deleteProfile(profile)
await this.config.save()
}
}
async newProfileGroup (): Promise<void> {
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) {
this.profilesService.providerForProfile(profile)?.deleteProfile(
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)) {
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
// 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)
if (!group.defaults) {
group.defaults = {}
}
group.defaults[provider.id] = model
await this.config.save()
}
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(
{
type: 'warning',
@@ -228,8 +219,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
cancelId: 1,
},
)).response === 0) {
let deleteProfiles = false
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: this.translate.instant('Delete the group\'s profiles?'),
@@ -240,26 +230,19 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
defaultId: 0,
cancelId: 0,
},
)).response !== 0) {
deleteProfiles = true
)).response === 0) {
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()
}
}
async refresh (): Promise<void> {
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
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))
isGroupVisible (group: ProfileGroup): boolean {
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
}
isProfileVisible (profile: PartialProfile<Profile>): boolean {
@@ -287,12 +270,11 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
}
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
if (group.profiles?.length === 0) {
return
}
toggleGroupCollapse (group: ProfileGroup): void {
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> {
@@ -300,40 +282,21 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
EditProfileModalComponent,
{ size: 'lg' },
)
const model = this.profilesService.getProviderDefaults(provider)
const model = this.config.store.profileDefaults[provider.id] ?? {}
model.type = provider.id
modal.componentInstance.profile = Object.assign({}, model)
modal.componentInstance.profileProvider = provider
modal.componentInstance.defaultsMode = 'enabled'
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
delete model[k]
}
Object.assign(model, result)
this.profilesService.setProviderDefaults(provider, model)
await this.config.save()
}
}
modal.componentInstance.defaultsMode = true
const result = await modal.result
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()
// Fully replace the config
for (const k in model) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete model[k]
}
Object.assign(model, result)
this.config.store.profileDefaults[provider.id] = model
await this.config.save()
}
blacklistProfile (profile: PartialProfile<Profile>): void {
@@ -351,29 +314,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}
getQuickConnectProviders (): ProfileProvider<Profile>[] {
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider)
}
/**
* 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
return this.profileProviders.filter(x => x.supportsQuickConnect)
}
}

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,6 @@
"@types/node": "20.3.1",
"@types/ssh2": "^0.5.46",
"ansi-colors": "^4.1.1",
"diffie-hellman": "^5.0.3",
"sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
"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 {
HMAC = 'hmac',
@@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
HOSTKEY = 'serverHostKey',
}
export interface SSHProfile extends ConnectableTerminalProfile {
export interface SSHProfile extends BaseTerminalProfile {
options: SSHProfileOptions
}

View File

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

View File

@@ -113,8 +113,8 @@ export class SFTPPanelComponent {
async openCreateDirectoryModal (): Promise<void> {
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
const directoryName = await modal.result.catch(() => null)
if (directoryName?.trim()) {
const directoryName = await modal.result
if (directoryName !== '') {
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
this.notifications.notice('The directory was created successfully')
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.password = true
try {
const result = await modal.result.catch(() => null)
const result = await modal.result
if (result?.value) {
this.passwordStorage.savePassword(this.profile, result.value)
this.hasSavedPassword = true
@@ -89,13 +89,11 @@ export class SSHProfileSettingsComponent {
}
async addPrivateKey () {
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null)
if (ref) {
this.profile.options.privateKeys = [
...this.profile.options.privateKeys!,
ref,
]
}
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`)
this.profile.options.privateKeys = [
...this.profile.options.privateKeys!,
ref,
]
}
removePrivateKey (path: string) {

View File

@@ -61,4 +61,12 @@ h3 SSH
(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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ export class X11Socket {
static resolveDisplaySpec (spec?: string|null): SocketConnectOpts {
// 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') {
xHost ??= 'localhost'
} else {
@@ -18,7 +18,7 @@ export class X11Socket {
xHost = spec
}
const display = parseInt(xDisplay ?? '0')
const display = parseInt(xDisplay || '0')
const port = display < 100 ? display + 6000 : display
if (xHost === 'unix') {

View File

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

View File

@@ -68,11 +68,6 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
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:
version "1.1.11"
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"
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:
version "0.4.5"
resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61"
@@ -129,15 +119,6 @@ dashdash@^1.12.0:
dependencies:
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:
version "0.1.2"
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"
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:
version "3.0.4"
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"
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:
version "3.0.2"
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"
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:
version "2.1.2"
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 { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
import { TelnetTabComponent } from './components/telnetTab.component'
import { TelnetProfile } from './session'
@Injectable({ providedIn: 'root' })
export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetProfile> {
export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
id = 'telnet'
name = 'Telnet'
supportsQuickConnect = true
@@ -21,7 +21,6 @@ export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetPro
scripts: [],
input: { backspace: 'backspace' },
},
clearServiceMessagesOnConnect: false,
}
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 colors from 'ansi-colors'
import stripAnsi from 'strip-ansi'
import { Injector } from '@angular/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'
export interface TelnetProfile extends ConnectableTerminalProfile {
export interface TelnetProfile extends BaseTerminalProfile {
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 colors from 'ansi-colors'
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 { MultifocusService } from '../services/multifocus.service'
const INACTIVE_TAB_UNLOAD_DELAY = 1000 * 30
/**
* 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 spinnerPaused = false
private toolbarRevealTimeout = new ResettableTimeout(() => {
this.revealToolbar = false
}, 1000)
private frontendWriteLock = Promise.resolve()
get input$ (): Observable<Buffer> {
@@ -413,24 +408,6 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
this.blurred$.subscribe(() => {
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 {

View File

@@ -4,7 +4,7 @@ import { Injector, Component } from '@angular/core'
import { first } from 'rxjs'
import { ConnectableTerminalProfile } from './interfaces'
import { BaseTerminalProfile } from './interfaces'
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
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
*/
@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 isDisconnectedByHand = false
@@ -57,9 +57,6 @@ export abstract class ConnectableTerminalTabComponent<P extends ConnectableTermi
async initializeSession (): Promise<void> {
this.reconnectOffered = 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 {
columns: number
@@ -19,5 +19,3 @@ export interface TerminalColorScheme {
export interface BaseTerminalProfile extends Profile {
terminalColorScheme?: TerminalColorScheme
}
export interface ConnectableTerminalProfile extends BaseTerminalProfile, ConnectableProfile {}

View File

@@ -1,47 +1,5 @@
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')
li(ngbNavItem='dark')
a(ngbNavLink, translate) Dark mode

View File

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

View File

@@ -106,7 +106,6 @@ export class MultifocusService {
return
}
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)
}
}

View File

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

999
yarn.lock

File diff suppressed because it is too large Load Diff