فهرست منبع

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

Eugene Pankov 2 سال پیش
والد
کامیت
69d884e164
32فایلهای تغییر یافته به همراه192 افزوده شده و 106 حذف شده
  1. 1 0
      tabby-core/src/services/profiles.service.ts
  2. 3 3
      tabby-linkifier/src/api.ts
  3. 1 1
      tabby-linkifier/src/decorator.ts
  4. 2 2
      tabby-linkifier/src/handlers.ts
  5. 2 2
      tabby-local/src/api.ts
  6. 1 2
      tabby-local/src/components/terminalTab.component.ts
  7. 2 2
      tabby-local/src/services/terminal.service.ts
  8. 6 4
      tabby-local/src/tabContextMenu.ts
  9. 3 3
      tabby-serial/src/api.ts
  10. 3 9
      tabby-serial/src/components/serialTab.component.ts
  11. 2 3
      tabby-ssh/src/api/interfaces.ts
  12. 5 0
      tabby-ssh/src/components/sshProfileSettings.component.pug
  13. 3 12
      tabby-ssh/src/components/sshTab.component.ts
  14. 1 1
      tabby-ssh/src/tabContextMenu.ts
  15. 2 11
      tabby-telnet/src/components/telnetTab.component.ts
  16. 3 3
      tabby-telnet/src/session.ts
  17. 1 1
      tabby-terminal/package.json
  18. 13 10
      tabby-terminal/src/api/baseTerminalTab.component.ts
  19. 1 1
      tabby-terminal/src/api/contextMenuProvider.ts
  20. 4 4
      tabby-terminal/src/api/decorator.ts
  21. 7 0
      tabby-terminal/src/api/interfaces.ts
  22. 31 0
      tabby-terminal/src/components/colorSchemeSelector.component.pug
  23. 53 0
      tabby-terminal/src/components/colorSchemeSelector.component.ts
  24. 1 1
      tabby-terminal/src/components/terminalToolbar.component.ts
  25. 7 7
      tabby-terminal/src/features/debug.ts
  26. 2 2
      tabby-terminal/src/features/pathDrop.ts
  27. 2 2
      tabby-terminal/src/features/zmodem.ts
  28. 3 3
      tabby-terminal/src/frontends/frontend.ts
  29. 15 12
      tabby-terminal/src/frontends/xtermFrontend.ts
  30. 4 0
      tabby-terminal/src/index.ts
  31. 3 3
      tabby-terminal/src/services/multifocus.service.ts
  32. 5 2
      tabby-web-demo/src/components/terminalTab.component.ts

+ 1 - 0
tabby-core/src/services/profiles.service.ts

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

+ 3 - 3
tabby-linkifier/src/api.ts

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

+ 1 - 1
tabby-linkifier/src/decorator.ts

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

+ 2 - 2
tabby-linkifier/src/handlers.ts

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

+ 2 - 2
tabby-local/src/api.ts

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

+ 1 - 2
tabby-local/src/components/terminalTab.component.ts

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

+ 2 - 2
tabby-local/src/services/terminal.service.ts

@@ -30,7 +30,7 @@ export class TerminalService {
      * Launches a new terminal with a specific shell and CWD
      * @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) {
             profile = await this.getDefaultProfile()
         }
@@ -55,6 +55,6 @@ export class TerminalService {
         return (await this.profilesService.openNewTabForProfile({
             ...fullProfile,
             options,
-        })) as TerminalTabComponent
+        })) as TerminalTabComponent|null
     }
 }

+ 6 - 4
tabby-local/src/tabContextMenu.ts

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

+ 3 - 3
tabby-serial/src/api.ts

@@ -1,12 +1,12 @@
 import stripAnsi from 'strip-ansi'
 import { SerialPortStream } from '@serialport/stream'
-import { LogService, NotificationsService, Profile } from 'tabby-core'
+import { LogService, NotificationsService } from 'tabby-core'
 import { Subject, Observable } from 'rxjs'
 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'
 
-export interface SerialProfile extends Profile {
+export interface SerialProfile extends BaseTerminalProfile {
     options: SerialProfileOptions
 }
 

+ 3 - 9
tabby-serial/src/components/serialTab.component.ts

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

+ 2 - 3
tabby-ssh/src/api/interfaces.ts

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

+ 5 - 0
tabby-ssh/src/components/sshProfileSettings.component.pug

@@ -248,6 +248,11 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
                     div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
                         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)
         a(ngbNavLink, translate) Login scripts
         ng-template(ngbNavContent)

+ 3 - 12
tabby-ssh/src/components/sshTab.component.ts

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

+ 1 - 1
tabby-ssh/src/tabContextMenu.ts

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

+ 2 - 11
tabby-telnet/src/components/telnetTab.component.ts

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

+ 3 - 3
tabby-telnet/src/session.ts

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

+ 1 - 1
tabby-terminal/package.json

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

+ 13 - 10
tabby-terminal/src/api/baseTerminalTab.component.ts

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

+ 1 - 1
tabby-terminal/src/api/contextMenuProvider.ts

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

+ 4 - 4
tabby-terminal/src/api/decorator.ts

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

+ 7 - 0
tabby-terminal/src/api/interfaces.ts

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

+ 31 - 0
tabby-terminal/src/components/colorSchemeSelector.component.pug

@@ -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')

+ 53 - 0
tabby-terminal/src/components/colorSchemeSelector.component.ts

@@ -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()
+    }
+}

+ 1 - 1
tabby-terminal/src/components/terminalToolbar.component.ts

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

+ 7 - 7
tabby-terminal/src/features/debug.ts

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

+ 2 - 2
tabby-terminal/src/features/pathDrop.ts

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

+ 2 - 2
tabby-terminal/src/features/zmodem.ts

@@ -220,7 +220,7 @@ export class ZModemDecorator extends TerminalDecorator {
         super()
     }
 
-    attach (terminal: BaseTerminalTabComponent): void {
+    attach (terminal: BaseTerminalTabComponent<any>): void {
         setTimeout(() => {
             this.attachToSession(terminal)
             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) {
             return
         }

+ 3 - 3
tabby-terminal/src/frontends/frontend.ts

@@ -1,6 +1,6 @@
 import { Injector } from '@angular/core'
 import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
-import { ResizeEvent } from '../api/interfaces'
+import { BaseTerminalProfile, ResizeEvent } from '../api/interfaces'
 
 export interface SearchOptions {
     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
 
     abstract getSelection (): string
@@ -80,7 +80,7 @@ export abstract class Frontend {
     abstract scrollPages (pages: number): void
     abstract scrollToBottom (): void
 
-    abstract configure (): void
+    abstract configure (profile: BaseTerminalProfile): void
     abstract setZoom (zoom: number): void
 
     abstract findNext (term: string, searchOptions?: SearchOptions): SearchState

+ 15 - 12
tabby-terminal/src/frontends/xtermFrontend.ts

@@ -16,6 +16,7 @@ import deepEqual from 'deep-equal'
 import { Attributes } from 'xterm/src/common/buffer/Constants'
 import { AttributeData } from 'xterm/src/common/buffer/AttributeData'
 import { CellData } from 'xterm/src/common/buffer/CellData'
+import { BaseTerminalProfile, TerminalColorScheme } from '../api/interfaces'
 
 const COLOR_NAMES = [
     '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.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))
 
         // Just configure the colors to avoid a flash
-        this.configureColors()
+        this.configureColors(profile.terminalColorScheme)
 
         if (this.enableWebGL) {
             this.webGLAddon = new WebglAddon()
@@ -353,20 +354,22 @@ export class XTermFrontend extends Frontend {
         this.xtermCore._scrollToBottom()
     }
 
-    private configureColors () {
+    private configureColors (scheme: TerminalColorScheme|undefined): void {
         const config = this.configService.store
 
+        scheme = scheme ?? config.terminal.colorScheme
+
         const theme: ITheme = {
-            foreground: config.terminal.colorScheme.foreground,
-            selectionBackground: config.terminal.colorScheme.selection || '#88888888',
-            selectionForeground: config.terminal.colorScheme.selectionForeground || undefined,
-            background: config.terminal.background === 'colorScheme' ? config.terminal.colorScheme.background : '#00000000',
-            cursor: config.terminal.colorScheme.cursor,
-            cursorAccent: config.terminal.colorScheme.cursorAccent,
+            foreground: scheme!.foreground,
+            selectionBackground: scheme!.selection ?? '#88888888',
+            selectionForeground: scheme!.selectionForeground ?? undefined,
+            background: config.terminal.background === 'colorScheme' ? scheme!.background : '#00000000',
+            cursor: scheme!.cursor,
+            cursorAccent: scheme!.cursorAccent,
         }
 
         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)) {
@@ -375,7 +378,7 @@ export class XTermFrontend extends Frontend {
         }
     }
 
-    configure (): void {
+    configure (profile: BaseTerminalProfile): void {
         const config = this.configService.store
 
         setImmediate(() => {
@@ -408,7 +411,7 @@ export class XTermFrontend extends Frontend {
 
         this.copyOnSelect = config.terminal.copyOnSelect
 
-        this.configureColors()
+        this.configureColors(profile.terminalColorScheme)
 
         if (this.opened && config.terminal.ligatures && !this.ligaturesAddon && this.hostApp.platform !== Platform.Web) {
             this.ligaturesAddon = new LigaturesAddon()

+ 4 - 0
tabby-terminal/src/index.ts

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

+ 3 - 3
tabby-terminal/src/services/multifocus.service.ts

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

+ 5 - 2
tabby-web-demo/src/components/terminalTab.component.ts

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