separate color schemes per profile - fixes #5885, fixes #4593, fixes #3516, fixes #7457, fixes #765

This commit is contained in:
Eugene Pankov
2023-02-04 19:51:16 +01:00
parent e0181973f7
commit 69d884e164
32 changed files with 192 additions and 106 deletions

View File

@@ -23,6 +23,7 @@ export class ProfilesService {
weight: 0, weight: 0,
isBuiltin: false, isBuiltin: false,
isTemplate: false, isTemplate: false,
terminalColorScheme: null,
} }
constructor ( constructor (

View File

@@ -4,13 +4,13 @@ export abstract class LinkHandler {
regex: RegExp regex: RegExp
priority = 1 priority = 1
convert (uri: string, _tab?: BaseTerminalTabComponent): Promise<string>|string { convert (uri: string, _tab?: BaseTerminalTabComponent<any>): Promise<string>|string {
return uri return uri
} }
verify (_uri: string, _tab?: BaseTerminalTabComponent): Promise<boolean>|boolean { verify (_uri: string, _tab?: BaseTerminalTabComponent<any>): Promise<boolean>|boolean {
return true return true
} }
abstract handle (uri: string, tab?: BaseTerminalTabComponent): void abstract handle (uri: string, tab?: BaseTerminalTabComponent<any>): void
} }

View File

@@ -14,7 +14,7 @@ export class LinkHighlighterDecorator extends TerminalDecorator {
super() super()
} }
attach (tab: BaseTerminalTabComponent): void { attach (tab: BaseTerminalTabComponent<any>): void {
if (!(tab.frontend instanceof XTermFrontend)) { if (!(tab.frontend instanceof XTermFrontend)) {
// not xterm // not xterm
return return

View File

@@ -65,7 +65,7 @@ export class BaseFileHandler extends LinkHandler {
} }
} }
async convert (uri: string, tab?: BaseTerminalTabComponent): Promise<string> { async convert (uri: string, tab?: BaseTerminalTabComponent<any>): Promise<string> {
let p = untildify(uri) let p = untildify(uri)
if (!path.isAbsolute(p) && tab) { if (!path.isAbsolute(p) && tab) {
const cwd = await tab.session?.getWorkingDirectory() const cwd = await tab.session?.getWorkingDirectory()
@@ -102,7 +102,7 @@ export class WindowsFileHandler extends BaseFileHandler {
super(toastr, platform) super(toastr, platform)
} }
convert (uri: string, tab?: BaseTerminalTabComponent): Promise<string> { convert (uri: string, tab?: BaseTerminalTabComponent<any>): Promise<string> {
const sanitizedUri = uri.replace(/"/g, '') const sanitizedUri = uri.replace(/"/g, '')
return super.convert(sanitizedUri, tab) return super.convert(sanitizedUri, tab)
} }

View File

@@ -1,4 +1,4 @@
import { Profile } from 'tabby-core' import { BaseTerminalProfile } from 'tabby-terminal'
export interface Shell { export interface Shell {
id: string id: string
@@ -44,7 +44,7 @@ export interface SessionOptions {
runAsAdministrator?: boolean runAsAdministrator?: boolean
} }
export interface LocalProfile extends Profile { export interface LocalProfile extends BaseTerminalProfile {
options: SessionOptions options: SessionOptions
} }

View File

@@ -13,9 +13,8 @@ import { UACService } from '../services/uac.service'
styles: BaseTerminalTabComponent.styles, styles: BaseTerminalTabComponent.styles,
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class TerminalTabComponent extends BaseTerminalTabComponent { export class TerminalTabComponent extends BaseTerminalTabComponent<LocalProfile> {
@Input() sessionOptions: SessionOptions // Deprecated @Input() sessionOptions: SessionOptions // Deprecated
@Input() profile: LocalProfile
session: Session|null = null session: Session|null = null
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor

View File

@@ -30,7 +30,7 @@ export class TerminalService {
* Launches a new terminal with a specific shell and CWD * Launches a new terminal with a specific shell and CWD
* @param pause Wait for a keypress when the shell exits * @param pause Wait for a keypress when the shell exits
*/ */
async openTab (profile?: PartialProfile<LocalProfile>|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> { async openTab (profile?: PartialProfile<LocalProfile>|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent|null> {
if (!profile) { if (!profile) {
profile = await this.getDefaultProfile() profile = await this.getDefaultProfile()
} }
@@ -55,6 +55,6 @@ export class TerminalService {
return (await this.profilesService.openNewTabForProfile({ return (await this.profilesService.openNewTabForProfile({
...fullProfile, ...fullProfile,
options, options,
})) as TerminalTabComponent })) as TerminalTabComponent|null
} }
} }

View File

@@ -22,6 +22,7 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
if (!(tab instanceof TerminalTabComponent)) { if (!(tab instanceof TerminalTabComponent)) {
return [] return []
} }
const terminalTab = tab
const items: MenuItemOptions[] = [ const items: MenuItemOptions[] = [
{ {
label: this.translate.instant('Save as profile'), label: this.translate.instant('Save as profile'),
@@ -34,8 +35,8 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
} }
const profile = { const profile = {
options: { options: {
...tab.profile.options, ...terminalTab.profile.options,
cwd: await tab.session?.getWorkingDirectory() ?? tab.profile.options.cwd, cwd: await terminalTab.session?.getWorkingDirectory() ?? terminalTab.profile.options.cwd,
}, },
name, name,
type: 'local', type: 'local',
@@ -117,13 +118,14 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
} }
if (tab instanceof TerminalTabComponent && tabHeader && this.uac.isAvailable) { if (tab instanceof TerminalTabComponent && tabHeader && this.uac.isAvailable) {
const terminalTab = tab
items.push({ items.push({
label: this.translate.instant('Duplicate as administrator'), label: this.translate.instant('Duplicate as administrator'),
click: () => { click: () => {
this.profilesService.openNewTabForProfile({ this.profilesService.openNewTabForProfile({
...tab.profile, ...terminalTab.profile,
options: { options: {
...tab.profile.options, ...terminalTab.profile.options,
runAsAdministrator: true, runAsAdministrator: true,
}, },
}) })

View File

@@ -1,12 +1,12 @@
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import { SerialPortStream } from '@serialport/stream' import { SerialPortStream } from '@serialport/stream'
import { LogService, NotificationsService, Profile } from 'tabby-core' import { LogService, NotificationsService } from 'tabby-core'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Injector, NgZone } from '@angular/core' import { Injector, NgZone } from '@angular/core'
import { BaseSession, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' import { BaseSession, BaseTerminalProfile, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
import { SerialService } from './services/serial.service' import { SerialService } from './services/serial.service'
export interface SerialProfile extends Profile { export interface SerialProfile extends BaseTerminalProfile {
options: SerialProfileOptions options: SerialProfileOptions
} }

View File

@@ -14,8 +14,7 @@ import { SerialSession, BAUD_RATES, SerialProfile } from '../api'
styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles], styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class SerialTabComponent extends BaseTerminalTabComponent { export class SerialTabComponent extends BaseTerminalTabComponent<SerialProfile> {
profile?: SerialProfile
session: SerialSession|null = null session: SerialSession|null = null
serialPort: any serialPort: any
Platform = Platform Platform = Platform
@@ -56,16 +55,11 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
super.ngOnInit() super.ngOnInit()
setImmediate(() => { setImmediate(() => {
this.setTitle(this.profile!.name) this.setTitle(this.profile.name)
}) })
} }
async initializeSession () { async initializeSession () {
if (!this.profile) {
this.logger.error('No serial profile info supplied')
return
}
const session = new SerialSession(this.injector, this.profile) const session = new SerialSession(this.injector, this.profile)
this.setSession(session) this.setSession(session)
@@ -121,6 +115,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
})), })),
) )
this.serialPort.update({ baudRate: rate }) this.serialPort.update({ baudRate: rate })
this.profile!.options.baudrate = rate this.profile.options.baudrate = rate
} }
} }

View File

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

View File

@@ -248,6 +248,11 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
div(*ngFor='let alg of supportedAlgorithms.serverHostKey') div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]') checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')
li(ngbNavItem)
a(ngbNavLink, translate) Color scheme
ng-template(ngbNavContent)
color-scheme-selector([(model)]='profile.terminalColorScheme')
li(ngbNavItem) li(ngbNavItem)
a(ngbNavLink, translate) Login scripts a(ngbNavLink, translate) Login scripts
ng-template(ngbNavContent) ng-template(ngbNavContent)

View File

@@ -19,9 +19,8 @@ import { SSHMultiplexerService } from '../services/sshMultiplexer.service'
styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles], styles: [require('./sshTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class SSHTabComponent extends BaseTerminalTabComponent { export class SSHTabComponent extends BaseTerminalTabComponent<SSHProfile> {
Platform = Platform Platform = Platform
profile?: SSHProfile
sshSession: SSHSession|null = null sshSession: SSHSession|null = null
session: SSHShellSession|null = null session: SSHShellSession|null = null
sftpPanelVisible = false sftpPanelVisible = false
@@ -45,10 +44,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
} }
ngOnInit (): void { ngOnInit (): void {
if (!this.profile) {
throw new Error('Profile not set')
}
this.logger = this.log.create('terminalTab') this.logger = this.log.create('terminalTab')
this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => {
@@ -184,10 +179,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
} }
private async initializeSessionMaybeMultiplex (multiplex = true): Promise<void> { private async initializeSessionMaybeMultiplex (multiplex = true): Promise<void> {
if (!this.profile) {
throw new Error('No SSH connection info supplied')
}
this.sshSession = await this.setupOneSession(this.injector, this.profile, multiplex) this.sshSession = await this.setupOneSession(this.injector, this.profile, multiplex)
const session = new SSHShellSession(this.injector, this.sshSession, this.profile) const session = new SSHShellSession(this.injector, this.sshSession, this.profile)
@@ -244,13 +235,13 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
if (!this.session?.open) { if (!this.session?.open) {
return true return true
} }
if (!(this.profile?.options.warnOnClose ?? this.config.store.ssh.warnOnClose)) { if (!(this.profile.options.warnOnClose ?? this.config.store.ssh.warnOnClose)) {
return true return true
} }
return (await this.platform.showMessageBox( return (await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
message: this.translate.instant(_('Disconnect from {host}?'), this.profile?.options), message: this.translate.instant(_('Disconnect from {host}?'), this.profile.options),
buttons: [ buttons: [
this.translate.instant(_('Disconnect')), this.translate.instant(_('Disconnect')),
this.translate.instant(_('Do not close')), this.translate.instant(_('Do not close')),

View File

@@ -18,7 +18,7 @@ export class SFTPContextMenu extends TabContextMenuItemProvider {
} }
async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> { async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> {
if (!(tab instanceof SSHTabComponent) || !tab.profile) { if (!(tab instanceof SSHTabComponent)) {
return [] return []
} }
const items = [{ const items = [{

View File

@@ -14,9 +14,8 @@ import { TelnetProfile, TelnetSession } from '../session'
styles: [require('./telnetTab.component.scss'), ...BaseTerminalTabComponent.styles], styles: [require('./telnetTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class TelnetTabComponent extends BaseTerminalTabComponent { export class TelnetTabComponent extends BaseTerminalTabComponent<TelnetProfile> {
Platform = Platform Platform = Platform
profile?: TelnetProfile
session: TelnetSession|null = null session: TelnetSession|null = null
private reconnectOffered = false private reconnectOffered = false
@@ -29,10 +28,6 @@ export class TelnetTabComponent extends BaseTerminalTabComponent {
} }
ngOnInit (): void { ngOnInit (): void {
if (!this.profile) {
throw new Error('Profile not set')
}
this.logger = this.log.create('telnetTab') this.logger = this.log.create('telnetTab')
this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => { this.subscribeUntilDestroyed(this.hotkeys.hotkey$, hotkey => {
@@ -69,10 +64,6 @@ export class TelnetTabComponent extends BaseTerminalTabComponent {
async initializeSession (): Promise<void> { async initializeSession (): Promise<void> {
this.reconnectOffered = false this.reconnectOffered = false
if (!this.profile) {
this.logger.error('No Telnet connection info supplied')
return
}
const session = new TelnetSession(this.injector, this.profile) const session = new TelnetSession(this.injector, this.profile)
this.setSession(session) this.setSession(session)
@@ -119,7 +110,7 @@ export class TelnetTabComponent extends BaseTerminalTabComponent {
return (await this.platform.showMessageBox( return (await this.platform.showMessageBox(
{ {
type: 'warning', type: 'warning',
message: this.translate.instant(_('Disconnect from {host}?'), this.profile?.options), message: this.translate.instant(_('Disconnect from {host}?'), this.profile.options),
buttons: [ buttons: [
this.translate.instant(_('Disconnect')), this.translate.instant(_('Disconnect')),
this.translate.instant(_('Do not close')), this.translate.instant(_('Do not close')),

View File

@@ -2,12 +2,12 @@ import { Socket } from 'net'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import { Injector } from '@angular/core' import { Injector } from '@angular/core'
import { Profile, LogService } from 'tabby-core' import { LogService } from 'tabby-core'
import { BaseSession, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' import { BaseSession, BaseTerminalProfile, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
export interface TelnetProfile extends Profile { export interface TelnetProfile extends BaseTerminalProfile {
options: TelnetProfileOptions options: TelnetProfileOptions
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "tabby-terminal", "name": "tabby-terminal",
"version": "1.0.189-nightly.0", "version": "1.0.189-nightly.2",
"description": "Tabby's terminal emulation core", "description": "Tabby's terminal emulation core",
"keywords": [ "keywords": [
"tabby-builtin-plugin" "tabby-builtin-plugin"

View File

@@ -9,7 +9,7 @@ import { BaseSession } from '../session'
import { Frontend } from '../frontends/frontend' import { Frontend } from '../frontends/frontend'
import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend' import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend'
import { ResizeEvent } from './interfaces' import { ResizeEvent, BaseTerminalProfile } from './interfaces'
import { TerminalDecorator } from './decorator' import { TerminalDecorator } from './decorator'
import { SearchPanelComponent } from '../components/searchPanel.component' import { SearchPanelComponent } from '../components/searchPanel.component'
import { MultifocusService } from '../services/multifocus.service' import { MultifocusService } from '../services/multifocus.service'
@@ -17,7 +17,7 @@ import { MultifocusService } from '../services/multifocus.service'
/** /**
* A class to base your custom terminal tabs on * A class to base your custom terminal tabs on
*/ */
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy { export class BaseTerminalTabComponent<P extends BaseTerminalProfile> extends BaseTabComponent implements OnInit, OnDestroy {
static template: string = require<string>('../components/baseTerminalTab.component.pug') static template: string = require<string>('../components/baseTerminalTab.component.pug')
static styles: string[] = [require<string>('../components/baseTerminalTab.component.scss')] static styles: string[] = [require<string>('../components/baseTerminalTab.component.scss')]
static animations: AnimationTriggerMetadata[] = [ static animations: AnimationTriggerMetadata[] = [
@@ -90,6 +90,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
frontendReady = new Subject<void>() frontendReady = new Subject<void>()
size: ResizeEvent size: ResizeEvent
profile: P
/** /**
* Enables normall passthrough from session output to terminal input * Enables normall passthrough from session output to terminal input
*/ */
@@ -356,12 +358,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
setImmediate(async () => { setImmediate(async () => {
if (this.hasFocus) { if (this.hasFocus) {
await this.frontend?.attach(this.content.nativeElement) await this.frontend?.attach(this.content.nativeElement, this.profile)
this.frontend?.configure() this.frontend?.configure(this.profile)
} else { } else {
this.focused$.pipe(first()).subscribe(async () => { this.focused$.pipe(first()).subscribe(async () => {
await this.frontend?.attach(this.content.nativeElement) await this.frontend?.attach(this.content.nativeElement, this.profile)
this.frontend?.configure() this.frontend?.configure(this.profile)
}) })
} }
}) })
@@ -508,11 +510,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
* Applies the user settings to the terminal * Applies the user settings to the terminal
*/ */
configure (): void { configure (): void {
this.frontend?.configure() this.frontend?.configure(this.profile)
if (this.config.store.terminal.background === 'colorScheme') { if (this.config.store.terminal.background === 'colorScheme') {
if (this.config.store.terminal.colorScheme.background) { const scheme = this.profile.terminalColorScheme ?? this.config.store.terminal.colorScheme
this.backgroundColor = this.config.store.terminal.colorScheme.background if (scheme.background) {
this.backgroundColor = scheme.background
} }
} else { } else {
this.backgroundColor = null this.backgroundColor = null
@@ -809,7 +812,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
} }
protected forEachFocusedTerminalPane (cb: (tab: BaseTerminalTabComponent) => void): void { protected forEachFocusedTerminalPane (cb: (tab: BaseTerminalTabComponent<any>) => void): void {
if (this.parent && this.parent instanceof SplitTabComponent && this.parent._allFocusMode) { if (this.parent && this.parent instanceof SplitTabComponent && this.parent._allFocusMode) {
for (const tab of this.parent.getAllTabs()) { for (const tab of this.parent.getAllTabs()) {
if (tab instanceof BaseTerminalTabComponent) { if (tab instanceof BaseTerminalTabComponent) {

View File

@@ -8,5 +8,5 @@ import { BaseTerminalTabComponent } from './baseTerminalTab.component'
export abstract class TerminalContextMenuItemProvider { export abstract class TerminalContextMenuItemProvider {
weight: number weight: number
abstract getItems (tab: BaseTerminalTabComponent): Promise<MenuItemOptions[]> abstract getItems (tab: BaseTerminalTabComponent<any>): Promise<MenuItemOptions[]>
} }

View File

@@ -5,18 +5,18 @@ import { BaseTerminalTabComponent } from './baseTerminalTab.component'
* Extend to automatically run actions on new terminals * Extend to automatically run actions on new terminals
*/ */
export abstract class TerminalDecorator { export abstract class TerminalDecorator {
private smartSubscriptions = new Map<BaseTerminalTabComponent, Subscription[]>() private smartSubscriptions = new Map<BaseTerminalTabComponent<any>, Subscription[]>()
/** /**
* Called when a new terminal tab starts * Called when a new terminal tab starts
*/ */
attach (terminal: BaseTerminalTabComponent): void { } // eslint-disable-line attach (terminal: BaseTerminalTabComponent<any>): void { } // eslint-disable-line
/** /**
* Called before a terminal tab is destroyed. * Called before a terminal tab is destroyed.
* Make sure to call super() * Make sure to call super()
*/ */
detach (terminal: BaseTerminalTabComponent): void { detach (terminal: BaseTerminalTabComponent<any>): void {
for (const s of this.smartSubscriptions.get(terminal) ?? []) { for (const s of this.smartSubscriptions.get(terminal) ?? []) {
s.unsubscribe() s.unsubscribe()
} }
@@ -26,7 +26,7 @@ export abstract class TerminalDecorator {
/** /**
* Automatically cancel @subscription once detached from @terminal * Automatically cancel @subscription once detached from @terminal
*/ */
protected subscribeUntilDetached (terminal: BaseTerminalTabComponent, subscription?: Subscription): void { protected subscribeUntilDetached (terminal: BaseTerminalTabComponent<any>, subscription?: Subscription): void {
if (!subscription) { if (!subscription) {
return return
} }

View File

@@ -1,3 +1,5 @@
import { Profile } from 'tabby-core'
export interface ResizeEvent { export interface ResizeEvent {
columns: number columns: number
rows: number rows: number
@@ -11,4 +13,9 @@ export interface TerminalColorScheme {
colors: string[] colors: string[]
selection?: string selection?: string
selectionForeground?: string selectionForeground?: string
cursorAccent?: string
}
export interface BaseTerminalProfile extends Profile {
terminalColorScheme?: TerminalColorScheme
} }

View File

@@ -0,0 +1,31 @@
.head
.bg-dark.p-3.mb-4(*ngIf='model')
.d-flex.align-items-center
span {{model.name}}
.mr-auto
a.btn-link((click)='selectScheme(null); $event.preventDefault()', href='#', translate) Clear
color-scheme-preview([scheme]='model')
.input-group.mb-3
.input-group-prepend
.input-group-text
i.fas.fa-fw.fa-search
input.form-control(type='search', [placeholder]='"Search color schemes"|translate', [(ngModel)]='filter')
.body
.list-group-light.mb-3
ng-container(*ngFor='let scheme of allColorSchemes')
.list-group-item.list-group-item-action(
[hidden]='filter && !scheme.name.toLowerCase().includes(filter.toLowerCase())',
(click)='selectScheme(scheme)',
[class.active]='(currentCustomScheme || currentStockScheme) === scheme'
)
.d-flex.w-100.align-items-center
i.fas.fa-fw([class.fa-check]='model?.name === scheme.name')
.ml-2
.mr-auto {{scheme.name}}
color-scheme-preview([scheme]='scheme')

View File

@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
import { Component, Inject, Input, ChangeDetectionStrategy, ChangeDetectorRef, HostBinding, Output, EventEmitter } from '@angular/core'
import { ConfigService } from 'tabby-core'
import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider'
import { TerminalColorScheme } from '../api/interfaces'
_('Search color schemes')
/** @hidden */
@Component({
selector: 'color-scheme-selector',
template: require('./colorSchemeSelector.component.pug'),
styles: [`
:host {
display: block;
max-height: 100vh;
overflow-y: auto;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ColorSchemeSelectorComponent {
allColorSchemes: TerminalColorScheme[] = []
filter = ''
@Input() model: TerminalColorScheme|null = null
@Output() modelChange = new EventEmitter<TerminalColorScheme|null>()
@HostBinding('class.content-box') true
constructor (
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
private changeDetector: ChangeDetectorRef,
public config: ConfigService,
) { }
async ngOnInit () {
const stockColorSchemes = (await Promise.all(this.config.enabledServices(this.colorSchemeProviders).map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
stockColorSchemes.sort((a, b) => a.name.localeCompare(b.name))
const customColorSchemes = this.config.store.terminal.customColorSchemes
this.allColorSchemes = customColorSchemes.concat(stockColorSchemes)
this.changeDetector.markForCheck()
}
selectScheme (scheme: TerminalColorScheme) {
this.model = scheme
this.modelChange.emit(scheme)
this.changeDetector.markForCheck()
}
}

View File

@@ -10,7 +10,7 @@ import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
styles: [require('./terminalToolbar.component.scss')], styles: [require('./terminalToolbar.component.scss')],
}) })
export class TerminalToolbarComponent { export class TerminalToolbarComponent {
@Input() tab: BaseTerminalTabComponent @Input() tab: BaseTerminalTabComponent<any>
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor ( constructor (

View File

@@ -12,7 +12,7 @@ export class DebugDecorator extends TerminalDecorator {
super() super()
} }
attach (terminal: BaseTerminalTabComponent): void { attach (terminal: BaseTerminalTabComponent<any>): void {
let sessionOutputBuffer = '' let sessionOutputBuffer = ''
const bufferLength = 8192 const bufferLength = 8192
@@ -83,23 +83,23 @@ export class DebugDecorator extends TerminalDecorator {
} }
} }
private doSaveState (terminal: BaseTerminalTabComponent) { private doSaveState (terminal: BaseTerminalTabComponent<any>) {
this.saveFile(terminal.frontend!.saveState(), 'state.txt') this.saveFile(terminal.frontend!.saveState(), 'state.txt')
} }
private async doCopyState (terminal: BaseTerminalTabComponent) { private async doCopyState (terminal: BaseTerminalTabComponent<any>) {
const data = '```' + JSON.stringify(terminal.frontend!.saveState()) + '```' const data = '```' + JSON.stringify(terminal.frontend!.saveState()) + '```'
this.platform.setClipboard({ text: data }) this.platform.setClipboard({ text: data })
} }
private async doLoadState (terminal: BaseTerminalTabComponent) { private async doLoadState (terminal: BaseTerminalTabComponent<any>) {
const data = await this.loadFile() const data = await this.loadFile()
if (data) { if (data) {
terminal.frontend!.restoreState(data) terminal.frontend!.restoreState(data)
} }
} }
private async doPasteState (terminal: BaseTerminalTabComponent) { private async doPasteState (terminal: BaseTerminalTabComponent<any>) {
let data = this.platform.readClipboard() let data = this.platform.readClipboard()
if (data) { if (data) {
if (data.startsWith('`')) { if (data.startsWith('`')) {
@@ -118,14 +118,14 @@ export class DebugDecorator extends TerminalDecorator {
this.platform.setClipboard({ text: data }) this.platform.setClipboard({ text: data })
} }
private async doLoadOutput (terminal: BaseTerminalTabComponent) { private async doLoadOutput (terminal: BaseTerminalTabComponent<any>) {
const data = await this.loadFile() const data = await this.loadFile()
if (data) { if (data) {
await terminal.frontend?.write(data) await terminal.frontend?.write(data)
} }
} }
private async doPasteOutput (terminal: BaseTerminalTabComponent) { private async doPasteOutput (terminal: BaseTerminalTabComponent<any>) {
let data = this.platform.readClipboard() let data = this.platform.readClipboard()
if (data) { if (data) {
if (data.startsWith('`')) { if (data.startsWith('`')) {

View File

@@ -5,7 +5,7 @@ import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
/** @hidden */ /** @hidden */
@Injectable() @Injectable()
export class PathDropDecorator extends TerminalDecorator { export class PathDropDecorator extends TerminalDecorator {
attach (terminal: BaseTerminalTabComponent): void { attach (terminal: BaseTerminalTabComponent<any>): void {
setTimeout(() => { setTimeout(() => {
this.subscribeUntilDetached(terminal, terminal.frontend?.dragOver$.subscribe(event => { this.subscribeUntilDetached(terminal, terminal.frontend?.dragOver$.subscribe(event => {
event.preventDefault() event.preventDefault()
@@ -19,7 +19,7 @@ export class PathDropDecorator extends TerminalDecorator {
}) })
} }
private injectPath (terminal: BaseTerminalTabComponent, path: string) { private injectPath (terminal: BaseTerminalTabComponent<any>, path: string) {
if (path.includes(' ')) { if (path.includes(' ')) {
path = `"${path}"` path = `"${path}"`
} }

View File

@@ -220,7 +220,7 @@ export class ZModemDecorator extends TerminalDecorator {
super() super()
} }
attach (terminal: BaseTerminalTabComponent): void { attach (terminal: BaseTerminalTabComponent<any>): void {
setTimeout(() => { setTimeout(() => {
this.attachToSession(terminal) this.attachToSession(terminal)
this.subscribeUntilDetached(terminal, terminal.sessionChanged$.subscribe(() => { this.subscribeUntilDetached(terminal, terminal.sessionChanged$.subscribe(() => {
@@ -229,7 +229,7 @@ export class ZModemDecorator extends TerminalDecorator {
}) })
} }
private attachToSession (terminal: BaseTerminalTabComponent) { private attachToSession (terminal: BaseTerminalTabComponent<any>) {
if (!terminal.session) { if (!terminal.session) {
return return
} }

View File

@@ -1,6 +1,6 @@
import { Injector } from '@angular/core' import { Injector } from '@angular/core'
import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs' import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
import { ResizeEvent } from '../api/interfaces' import { BaseTerminalProfile, ResizeEvent } from '../api/interfaces'
export interface SearchOptions { export interface SearchOptions {
regex?: boolean regex?: boolean
@@ -64,7 +64,7 @@ export abstract class Frontend {
} }
} }
abstract attach (host: HTMLElement): Promise<void> abstract attach (host: HTMLElement, profile: BaseTerminalProfile): Promise<void>
detach (host: HTMLElement): void { } // eslint-disable-line detach (host: HTMLElement): void { } // eslint-disable-line
abstract getSelection (): string abstract getSelection (): string
@@ -80,7 +80,7 @@ export abstract class Frontend {
abstract scrollPages (pages: number): void abstract scrollPages (pages: number): void
abstract scrollToBottom (): void abstract scrollToBottom (): void
abstract configure (): void abstract configure (profile: BaseTerminalProfile): void
abstract setZoom (zoom: number): void abstract setZoom (zoom: number): void
abstract findNext (term: string, searchOptions?: SearchOptions): SearchState abstract findNext (term: string, searchOptions?: SearchOptions): SearchState

View File

@@ -16,6 +16,7 @@ import deepEqual from 'deep-equal'
import { Attributes } from 'xterm/src/common/buffer/Constants' import { Attributes } from 'xterm/src/common/buffer/Constants'
import { AttributeData } from 'xterm/src/common/buffer/AttributeData' import { AttributeData } from 'xterm/src/common/buffer/AttributeData'
import { CellData } from 'xterm/src/common/buffer/CellData' import { CellData } from 'xterm/src/common/buffer/CellData'
import { BaseTerminalProfile, TerminalColorScheme } from '../api/interfaces'
const COLOR_NAMES = [ const COLOR_NAMES = [
'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
@@ -214,7 +215,7 @@ export class XTermFrontend extends Frontend {
}) })
} }
async attach (host: HTMLElement): Promise<void> { async attach (host: HTMLElement, profile: BaseTerminalProfile): Promise<void> {
this.element = host this.element = host
this.xterm.open(host) this.xterm.open(host)
@@ -224,7 +225,7 @@ export class XTermFrontend extends Frontend {
await new Promise(resolve => setTimeout(resolve, this.hostApp.platform === Platform.Web ? 1000 : 0)) await new Promise(resolve => setTimeout(resolve, this.hostApp.platform === Platform.Web ? 1000 : 0))
// Just configure the colors to avoid a flash // Just configure the colors to avoid a flash
this.configureColors() this.configureColors(profile.terminalColorScheme)
if (this.enableWebGL) { if (this.enableWebGL) {
this.webGLAddon = new WebglAddon() this.webGLAddon = new WebglAddon()
@@ -353,20 +354,22 @@ export class XTermFrontend extends Frontend {
this.xtermCore._scrollToBottom() this.xtermCore._scrollToBottom()
} }
private configureColors () { private configureColors (scheme: TerminalColorScheme|undefined): void {
const config = this.configService.store const config = this.configService.store
scheme = scheme ?? config.terminal.colorScheme
const theme: ITheme = { const theme: ITheme = {
foreground: config.terminal.colorScheme.foreground, foreground: scheme!.foreground,
selectionBackground: config.terminal.colorScheme.selection || '#88888888', selectionBackground: scheme!.selection ?? '#88888888',
selectionForeground: config.terminal.colorScheme.selectionForeground || undefined, selectionForeground: scheme!.selectionForeground ?? undefined,
background: config.terminal.background === 'colorScheme' ? config.terminal.colorScheme.background : '#00000000', background: config.terminal.background === 'colorScheme' ? scheme!.background : '#00000000',
cursor: config.terminal.colorScheme.cursor, cursor: scheme!.cursor,
cursorAccent: config.terminal.colorScheme.cursorAccent, cursorAccent: scheme!.cursorAccent,
} }
for (let i = 0; i < COLOR_NAMES.length; i++) { for (let i = 0; i < COLOR_NAMES.length; i++) {
theme[COLOR_NAMES[i]] = config.terminal.colorScheme.colors[i] theme[COLOR_NAMES[i]] = scheme!.colors[i]
} }
if (!deepEqual(this.configuredTheme, theme)) { if (!deepEqual(this.configuredTheme, theme)) {
@@ -375,7 +378,7 @@ export class XTermFrontend extends Frontend {
} }
} }
configure (): void { configure (profile: BaseTerminalProfile): void {
const config = this.configService.store const config = this.configService.store
setImmediate(() => { setImmediate(() => {
@@ -408,7 +411,7 @@ export class XTermFrontend extends Frontend {
this.copyOnSelect = config.terminal.copyOnSelect this.copyOnSelect = config.terminal.copyOnSelect
this.configureColors() this.configureColors(profile.terminalColorScheme)
if (this.opened && config.terminal.ligatures && !this.ligaturesAddon && this.hostApp.platform !== Platform.Web) { if (this.opened && config.terminal.ligatures && !this.ligaturesAddon && this.hostApp.platform !== Platform.Web) {
this.ligaturesAddon = new LigaturesAddon() this.ligaturesAddon = new LigaturesAddon()

View File

@@ -17,6 +17,7 @@ import { SearchPanelComponent } from './components/searchPanel.component'
import { StreamProcessingSettingsComponent } from './components/streamProcessingSettings.component' import { StreamProcessingSettingsComponent } from './components/streamProcessingSettings.component'
import { LoginScriptsSettingsComponent } from './components/loginScriptsSettings.component' import { LoginScriptsSettingsComponent } from './components/loginScriptsSettings.component'
import { TerminalToolbarComponent } from './components/terminalToolbar.component' import { TerminalToolbarComponent } from './components/terminalToolbar.component'
import { ColorSchemeSelectorComponent } from './components/colorSchemeSelector.component'
import { TerminalDecorator } from './api/decorator' import { TerminalDecorator } from './api/decorator'
import { TerminalContextMenuItemProvider } from './api/contextMenuProvider' import { TerminalContextMenuItemProvider } from './api/contextMenuProvider'
@@ -64,10 +65,12 @@ import { TerminalCLIHandler } from './cli'
AppearanceSettingsTabComponent, AppearanceSettingsTabComponent,
ColorSchemeSettingsTabComponent, ColorSchemeSettingsTabComponent,
TerminalSettingsTabComponent, TerminalSettingsTabComponent,
ColorSchemeSelectorComponent,
], ],
declarations: [ declarations: [
ColorPickerComponent, ColorPickerComponent,
ColorSchemePreviewComponent, ColorSchemePreviewComponent,
ColorSchemeSelectorComponent,
AppearanceSettingsTabComponent, AppearanceSettingsTabComponent,
ColorSchemeSettingsTabComponent, ColorSchemeSettingsTabComponent,
TerminalSettingsTabComponent, TerminalSettingsTabComponent,
@@ -78,6 +81,7 @@ import { TerminalCLIHandler } from './cli'
], ],
exports: [ exports: [
ColorPickerComponent, ColorPickerComponent,
ColorSchemeSelectorComponent,
SearchPanelComponent, SearchPanelComponent,
StreamProcessingSettingsComponent, StreamProcessingSettingsComponent,
LoginScriptsSettingsComponent, LoginScriptsSettingsComponent,

View File

@@ -6,7 +6,7 @@ import { SplitTabComponent, TranslateService, AppService, HotkeysService } from
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class MultifocusService { export class MultifocusService {
private inputSubscription: Subscription|null = null private inputSubscription: Subscription|null = null
private currentTab: BaseTerminalTabComponent|null = null private currentTab: BaseTerminalTabComponent<any>|null = null
private warningElement: HTMLElement private warningElement: HTMLElement
constructor ( constructor (
@@ -32,7 +32,7 @@ export class MultifocusService {
}) })
} }
start (currentTab: BaseTerminalTabComponent, tabs: BaseTerminalTabComponent[]): void { start (currentTab: BaseTerminalTabComponent<any>, tabs: BaseTerminalTabComponent<any>[]): void {
if (this.inputSubscription) { if (this.inputSubscription) {
return return
} }
@@ -87,7 +87,7 @@ export class MultifocusService {
} else { } else {
return [] return []
} }
}) as (_) => BaseTerminalTabComponent[]) }) as (_) => BaseTerminalTabComponent<any>[])
.flat() .flat()
this.start(currentTab, tabs) this.start(currentTab, tabs)

View File

@@ -1,7 +1,10 @@
import { Component, Injector } from '@angular/core' import { Component, Injector } from '@angular/core'
import { BaseTerminalTabComponent } from 'tabby-terminal' import { BaseTerminalProfile, BaseTerminalTabComponent } from 'tabby-terminal'
import { Session } from '../session' import { Session } from '../session'
// eslint-disable-next-line @typescript-eslint/no-type-alias
type DemoProfile = BaseTerminalProfile
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'demoTerminalTab', selector: 'demoTerminalTab',
@@ -9,7 +12,7 @@ import { Session } from '../session'
styles: BaseTerminalTabComponent.styles, styles: BaseTerminalTabComponent.styles,
animations: BaseTerminalTabComponent.animations, animations: BaseTerminalTabComponent.animations,
}) })
export class DemoTerminalTabComponent extends BaseTerminalTabComponent { export class DemoTerminalTabComponent extends BaseTerminalTabComponent<DemoProfile> {
session: Session|null = null session: Session|null = null
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor