bundle the clickable-links plugin with Tabby

This commit is contained in:
Eugene Pankov 2021-12-08 19:54:26 +01:00
parent 3eaf46e09d
commit bbb02f4e64
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
25 changed files with 518 additions and 170 deletions

View File

@ -0,0 +1,6 @@
export const PLUGIN_BLACKLIST = [
'terminus-shell-selector', // superseded by profiles
'terminus-scrollbar', // now useless
'terminus-clickable-links', // now bundled with Tabby
'tabby-clickable-links', // now bundled with Tabby
]

View File

@ -2,6 +2,7 @@ import * as fs from 'mz/fs'
import * as path from 'path' import * as path from 'path'
import * as remote from '@electron/remote' import * as remote from '@electron/remote'
import { PluginInfo } from '../../tabby-core/src/api/mainProcess' import { PluginInfo } from '../../tabby-core/src/api/mainProcess'
import { PLUGIN_BLACKLIST } from './pluginBlacklist'
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
@ -109,7 +110,7 @@ export async function findPlugins (): Promise<PluginInfo[]> {
}) })
} }
for (const packageName of pluginNames) { for (const packageName of pluginNames) {
if (packageName.startsWith(PREFIX) || packageName.startsWith(LEGACY_PREFIX)) { if ((packageName.startsWith(PREFIX) || packageName.startsWith(LEGACY_PREFIX)) && !PLUGIN_BLACKLIST.includes(packageName)) {
candidateLocations.push({ pluginDir, packageName }) candidateLocations.push({ pluginDir, packageName })
} }
} }

View File

@ -91,7 +91,7 @@
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect", "start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
"start:prod": "electron app --debug", "start:prod": "electron app --debug",
"prod": "cross-env TABBY_DEV=1 electron app", "prod": "cross-env TABBY_DEV=1 electron app",
"docs": "typedoc --out docs/api --tsconfig tabby-core/tsconfig.typings.json tabby-core/src/index.ts && typedoc --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts", "docs": "node scripts/build-docs.js",
"lint": "eslint --ext ts */src */lib", "lint": "eslint --ext ts */src */lib",
"postinstall": "patch-package && node ./scripts/install-deps.js" "postinstall": "patch-package && node ./scripts/install-deps.js"
}, },

9
scripts/build-docs.js Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env node
const sh = require('shelljs')
const vars = require('./vars')
const log = require('npmlog')
vars.packagesWithDocs.forEach(([dest, src]) => {
log.info('docs', src)
sh.exec(`yarn typedoc --out docs/api/${dest} --tsconfig ${src}/tsconfig.typings.json ${src}/src/index.ts`)
})

View File

@ -25,6 +25,14 @@ exports.builtinPlugins = [
'tabby-electron', 'tabby-electron',
'tabby-local', 'tabby-local',
'tabby-plugin-manager', 'tabby-plugin-manager',
'tabby-linkifier',
]
exports.packagesWithDocs = [
['.', 'tabby-core'],
['terminal', 'tabby-terminal'],
['local', 'tabby-local'],
['settings', 'tabby-settings'],
] ]
exports.allPackages = [ exports.allPackages = [

View File

@ -1,7 +1,13 @@
Tabby Core Plugin # Tabby Core Plugin
--------------------
See also: [Settings plugin API](./settings/), [Terminal plugin API](./terminal/), [Local terminal API](./local/) See also:
* [Settings plugin API](./settings/)
* [Terminal plugin API](./terminal/)
* [Local terminal API](./local/)
* [Linkifier plugin API](./linkifier/)
This module provides:
* tabbed interface services * tabbed interface services
* toolbar UI * toolbar UI

View File

@ -25,7 +25,7 @@ export { DockingService, Screen } from '../services/docking.service'
export { Logger, ConsoleLogger, LogService } from '../services/log.service' export { Logger, ConsoleLogger, LogService } from '../services/log.service'
export { HomeBaseService } from '../services/homeBase.service' export { HomeBaseService } from '../services/homeBase.service'
export { HotkeysService } from '../services/hotkeys.service' export { HotkeysService } from '../services/hotkeys.service'
export { KeyEventData, KeyName, Keystroke } from '../services/hotkeys.util' export { KeyEventData, KeyName, Keystroke, altKeyName, metaKeyName } from '../services/hotkeys.util'
export { NotificationsService } from '../services/notifications.service' export { NotificationsService } from '../services/notifications.service'
export { ThemesService } from '../services/themes.service' export { ThemesService } from '../services/themes.service'
export { ProfilesService } from '../services/profiles.service' export { ProfilesService } from '../services/profiles.service'

View File

@ -0,0 +1,3 @@
# Tabby Linkifier Plugin
This plugin makes URLs, IPs and file paths in the terminal clickable and adds a context menu that allows quickly copying them.

View File

@ -0,0 +1,22 @@
{
"name": "tabby-linkifier",
"version": "1.0.165-nightly.0",
"description": "Makes URLs, IPs and file paths clickable in Tabby",
"keywords": [
"tabby-builtin-plugin"
],
"main": "dist/index.js",
"typings": "typings/index.d.ts",
"scripts": {
"build": "webpack --progress --color --display-modules",
"watch": "webpack --progress --color --watch"
},
"files": [
"typings"
],
"author": "Eugene Pankov",
"license": "MIT",
"devDependencies": {
"untildify": "^4.0.0"
}
}

View File

@ -0,0 +1,16 @@
import { BaseTerminalTabComponent } from 'tabby-terminal'
export abstract class LinkHandler {
regex: RegExp
priority = 1
convert (uri: string, _tab?: BaseTerminalTabComponent): Promise<string>|string {
return uri
}
verify (_uri: string, _tab?: BaseTerminalTabComponent): Promise<boolean>|boolean {
return true
}
abstract handle (uri: string, tab?: BaseTerminalTabComponent): void
}

View File

@ -0,0 +1,12 @@
import { ConfigProvider } from 'tabby-core'
/** @hidden */
export class ClickableLinksConfigProvider extends ConfigProvider {
defaults = {
clickableLinks: {
modifier: null,
},
}
platformDefaults = { }
}

View File

@ -0,0 +1,67 @@
import { Inject, Injectable } from '@angular/core'
import { ConfigService, PlatformService } from 'tabby-core'
import { TerminalDecorator, BaseTerminalTabComponent } from 'tabby-terminal'
import { LinkHandler } from './api'
@Injectable()
export class LinkHighlighterDecorator extends TerminalDecorator {
constructor (
private config: ConfigService,
private platform: PlatformService,
@Inject(LinkHandler) private handlers: LinkHandler[],
) {
super()
}
attach (tab: BaseTerminalTabComponent): void {
if (!(tab.frontend as any).xterm) {
// not hterm
return
}
for (let handler of this.handlers) {
const getLink = async uri => handler.convert(uri, tab)
const openLink = async uri => handler.handle(await getLink(uri), tab)
;(tab.frontend as any).xterm.registerLinkMatcher(
handler.regex,
(event: MouseEvent, uri: string) => {
if (!this.willHandleEvent(event)) {
return
}
openLink(uri)
},
{
priority: handler.priority,
validationCallback: async (uri: string, callback: (isValid: boolean) => void) => {
callback(await handler.verify(await handler.convert(uri, tab), tab))
},
willLinkActivate: (event: MouseEvent, uri: string) => {
if (event.button === 2) {
this.platform.popupContextMenu([
{
click: () => openLink(uri),
label: 'Open',
},
{
click: async () => {
this.platform.setClipboard({ text: await getLink(uri) })
},
label: 'Copy',
},
])
return false
}
return this.willHandleEvent(event)
},
}
)
}
}
private willHandleEvent (event: MouseEvent) {
const modifier = this.config.store.clickableLinks.modifier
return !modifier || event[modifier]
}
}

View File

@ -0,0 +1,108 @@
import * as fs from 'fs/promises'
import * as path from 'path'
import untildify from 'untildify'
import { Injectable } from '@angular/core'
import { ToastrService } from 'ngx-toastr'
import { PlatformService } from 'tabby-core'
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { LinkHandler } from './api'
@Injectable()
export class URLHandler extends LinkHandler {
// From https://urlregex.com/
regex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/
priority = 5
constructor (private platform: PlatformService) {
super()
}
handle (uri: string) {
this.platform.openExternal(uri)
}
}
@Injectable()
export class IPHandler extends LinkHandler {
regex = /\b((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/
priority = 4
constructor (private platform: PlatformService) {
super()
}
handle (uri: string) {
this.platform.openExternal(`http://${uri}`)
}
}
export class BaseFileHandler extends LinkHandler {
constructor (
protected toastr: ToastrService,
protected platform: PlatformService,
) {
super()
}
async handle (uri: string) {
try {
await this.platform.openExternal('file://' + uri)
} catch (err) {
this.toastr.error(err.toString())
}
}
async verify (uri: string): Promise<boolean> {
try {
await fs.access(uri)
return true
} catch {
return false
}
}
async convert (uri: string, tab?: BaseTerminalTabComponent): Promise<string> {
let p = untildify(uri)
if (!path.isAbsolute(p) && tab) {
const cwd = await tab.session?.getWorkingDirectory()
if (cwd) {
p = path.resolve(cwd, p)
}
}
return p
}
}
@Injectable()
export class UnixFileHandler extends BaseFileHandler {
// Only absolute and home paths
regex = /[~]?(\/[\w\d.~-]{1,100})+/
constructor (
protected toastr: ToastrService,
protected platform: PlatformService,
) {
super(toastr, platform)
}
}
@Injectable()
export class WindowsFileHandler extends BaseFileHandler {
regex = /(([a-zA-Z]:|\\|~)\\[\w\-()\\\.]{1,1024}|"([a-zA-Z]:|\\)\\[\w\s\-()\\\.]{1,1024}")/
constructor (
protected toastr: ToastrService,
protected platform: PlatformService,
) {
super(toastr, platform)
}
convert (uri: string, tab?: BaseTerminalTabComponent): Promise<string> {
const sanitizedUri = uri.replace(/"/g, '')
return super.convert(sanitizedUri, tab)
}
}

View File

@ -0,0 +1,26 @@
import { NgModule } from '@angular/core'
import { ToastrModule } from 'ngx-toastr'
import { ConfigProvider } from 'tabby-core'
import { TerminalDecorator } from 'tabby-terminal'
import { LinkHandler } from './api'
import { UnixFileHandler, WindowsFileHandler, URLHandler, IPHandler } from './handlers'
import { LinkHighlighterDecorator } from './decorator'
import { ClickableLinksConfigProvider } from './config'
@NgModule({
imports: [
ToastrModule,
],
providers: [
{ provide: LinkHandler, useClass: URLHandler, multi: true },
{ provide: LinkHandler, useClass: IPHandler, multi: true },
{ provide: LinkHandler, useClass: UnixFileHandler, multi: true },
{ provide: LinkHandler, useClass: WindowsFileHandler, multi: true },
{ provide: TerminalDecorator, useClass: LinkHighlighterDecorator, multi: true },
{ provide: ConfigProvider, useClass: ClickableLinksConfigProvider, multi: true },
],
})
export default class LinkifierModule { }
export * from './api'

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "src",
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"compilerOptions": {
"baseUrl": "src",
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./typings",
"paths": {
"tabby-*": ["../../tabby-*"],
"*": ["../../app/node_modules/*"]
}
}
}

View File

@ -0,0 +1,5 @@
const config = require('../webpack.plugin.config')
module.exports = config({
name: 'linkifier',
dirname: __dirname,
})

View File

@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
untildify@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==

View File

@ -1,5 +1,4 @@
Tabby Local Plugin # Tabby Local Plugin
---------------------
* local shells * local shells

View File

@ -3,13 +3,10 @@ import { compare as semverCompare } from 'semver'
import { Observable, from, forkJoin, map } from 'rxjs' import { Observable, from, forkJoin, map } from 'rxjs'
import { Injectable, Inject } from '@angular/core' import { Injectable, Inject } from '@angular/core'
import { Logger, LogService, PlatformService, BOOTSTRAP_DATA, BootstrapData, PluginInfo } from 'tabby-core' import { Logger, LogService, PlatformService, BOOTSTRAP_DATA, BootstrapData, PluginInfo } from 'tabby-core'
import { PLUGIN_BLACKLIST } from '../../../app/src/pluginBlacklist'
const OFFICIAL_NPM_ACCOUNT = 'eugenepankov' const OFFICIAL_NPM_ACCOUNT = 'eugenepankov'
const BLACKLIST = [
'terminus-shell-selector', // superseded by profiles
'terminus-scrollbar', // now useless
]
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PluginManagerService { export class PluginManagerService {
@ -69,7 +66,7 @@ export class PluginManagerService {
})) }))
), ),
map(plugins => plugins.filter(x => x.packageName.startsWith(namePrefix))), map(plugins => plugins.filter(x => x.packageName.startsWith(namePrefix))),
map(plugins => plugins.filter(x => !BLACKLIST.includes(x.packageName))), map(plugins => plugins.filter(x => !PLUGIN_BLACKLIST.includes(x.packageName))),
map(plugins => { map(plugins => {
const mapping: Record<string, PluginInfo[]> = {} const mapping: Record<string, PluginInfo[]> = {}
for (const p of plugins) { for (const p of plugins) {

View File

@ -1,5 +1,4 @@
Tabby Settings Plugin # Tabby Settings Plugin
------------------------
* tabbed settings interface * tabbed settings interface

View File

@ -16,7 +16,7 @@
> .nav { > .nav {
padding: 20px 10px; padding: 20px 10px;
width: 212px; width: 222px;
flex: none; flex: none;
overflow-y: auto; overflow-y: auto;
flex-wrap: nowrap; flex-wrap: nowrap;

View File

@ -1,5 +1,4 @@
Tabby Terminal Plugin # Tabby Terminal Plugin
------------------------
* terminal tabs * terminal tabs
* terminal frontends * terminal frontends

View File

@ -1,169 +1,203 @@
h3.mb-3 Terminal div
h3.mb-3 Rendering
.form-line(*ngIf='hostApp.platform !== Platform.Web') .form-line(*ngIf='hostApp.platform !== Platform.Web')
.header .header
.title Frontend .title Frontend
.description Switches terminal frontend implementation (experimental) .description Switches terminal frontend implementation (experimental)
select.form-control( select.form-control(
[(ngModel)]='config.store.terminal.frontend', [(ngModel)]='config.store.terminal.frontend',
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
option(value='xterm') xterm option(value='xterm') xterm
option(value='xterm-webgl') xterm (WebGL) option(value='xterm-webgl') xterm (WebGL)
.form-line div.mt-4
.header h3 Keyboard
.title Terminal bell
.btn-group(
[(ngModel)]='config.store.terminal.bell',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"off"'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"visual"'
)
| Visual
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"audible"'
)
| Audible
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.bell != "audible" && (config.store.terminal.profile || "").startsWith("wsl")') .form-line
.mr-auto WSL terminal bell can only be muted via Volume Mixer .header
button.btn.btn-secondary((click)='openWSLVolumeMixer()') Show Mixer .title Use {{altKeyName}} as the Meta key
.description Lets the shell handle Meta key instead of OS
toggle(
[(ngModel)]='config.store.terminal.altIsMeta',
(ngModelChange)='config.save()',
)
.form-line .form-line
.header .header
.title Right click .title Scroll on input
.description(*ngIf='config.store.terminal.rightClick == "paste"') Long-click for context menu .description Scrolls the terminal to the bottom on user input
.btn-group( toggle(
[(ngModel)]='config.store.terminal.rightClick', [(ngModel)]='config.store.terminal.scrollOnInput',
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
ngbRadioGroup )
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='off'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='menu'
)
| Context menu
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='paste'
)
| Paste
.form-line div.mt-4
.header h3 Mouse
.title Paste on middle-click
toggle( .form-line
[(ngModel)]='config.store.terminal.pasteOnMiddleClick', .header
(ngModelChange)='config.save()', .title Right click
) .description(*ngIf='config.store.terminal.rightClick == "paste"') Long-click for context menu
.btn-group(
[(ngModel)]='config.store.terminal.rightClick',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='off'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='menu'
)
| Context menu
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='paste'
)
| Paste
.form-line(*ngIf='hostApp.platform !== Platform.Web') .form-line
.header .header
.title Auto-open a terminal on app start .title Paste on middle-click
toggle( toggle(
[(ngModel)]='config.store.terminal.autoOpen', [(ngModel)]='config.store.terminal.pasteOnMiddleClick',
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
.form-line .form-line
.header .header
.title Restore terminal tabs on app start .title Word separators
.description Double-click selection will stop at these characters
input.form-control(
type='text',
placeholder=' ()[]{}\'"',
[(ngModel)]='config.store.terminal.wordSeparator',
(ngModelChange)='config.save()',
)
toggle( .form-line
[(ngModel)]='config.store.recoverTabs', .header
(ngModelChange)='config.save()', .title Require a key to click links
) .description When enabled, links are only clickable while holding this key
.form-line select.form-control(
.header [(ngModel)]='config.store.clickableLinks.modifier',
.title Bracketed paste (requires shell support) (ngModelChange)='config.save()',
.description Prevents accidental execution of pasted commands )
toggle( option([value]='null') None
[(ngModel)]='config.store.terminal.bracketedPaste', option(value='ctrlKey') Ctrl
(ngModelChange)='config.save()', option(value='altKey') {{altKeyName}}
) option(value='shiftKey') Shift
option(value='metaKey') {{metaKeyName}}
.form-line div.mt-4
.header h3 Clipboard
.title Copy on select
toggle(
[(ngModel)]='config.store.terminal.copyOnSelect',
(ngModelChange)='config.save()',
)
.form-line .form-line
.header .header
.title Scroll on input .title Copy on select
.description Scrolls the terminal to the bottom on user input toggle(
toggle( [(ngModel)]='config.store.terminal.copyOnSelect',
[(ngModel)]='config.store.terminal.scrollOnInput', (ngModelChange)='config.save()',
(ngModelChange)='config.save()', )
)
.form-line .form-line
.header .header
.title Use Alt key as the Meta key .title Bracketed paste (requires shell support)
.description Lets the shell handle Meta key instead of OS .description Prevents accidental execution of pasted commands
toggle( toggle(
[(ngModel)]='config.store.terminal.altIsMeta', [(ngModel)]='config.store.terminal.bracketedPaste',
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
.form-line .form-line
.header .header
.title Word separators .title Warn on multi-line paste
.description Double-click selection will stop at these characters .description Show a confirmation box when pasting multiple lines
input.form-control( toggle(
type='text', [(ngModel)]='config.store.terminal.warnOnMultilinePaste',
placeholder=' ()[]{}\'"', (ngModelChange)='config.save()',
[(ngModel)]='config.store.terminal.wordSeparator', )
(ngModelChange)='config.save()',
)
.form-line div.mt-4
.header h3 Sound
.title Warn on multi-line paste
.description Show a confirmation box when pasting multiple lines
toggle(
[(ngModel)]='config.store.terminal.warnOnMultilinePaste',
(ngModelChange)='config.save()',
)
.form-line(*ngIf='hostApp.platform === Platform.Windows') .form-line
.header .header
.title Set Tabby as %COMSPEC% .title Terminal bell
.description Allows opening .bat files in tabs, but breaks some shells .btn-group(
toggle( [(ngModel)]='config.store.terminal.bell',
[(ngModel)]='config.store.terminal.setComSpec', (ngModelChange)='config.save()',
(ngModelChange)='config.save()', ngbRadioGroup
) )
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"off"'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"visual"'
)
| Visual
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"audible"'
)
| Audible
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.bell != "audible" && (config.store.terminal.profile || "").startsWith("wsl")')
.mr-auto WSL terminal bell can only be muted via Volume Mixer
button.btn.btn-secondary((click)='openWSLVolumeMixer()') Show Mixer
div.mt-4
h3 Startup
.form-line(*ngIf='hostApp.platform !== Platform.Web')
.header
.title Auto-open a terminal on app start
toggle(
[(ngModel)]='config.store.terminal.autoOpen',
(ngModelChange)='config.save()',
)
.form-line
.header
.title Restore terminal tabs on app start
toggle(
[(ngModel)]='config.store.recoverTabs',
(ngModelChange)='config.save()',
)
div.mt-4(*ngIf='hostApp.platform === Platform.Windows')
h3 Windows
.form-line
.header
.title Set Tabby as %COMSPEC%
.description Allows opening .bat files in tabs, but breaks some shells
toggle(
[(ngModel)]='config.store.terminal.setComSpec',
(ngModelChange)='config.save()',
)

View File

@ -1,5 +1,5 @@
import { Component, HostBinding } from '@angular/core' import { Component, HostBinding } from '@angular/core'
import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core' import { ConfigService, HostAppService, Platform, PlatformService, altKeyName, metaKeyName } from 'tabby-core'
/** @hidden */ /** @hidden */
@Component({ @Component({
@ -7,6 +7,8 @@ import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-
}) })
export class TerminalSettingsTabComponent { export class TerminalSettingsTabComponent {
Platform = Platform Platform = Platform
altKeyName = altKeyName
metaKeyName = metaKeyName
@HostBinding('class.content-box') true @HostBinding('class.content-box') true