mirror of
https://github.com/Eugeny/tabby.git
synced 2026-08-24 10:14:31 -05:00
clarify partial/full profile types
This commit is contained in:
@@ -21,7 +21,7 @@ export { PromptModalComponent } from '../components/promptModal.component'
|
||||
export * from './commands'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService, configMerge, ConfigProxy } from '../services/config.service'
|
||||
export { ConfigService, configMerge, ConfigProxy, ProxifiedConfig, FullyDefined } from '../services/config.service'
|
||||
export { DockingService, Screen } from '../services/docking.service'
|
||||
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
|
||||
export { HomeBaseService } from '../services/homeBase.service'
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { NewTabParameters } from '../services/tabs.service'
|
||||
import { FullyDefined } from '../services/config.service'
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
group?: string
|
||||
group: string
|
||||
options: any
|
||||
|
||||
icon?: string
|
||||
color?: string
|
||||
icon: string | null
|
||||
color: string | null
|
||||
disableDynamicTitle: boolean
|
||||
behaviorOnSessionEnd: 'auto'|'keep'|'reconnect'|'close'
|
||||
|
||||
@@ -50,16 +51,16 @@ export type PartialProfileGroup<T extends ProfileGroup> = Omit<Omit<{
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ProfileSettingsComponent<P extends Profile> {
|
||||
profile: P
|
||||
export interface ProfileSettingsComponent<P extends Profile, PP extends ProfileProvider<P>> {
|
||||
profile: FullyDefined<P>
|
||||
save?: () => void
|
||||
}
|
||||
|
||||
export abstract class ProfileProvider<P extends Profile> {
|
||||
id: string
|
||||
name: string
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||
configDefaults = {}
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P, ProfileProvider<P>>
|
||||
configDefaults: Pick<Profile, 'options'>
|
||||
|
||||
abstract getBuiltinProfiles (): Promise<PartialProfile<P>[]>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const configMergeByDefault = (a, b) => deepmerge(a, b) // eslint-disable-
|
||||
|
||||
const LATEST_VERSION = 1
|
||||
|
||||
function isStructuralMember (v) {
|
||||
function isStructuralMember (v): v is AnyRec {
|
||||
return v instanceof Object && !(v instanceof Array) &&
|
||||
Object.keys(v).length > 0 && !v.__nonStructural
|
||||
}
|
||||
@@ -30,15 +30,38 @@ function isNonStructuralObjectMember (v): boolean {
|
||||
return v instanceof Object && (v instanceof Array || v.__nonStructural)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
type AnyRec = Record<string, any>
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
type IsRecord<T> = T extends object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
? (T extends Function ? false : true)
|
||||
: false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
export type ProxifiedConfig<T extends AnyRec> = {
|
||||
[K in keyof T]:
|
||||
IsRecord<T[K]> extends true
|
||||
? ProxifiedConfig<T[K]> // structural -> nested proxy
|
||||
: T[K]; // leaf -> original type
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
export type FullyDefined<T> = T extends object
|
||||
? { [K in keyof T]-?: FullyDefined<T[K]> }
|
||||
: T
|
||||
|
||||
/** @hidden */
|
||||
export class ConfigProxy {
|
||||
constructor (real: Record<string, any>, defaults: Record<string, any>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export class ConfigProxy<T extends AnyRec> {
|
||||
constructor (real: Partial<T>, defaults: T) {
|
||||
for (const key in defaults) {
|
||||
if (isStructuralMember(defaults[key])) {
|
||||
if (!real[key]) {
|
||||
real[key] = {}
|
||||
real[key] = {} as any
|
||||
}
|
||||
const proxy = new ConfigProxy(real[key], defaults[key])
|
||||
const proxy = new ConfigProxy(real[key] as any, defaults[key])
|
||||
Object.defineProperty(
|
||||
this,
|
||||
key,
|
||||
@@ -64,7 +87,7 @@ export class ConfigProxy {
|
||||
}
|
||||
}
|
||||
|
||||
this.__getValue = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
this.__getValue = (key: keyof T) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (real[key] !== undefined) {
|
||||
return real[key]
|
||||
} else {
|
||||
@@ -78,11 +101,11 @@ export class ConfigProxy {
|
||||
}
|
||||
}
|
||||
|
||||
this.__getDefault = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
this.__getDefault = (key: keyof T) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
return deepClone(defaults[key])
|
||||
}
|
||||
|
||||
this.__setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
this.__setValue = (key: keyof T, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
|
||||
if (deepEqual(value, this.__getDefault(key))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete real[key]
|
||||
@@ -95,7 +118,7 @@ export class ConfigProxy {
|
||||
// Trigger removal of default values
|
||||
for (const key in defaults) {
|
||||
if (isStructuralMember(defaults[key])) {
|
||||
this[key].__cleanup()
|
||||
(this as any)[key].__cleanup()
|
||||
} else {
|
||||
const v = this.__getValue(key)
|
||||
this.__setValue(key, v)
|
||||
@@ -105,15 +128,18 @@ export class ConfigProxy {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__getValue (_key: string): any { }
|
||||
__getValue (_key: keyof T): any { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__setValue (_key: string, _value: any) { }
|
||||
__setValue (_key: keyof T, _value: any) { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__getDefault (_key: string): any { }
|
||||
__getDefault (_key: keyof T): any { }
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
__cleanup () { }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias, @typescript-eslint/no-redeclare
|
||||
// export type ConfigProxy<T extends AnyRec> = ProxifiedConfig<T>
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConfigService {
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, ProfileProvider } from '../api/profileProvider'
|
||||
import { SelectorOption } from '../api/selector'
|
||||
import { AppService } from './app.service'
|
||||
import { configMerge, ConfigProxy, ConfigService } from './config.service'
|
||||
import { configMerge, ConfigProxy, ConfigService, FullyDefined } from './config.service'
|
||||
import { NotificationsService } from './notifications.service'
|
||||
import { SelectorService } from './selector.service'
|
||||
import deepClone from 'clone-deep'
|
||||
@@ -14,7 +14,7 @@ import slugify from 'slugify'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProfilesService {
|
||||
private profileDefaults = {
|
||||
private profileDefaults: Profile = {
|
||||
id: '',
|
||||
type: '',
|
||||
name: '',
|
||||
@@ -26,7 +26,6 @@ export class ProfilesService {
|
||||
weight: 0,
|
||||
isBuiltin: false,
|
||||
isTemplate: false,
|
||||
terminalColorScheme: null,
|
||||
behaviorOnSessionEnd: 'auto',
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ export class ProfilesService {
|
||||
}
|
||||
|
||||
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
||||
profile = this.getConfigProxyForProfile(profile)
|
||||
profile = this.getConfigProxyForProfile(profile) as PartialProfile<P>
|
||||
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||
}
|
||||
|
||||
@@ -66,9 +65,9 @@ export class ProfilesService {
|
||||
* 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 {
|
||||
getConfigProxyForProfile <P extends Profile> (profile: PartialProfile<P>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): FullyDefined<P> & ConfigProxy<FullyDefined<P>> {
|
||||
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
return new ConfigProxy(profile, defaults) as any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +213,10 @@ export class ProfilesService {
|
||||
const provider = this.providerForProfile(fullProfile)
|
||||
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
|
||||
return {
|
||||
...profile,
|
||||
name: profile.name,
|
||||
icon: profile.icon ?? undefined,
|
||||
color: profile.color ?? undefined,
|
||||
weight: profile.weight,
|
||||
group: this.resolveProfileGroupName(profile.group ?? ''),
|
||||
freeInputEquivalent,
|
||||
description: provider?.getDescription(fullProfile),
|
||||
@@ -234,7 +236,7 @@ export class ProfilesService {
|
||||
...this.selectorOptionForProfile(p),
|
||||
group: this.translate.instant('Recent'),
|
||||
icon: 'fas fa-history',
|
||||
color: p.color,
|
||||
color: p.color ?? undefined,
|
||||
weight: i - (recentProfiles.length + 1),
|
||||
callback: async () => {
|
||||
if (p.id) {
|
||||
|
||||
@@ -32,16 +32,15 @@ export abstract class ShellProvider {
|
||||
|
||||
|
||||
export interface SessionOptions {
|
||||
restoreFromPTYID?: string
|
||||
name?: string
|
||||
restoreFromPTYID: string | null
|
||||
command: string
|
||||
args?: string[]
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
width?: number
|
||||
height?: number
|
||||
pauseAfterExit?: boolean
|
||||
runAsAdministrator?: boolean
|
||||
args: string[]
|
||||
cwd: string | null
|
||||
env: Record<string, string>
|
||||
width: number | null
|
||||
height: number | null
|
||||
pauseAfterExit: boolean
|
||||
runAsAdministrator: boolean
|
||||
}
|
||||
|
||||
export interface LocalProfile extends BaseTerminalProfile {
|
||||
|
||||
@@ -40,7 +40,7 @@ export class CommandLineEditorComponent {
|
||||
updateCommand () {
|
||||
this.command = shellQuote.quote([
|
||||
this.model.command,
|
||||
...this.model.args ?? [],
|
||||
...this.model.args,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Inject, Optional } from '@angular/core'
|
||||
import { LocalProfile, UACService } from '../api'
|
||||
import { PlatformService, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { FullyDefined, PlatformService, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { LocalProfilesService } from '../profiles'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './localProfileSettings.component.pug',
|
||||
})
|
||||
export class LocalProfileSettingsComponent implements ProfileSettingsComponent<LocalProfile> {
|
||||
profile: LocalProfile
|
||||
export class LocalProfileSettingsComponent implements ProfileSettingsComponent<LocalProfile, LocalProfilesService> {
|
||||
profile: FullyDefined<LocalProfile>
|
||||
|
||||
constructor (
|
||||
@Optional() @Inject(UACService) public uac: UACService|undefined,
|
||||
|
||||
@@ -51,13 +51,13 @@ export class LocalProfilesService extends ProfileProvider<LocalProfile> {
|
||||
|
||||
if (!profile.options.cwd) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
|
||||
profile.options.cwd = await this.app.activeTab.session.getWorkingDirectory() ?? undefined
|
||||
profile.options.cwd = await this.app.activeTab.session.getWorkingDirectory() ?? null
|
||||
}
|
||||
if (this.app.activeTab instanceof SplitTabComponent) {
|
||||
const focusedTab = this.app.activeTab.getFocusedTab()
|
||||
|
||||
if (focusedTab instanceof TerminalTabComponent && focusedTab.session) {
|
||||
profile.options.cwd = await focusedTab.session.getWorkingDirectory() ?? undefined
|
||||
profile.options.cwd = await focusedTab.session.getWorkingDirectory() ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,10 +77,11 @@ export class LocalProfilesService extends ProfileProvider<LocalProfile> {
|
||||
|
||||
optionsFromShell (shell: Shell): SessionOptions {
|
||||
return {
|
||||
...this.configDefaults.options,
|
||||
command: shell.command,
|
||||
args: shell.args ?? [],
|
||||
env: shell.env,
|
||||
cwd: shell.cwd,
|
||||
cwd: shell.cwd ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export class Session extends BaseSession {
|
||||
|
||||
if (options.restoreFromPTYID) {
|
||||
pty = await this.ptyInterface.restore(options.restoreFromPTYID)
|
||||
options.restoreFromPTYID = undefined
|
||||
options.restoreFromPTYID = null
|
||||
}
|
||||
|
||||
if (!pty) {
|
||||
@@ -74,7 +74,7 @@ export class Session extends BaseSession {
|
||||
TERM: 'xterm-256color',
|
||||
TERM_PROGRAM: 'Tabby',
|
||||
},
|
||||
substituteEnv(options.env ?? {}),
|
||||
substituteEnv(options.env),
|
||||
this.config.store.terminal.environment || {},
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ export class Session extends BaseSession {
|
||||
cwd = undefined
|
||||
}
|
||||
|
||||
pty = await this.ptyInterface.spawn(options.command, options.args ?? [], {
|
||||
pty = await this.ptyInterface.spawn(options.command, options.args, {
|
||||
name: 'xterm-256color',
|
||||
cols: options.width ?? 80,
|
||||
rows: options.height ?? 30,
|
||||
@@ -152,7 +152,7 @@ export class Session extends BaseSession {
|
||||
}
|
||||
})
|
||||
|
||||
this.pauseAfterExit = options.pauseAfterExit ?? false
|
||||
this.pauseAfterExit = options.pauseAfterExit
|
||||
|
||||
this.destroyed$.subscribe(() => this.pty!.unsubscribeAll())
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
click: async () => {
|
||||
let workingDirectory = profile.options.cwd
|
||||
if (!workingDirectory && tab instanceof TerminalTabComponent) {
|
||||
workingDirectory = await tab.session?.getWorkingDirectory() ?? undefined
|
||||
workingDirectory = await tab.session?.getWorkingDirectory() ?? null
|
||||
}
|
||||
await this.terminalService.openTab(profile, workingDirectory)
|
||||
},
|
||||
|
||||
+16
-16
@@ -12,15 +12,15 @@ export interface SerialProfile extends ConnectableTerminalProfile {
|
||||
|
||||
export interface SerialProfileOptions extends StreamProcessingOptions, LoginScriptsOptions {
|
||||
port: string
|
||||
baudrate?: number
|
||||
databits?: number
|
||||
stopbits?: number
|
||||
parity?: string
|
||||
rtscts?: boolean
|
||||
xon?: boolean
|
||||
xoff?: boolean
|
||||
xany?: boolean
|
||||
slowSend?: boolean
|
||||
baudrate: number | null
|
||||
databits: 5 | 6 | 7 | 8
|
||||
stopbits: 1 | 1.5 | 2
|
||||
parity: string
|
||||
rtscts: boolean
|
||||
xon: boolean
|
||||
xoff: boolean
|
||||
xany: boolean
|
||||
slowSend: boolean
|
||||
input: InputProcessingOptions,
|
||||
}
|
||||
|
||||
@@ -81,13 +81,13 @@ export class SerialSession extends BaseSession {
|
||||
path: this.profile.options.port,
|
||||
autoOpen: false,
|
||||
baudRate: parseInt(this.profile.options.baudrate as any),
|
||||
dataBits: this.profile.options.databits ?? 8 as any,
|
||||
stopBits: this.profile.options.stopbits ?? 1 as any,
|
||||
parity: this.profile.options.parity ?? 'none',
|
||||
rtscts: this.profile.options.rtscts ?? false,
|
||||
xon: this.profile.options.xon ?? false,
|
||||
xoff: this.profile.options.xoff ?? false,
|
||||
xany: this.profile.options.xany ?? false,
|
||||
dataBits: this.profile.options.databits,
|
||||
stopBits: this.profile.options.stopbits,
|
||||
parity: this.profile.options.parity,
|
||||
rtscts: this.profile.options.rtscts,
|
||||
xon: this.profile.options.xon,
|
||||
xoff: this.profile.options.xoff,
|
||||
xany: this.profile.options.xany,
|
||||
})
|
||||
let connected = false
|
||||
await new Promise(async (resolve, reject) => {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs'
|
||||
import { HostAppService, Platform, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { FullyDefined, HostAppService, Platform, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { SerialPortInfo, BAUD_RATES, SerialProfile } from '../api'
|
||||
import { SerialService } from '../services/serial.service'
|
||||
import { SerialProfilesService } from '../profiles'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './serialProfileSettings.component.pug',
|
||||
})
|
||||
export class SerialProfileSettingsComponent implements ProfileSettingsComponent<SerialProfile> {
|
||||
profile: SerialProfile
|
||||
export class SerialProfileSettingsComponent implements ProfileSettingsComponent<SerialProfile, SerialProfilesService> {
|
||||
profile: FullyDefined<SerialProfile>
|
||||
foundPorts: SerialPortInfo[]
|
||||
Platform = Platform
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ConfigProxy, ProfileGroup, Profile, ProfileProvider, PlatformService, T
|
||||
templateUrl: './editProfileGroupModal.component.pug',
|
||||
})
|
||||
export class EditProfileGroupModalComponent<G extends ProfileGroup> {
|
||||
@Input() group: G & ConfigProxy
|
||||
@Input() group: G & ConfigProxy<G>
|
||||
@Input() providers: ProfileProvider<Profile>[]
|
||||
|
||||
constructor (
|
||||
|
||||
@@ -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 { PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider, FullyDefined, ConfigProxy } from 'tabby-core'
|
||||
|
||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||
const iconsClassList = Object.keys(iconsData).map(
|
||||
@@ -15,17 +15,17 @@ const iconsClassList = Object.keys(iconsData).map(
|
||||
@Component({
|
||||
templateUrl: './editProfileModal.component.pug',
|
||||
})
|
||||
export class EditProfileModalComponent<P extends Profile> {
|
||||
@Input() profile: P & ConfigProxy
|
||||
@Input() profileProvider: ProfileProvider<P>
|
||||
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
|
||||
export class EditProfileModalComponent<P extends Profile, PP extends ProfileProvider<P>> {
|
||||
@Input('profile') _profile: P
|
||||
@Input() profileProvider: PP
|
||||
@Input() settingsComponent: new () => ProfileSettingsComponent<P, PP>
|
||||
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
|
||||
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
|
||||
groups: PartialProfileGroup<ProfileGroup>[]
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
private _profile: Profile
|
||||
private settingsComponentInstance?: ProfileSettingsComponent<P>
|
||||
protected profile: FullyDefined<P> & ConfigProxy<FullyDefined<P>>
|
||||
private settingsComponentInstance?: ProfileSettingsComponent<P, PP>
|
||||
|
||||
constructor (
|
||||
private injector: Injector,
|
||||
@@ -56,8 +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<P>(this._profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' })
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
@@ -90,7 +89,7 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
|
||||
save () {
|
||||
if (!this.profileGroup) {
|
||||
this.profile.group = undefined
|
||||
this.profile.group = ''
|
||||
} else {
|
||||
this.profile.group = this.profileGroup.id
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
base = await this.selector.show(
|
||||
this.translate.instant('Select a base profile to use as a template'),
|
||||
profiles.map(p => ({
|
||||
icon: p.icon,
|
||||
icon: p.icon ?? undefined,
|
||||
description: this.profilesService.getDescription(p) ?? undefined,
|
||||
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name,
|
||||
result: p,
|
||||
|
||||
@@ -17,25 +17,25 @@ export interface SSHProfileOptions extends LoginScriptsOptions {
|
||||
host: string
|
||||
port?: number
|
||||
user: string
|
||||
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
|
||||
password?: string
|
||||
privateKeys?: string[]
|
||||
keepaliveInterval?: number
|
||||
keepaliveCountMax?: number
|
||||
readyTimeout?: number
|
||||
x11?: boolean
|
||||
skipBanner?: boolean
|
||||
jumpHost?: string
|
||||
agentForward?: boolean
|
||||
warnOnClose?: boolean
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
forwardedPorts?: ForwardedPortConfig[]
|
||||
socksProxyHost?: string
|
||||
socksProxyPort?: number
|
||||
httpProxyHost?: string
|
||||
httpProxyPort?: number
|
||||
reuseSession?: boolean
|
||||
auth: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
|
||||
password: string
|
||||
privateKeys: string[]
|
||||
keepaliveInterval: number
|
||||
keepaliveCountMax: number
|
||||
readyTimeout: number | null
|
||||
x11: boolean
|
||||
skipBanner: boolean
|
||||
jumpHost: string | null
|
||||
agentForward: boolean
|
||||
warnOnClose: boolean
|
||||
algorithms: Record<SSHAlgorithmType, string[]>
|
||||
proxyCommand: string | null
|
||||
forwardedPorts: ForwardedPortConfig[]
|
||||
socksProxyHost: string | null
|
||||
socksProxyPort: number | null
|
||||
httpProxyHost: string | null
|
||||
httpProxyPort: number | null
|
||||
reuseSession: boolean
|
||||
input: InputProcessingOptions,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,21 @@ import { Component, ViewChild } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { firstBy } from 'thenby'
|
||||
|
||||
import { FileProvidersService, Platform, HostAppService, PromptModalComponent, PartialProfile, ProfilesService } from 'tabby-core'
|
||||
import { FileProvidersService, Platform, HostAppService, PromptModalComponent, PartialProfile, ProfilesService, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
|
||||
import { supportedAlgorithms } from '../algorithms'
|
||||
import { FullyDefined, ProxifiedConfig } from 'tabby-core/src/services/config.service'
|
||||
import { SSHProfilesService } from '../profiles'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './sshProfileSettings.component.pug',
|
||||
})
|
||||
export class SSHProfileSettingsComponent {
|
||||
export class SSHProfileSettingsComponent implements ProfileSettingsComponent<SSHProfile, SSHProfilesService> {
|
||||
Platform = Platform
|
||||
profile: SSHProfile
|
||||
profile: ProxifiedConfig<FullyDefined<SSHProfile>>
|
||||
hasSavedPassword: boolean
|
||||
|
||||
connectionMode: 'direct'|'proxyCommand'|'jumpHost'|'socksProxy'|'httpProxy' = 'direct'
|
||||
@@ -39,14 +41,11 @@ export class SSHProfileSettingsComponent {
|
||||
|
||||
for (const k of Object.values(SSHAlgorithmType)) {
|
||||
this.algorithms[k] = {}
|
||||
for (const alg of this.profile.options.algorithms?.[k] ?? []) {
|
||||
for (const alg of this.profile.options.algorithms[k]) {
|
||||
this.algorithms[k][alg] = true
|
||||
}
|
||||
}
|
||||
|
||||
this.profile.options.auth = this.profile.options.auth ?? null
|
||||
this.profile.options.privateKeys ??= []
|
||||
|
||||
if (this.profile.options.proxyCommand) {
|
||||
this.connectionMode = 'proxyCommand'
|
||||
} else if (this.profile.options.jumpHost) {
|
||||
@@ -92,49 +91,48 @@ export class SSHProfileSettingsComponent {
|
||||
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null)
|
||||
if (ref) {
|
||||
this.profile.options.privateKeys = [
|
||||
...this.profile.options.privateKeys!,
|
||||
...this.profile.options.privateKeys,
|
||||
ref,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
save () {
|
||||
for (const k of Object.values(SSHAlgorithmType)) {
|
||||
this.profile.options.algorithms![k] = Object.entries(this.algorithms[k])
|
||||
this.profile.options.algorithms[k] = Object.entries(this.algorithms[k])
|
||||
.filter(([_, v]) => !!v)
|
||||
.map(([key, _]) => key)
|
||||
if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms![k].sort() }
|
||||
if(k !== SSHAlgorithmType.COMPRESSION) { this.profile.options.algorithms[k].sort() }
|
||||
}
|
||||
|
||||
if (this.connectionMode !== 'jumpHost') {
|
||||
this.profile.options.jumpHost = undefined
|
||||
this.profile.options.jumpHost = null
|
||||
}
|
||||
if (this.connectionMode !== 'proxyCommand') {
|
||||
this.profile.options.proxyCommand = undefined
|
||||
this.profile.options.proxyCommand = null
|
||||
}
|
||||
if (this.connectionMode !== 'socksProxy') {
|
||||
this.profile.options.socksProxyHost = undefined
|
||||
this.profile.options.socksProxyPort = undefined
|
||||
this.profile.options.socksProxyHost = null
|
||||
this.profile.options.socksProxyPort = null
|
||||
}
|
||||
if (this.connectionMode !== 'httpProxy') {
|
||||
this.profile.options.httpProxyHost = undefined
|
||||
this.profile.options.httpProxyPort = undefined
|
||||
this.profile.options.httpProxyHost = null
|
||||
this.profile.options.httpProxyPort = null
|
||||
}
|
||||
|
||||
this.loginScriptsSettings?.save()
|
||||
}
|
||||
|
||||
onForwardAdded (fw: ForwardedPortConfig) {
|
||||
this.profile.options.forwardedPorts = this.profile.options.forwardedPorts ?? []
|
||||
this.profile.options.forwardedPorts.push(fw)
|
||||
}
|
||||
|
||||
onForwardRemoved (fw: ForwardedPortConfig) {
|
||||
this.profile.options.forwardedPorts = this.profile.options.forwardedPorts?.filter(x => x !== fw)
|
||||
this.profile.options.forwardedPorts = this.profile.options.forwardedPorts.filter(x => x !== fw)
|
||||
}
|
||||
|
||||
getConnectionDropdownTitle () {
|
||||
|
||||
@@ -195,7 +195,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
||||
if (!this.session?.open) {
|
||||
return true
|
||||
}
|
||||
if (!(this.profile.options.warnOnClose ?? this.config.store.ssh.warnOnClose)) {
|
||||
if (!this.profile.options.warnOnClose) {
|
||||
return true
|
||||
}
|
||||
return (await this.platform.showMessageBox(
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
|
||||
settingsComponent = SSHProfileSettingsComponent
|
||||
configDefaults = {
|
||||
options: {
|
||||
host: null,
|
||||
host: '',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
auth: null,
|
||||
|
||||
@@ -44,7 +44,7 @@ export class SSHService {
|
||||
uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}`
|
||||
}
|
||||
}
|
||||
if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys && jumpHostProfile.options.privateKeys.length > 0) {
|
||||
if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys.length > 0) {
|
||||
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(jumpHostProfile)
|
||||
tmpFile = privateKeyPairs.privateKeyFile
|
||||
if (tmpFile) {
|
||||
@@ -80,7 +80,7 @@ export class SSHService {
|
||||
}
|
||||
|
||||
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
|
||||
if (!profile.options.privateKeys || profile.options.privateKeys.length === 0) {
|
||||
if (profile.options.privateKeys.length === 0) {
|
||||
throw new Error('No private keys in profile')
|
||||
}
|
||||
const path = this.getWinSCPPath()
|
||||
@@ -122,7 +122,7 @@ export class SSHService {
|
||||
|
||||
let tmpFile: tmp.FileResult|null = null
|
||||
try {
|
||||
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
|
||||
if (session.activePrivateKey && session.profile.options.privateKeys.length > 0) {
|
||||
const profile = session.profile
|
||||
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
|
||||
tmpFile = privateKeyPairs.privateKeyFile
|
||||
|
||||
@@ -40,7 +40,7 @@ export class SSHShellSession extends BaseSession {
|
||||
this.logger.debug('Opening shell')
|
||||
|
||||
try {
|
||||
this.shell = await this.ssh.openShellChannel({ x11: this.profile.options.x11 ?? false })
|
||||
this.shell = await this.ssh.openShellChannel({ x11: this.profile.options.x11 })
|
||||
} catch (err) {
|
||||
if (err.toString().includes('Unable to request X11')) {
|
||||
this.emitServiceMessage(' Make sure `xauth` is installed on the remote side')
|
||||
|
||||
@@ -161,7 +161,7 @@ export class SSHSession {
|
||||
async init (): Promise<void> {
|
||||
this.allAuthMethods = [{ type: 'none' }]
|
||||
if (!this.profile.options.auth || this.profile.options.auth === 'publicKey') {
|
||||
if (this.profile.options.privateKeys?.length) {
|
||||
if (this.profile.options.privateKeys.length) {
|
||||
for (let pk of this.profile.options.privateKeys) {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||
let contents: Buffer
|
||||
@@ -207,7 +207,7 @@ export class SSHSession {
|
||||
} else {
|
||||
// If user configured specific private keys, try to load their corresponding
|
||||
// .pub files and use them first for agent-identity authentication
|
||||
if (this.profile.options.privateKeys?.length) {
|
||||
if (this.profile.options.privateKeys.length) {
|
||||
for (let pk of this.profile.options.privateKeys) {
|
||||
pk = pk.replace('%h', this.profile.options.host)
|
||||
pk = pk.replace('%r', this.profile.options.user)
|
||||
@@ -352,7 +352,7 @@ export class SSHSession {
|
||||
|
||||
const algorithms = {}
|
||||
for (const key of Object.values(SSHAlgorithmType)) {
|
||||
algorithms[key] = this.profile.options.algorithms![key].filter(x => supportedAlgorithms[key].includes(x))
|
||||
algorithms[key] = this.profile.options.algorithms[key].filter(x => supportedAlgorithms[key].includes(x))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||
@@ -396,13 +396,13 @@ export class SSHSession {
|
||||
},
|
||||
{
|
||||
preferred: {
|
||||
ciphers: this.profile.options.algorithms?.[SSHAlgorithmType.CIPHER]?.filter(x => supportedAlgorithms[SSHAlgorithmType.CIPHER].includes(x)),
|
||||
kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
|
||||
mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
|
||||
key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
|
||||
compression: this.profile.options.algorithms?.[SSHAlgorithmType.COMPRESSION]?.filter(x => supportedAlgorithms[SSHAlgorithmType.COMPRESSION].includes(x)),
|
||||
ciphers: this.profile.options.algorithms[SSHAlgorithmType.CIPHER].filter(x => supportedAlgorithms[SSHAlgorithmType.CIPHER].includes(x)),
|
||||
kex: this.profile.options.algorithms[SSHAlgorithmType.KEX].filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
|
||||
mac: this.profile.options.algorithms[SSHAlgorithmType.HMAC].filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
|
||||
key: this.profile.options.algorithms[SSHAlgorithmType.HOSTKEY].filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
|
||||
compression: this.profile.options.algorithms[SSHAlgorithmType.COMPRESSION].filter(x => supportedAlgorithms[SSHAlgorithmType.COMPRESSION].includes(x)),
|
||||
},
|
||||
keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000),
|
||||
keepaliveIntervalSeconds: Math.round(this.profile.options.keepaliveInterval / 1000),
|
||||
keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
||||
connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined,
|
||||
},
|
||||
@@ -466,7 +466,7 @@ export class SSHSession {
|
||||
this.passwordStorage.savePassword(this.profile, this.savedPassword, this.authUsername ?? undefined)
|
||||
}
|
||||
|
||||
for (const fw of this.profile.options.forwardedPorts ?? []) {
|
||||
for (const fw of this.profile.options.forwardedPorts) {
|
||||
this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ export class SSHSession {
|
||||
|
||||
const keyDigest = crypto.createHash('sha256').update(key.bytes()).digest('base64')
|
||||
|
||||
const knownHost = this.knownHosts.getFor(selector)
|
||||
const knownHost = this.profile.options.host ? this.knownHosts.getFor(selector) : null
|
||||
if (!knownHost || knownHost.digest !== keyDigest) {
|
||||
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
||||
modal.componentInstance.selector = selector
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
import { ProfileSettingsComponent } from 'tabby-core'
|
||||
import { FullyDefined, ProfileSettingsComponent } from 'tabby-core'
|
||||
import { TelnetProfile } from '../session'
|
||||
import { TelnetProfilesService } from '../profiles'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './telnetProfileSettings.component.pug',
|
||||
})
|
||||
export class TelnetProfileSettingsComponent implements ProfileSettingsComponent<TelnetProfile> {
|
||||
profile: TelnetProfile
|
||||
export class TelnetProfileSettingsComponent implements ProfileSettingsComponent<TelnetProfile, TelnetProfilesService> {
|
||||
profile: FullyDefined<TelnetProfile>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface TelnetProfile extends ConnectableTerminalProfile {
|
||||
|
||||
export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScriptsOptions {
|
||||
host: string
|
||||
port?: number
|
||||
port: number | null
|
||||
input: InputProcessingOptions,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Spinner } from 'cli-spinner'
|
||||
import colors from 'ansi-colors'
|
||||
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags, Component } from '@angular/core'
|
||||
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
|
||||
import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService, HostWindowService, ResettableTimeout, TranslateService, ThemesService } from 'tabby-core'
|
||||
import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService, HostWindowService, ResettableTimeout, TranslateService, ThemesService, FullyDefined } from 'tabby-core'
|
||||
|
||||
import { BaseSession } from '../session'
|
||||
|
||||
@@ -97,7 +97,7 @@ export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends Bas
|
||||
frontendReady = new Subject<void>()
|
||||
size: ResizeEvent
|
||||
|
||||
profile: P
|
||||
profile: FullyDefined<P>
|
||||
|
||||
/**
|
||||
* Enables normal passthrough from session output to terminal input
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface TerminalColorScheme {
|
||||
}
|
||||
|
||||
export interface BaseTerminalProfile extends Profile {
|
||||
terminalColorScheme?: TerminalColorScheme
|
||||
terminalColorScheme: TerminalColorScheme | null
|
||||
}
|
||||
|
||||
export interface ConnectableTerminalProfile extends BaseTerminalProfile, ConnectableProfile {}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class LoginScriptsSettingsComponent {
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.scripts = this.options.scripts ?? []
|
||||
this.scripts = this.options.scripts
|
||||
}
|
||||
|
||||
async deleteScript (script: LoginScript) {
|
||||
|
||||
@@ -385,7 +385,7 @@ export class XTermFrontend extends Frontend {
|
||||
this.xtermCore._scrollToBottom()
|
||||
}
|
||||
|
||||
private configureColors (scheme: TerminalColorScheme|undefined): void {
|
||||
private configureColors (scheme: TerminalColorScheme | null): void {
|
||||
const appColorScheme = this.themes._getActiveColorScheme() as TerminalColorScheme
|
||||
|
||||
scheme = scheme ?? appColorScheme
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ConfigService, ThemesService } from 'tabby-core'
|
||||
export function getTerminalBackgroundColor (
|
||||
config: ConfigService,
|
||||
themes: ThemesService,
|
||||
scheme?: TerminalColorScheme,
|
||||
scheme: TerminalColorScheme | null,
|
||||
): string|null {
|
||||
const appTheme = themes.findCurrentTheme()
|
||||
const appColorScheme = themes._getActiveColorScheme() as TerminalColorScheme
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface LoginScript {
|
||||
}
|
||||
|
||||
export interface LoginScriptsOptions {
|
||||
scripts?: LoginScript[]
|
||||
scripts: LoginScript[]
|
||||
}
|
||||
|
||||
export class LoginScriptProcessor extends SessionMiddleware {
|
||||
@@ -32,7 +32,7 @@ export class LoginScriptProcessor extends SessionMiddleware {
|
||||
options: LoginScriptsOptions,
|
||||
) {
|
||||
super()
|
||||
this.remainingScripts = deepClone(options.scripts ?? [])
|
||||
this.remainingScripts = deepClone(options.scripts)
|
||||
for (const script of this.remainingScripts) {
|
||||
if (!script.isRegex) {
|
||||
script.expect = this.unescape(script.expect)
|
||||
|
||||
@@ -12,10 +12,10 @@ export type OutputMode = null | 'hex'
|
||||
export type NewlineMode = null | 'cr' | 'lf' | 'crlf' | 'implicit_cr' | 'implicit_lf'
|
||||
|
||||
export interface StreamProcessingOptions {
|
||||
inputMode?: InputMode
|
||||
inputNewlines?: NewlineMode
|
||||
outputMode?: OutputMode
|
||||
outputNewlines?: NewlineMode
|
||||
inputMode: InputMode
|
||||
inputNewlines: NewlineMode
|
||||
outputMode: OutputMode
|
||||
outputNewlines: NewlineMode
|
||||
}
|
||||
|
||||
export class TerminalStreamProcessor extends SessionMiddleware {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DemoTerminalTabComponent } from './components/terminalTab.component'
|
||||
export class DemoProfilesService extends ProfileProvider<Profile> {
|
||||
id = 'demo'
|
||||
name = 'Demo'
|
||||
configDefaults = { options: {} }
|
||||
|
||||
async getBuiltinProfiles (): Promise<PartialProfile<Profile>[]> {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user