diff --git a/app/src/pluginBlacklist.ts b/app/src/pluginBlacklist.ts new file mode 100644 index 00000000..5b572449 --- /dev/null +++ b/app/src/pluginBlacklist.ts @@ -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 +] diff --git a/app/src/plugins.ts b/app/src/plugins.ts index 0873e589..9e5b6d03 100644 --- a/app/src/plugins.ts +++ b/app/src/plugins.ts @@ -2,6 +2,7 @@ import * as fs from 'mz/fs' import * as path from 'path' import * as remote from '@electron/remote' 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 @@ -109,7 +110,7 @@ export async function findPlugins (): Promise { }) } 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 }) } } diff --git a/package.json b/package.json index 682a8ed1..5ad40fc3 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "start": "cross-env TABBY_DEV=1 electron app --debug --inspect", "start:prod": "electron app --debug", "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", "postinstall": "patch-package && node ./scripts/install-deps.js" }, diff --git a/scripts/build-docs.js b/scripts/build-docs.js new file mode 100755 index 00000000..933cf2e4 --- /dev/null +++ b/scripts/build-docs.js @@ -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`) +}) diff --git a/scripts/vars.js b/scripts/vars.js index 15829fd5..bd7de09a 100755 --- a/scripts/vars.js +++ b/scripts/vars.js @@ -25,6 +25,14 @@ exports.builtinPlugins = [ 'tabby-electron', 'tabby-local', 'tabby-plugin-manager', + 'tabby-linkifier', +] + +exports.packagesWithDocs = [ + ['.', 'tabby-core'], + ['terminal', 'tabby-terminal'], + ['local', 'tabby-local'], + ['settings', 'tabby-settings'], ] exports.allPackages = [ diff --git a/tabby-core/README.md b/tabby-core/README.md index cf1c87b7..2d9b9163 100644 --- a/tabby-core/README.md +++ b/tabby-core/README.md @@ -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 * toolbar UI diff --git a/tabby-core/src/api/index.ts b/tabby-core/src/api/index.ts index 90057a71..d97630c4 100644 --- a/tabby-core/src/api/index.ts +++ b/tabby-core/src/api/index.ts @@ -25,7 +25,7 @@ export { DockingService, Screen } from '../services/docking.service' export { Logger, ConsoleLogger, LogService } from '../services/log.service' export { HomeBaseService } from '../services/homeBase.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 { ThemesService } from '../services/themes.service' export { ProfilesService } from '../services/profiles.service' diff --git a/tabby-linkifier/README.md b/tabby-linkifier/README.md new file mode 100644 index 00000000..081200e9 --- /dev/null +++ b/tabby-linkifier/README.md @@ -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. diff --git a/tabby-linkifier/package.json b/tabby-linkifier/package.json new file mode 100644 index 00000000..14ae0a6d --- /dev/null +++ b/tabby-linkifier/package.json @@ -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" + } +} diff --git a/tabby-linkifier/src/api.ts b/tabby-linkifier/src/api.ts new file mode 100644 index 00000000..255bef48 --- /dev/null +++ b/tabby-linkifier/src/api.ts @@ -0,0 +1,16 @@ +import { BaseTerminalTabComponent } from 'tabby-terminal' + +export abstract class LinkHandler { + regex: RegExp + priority = 1 + + convert (uri: string, _tab?: BaseTerminalTabComponent): Promise|string { + return uri + } + + verify (_uri: string, _tab?: BaseTerminalTabComponent): Promise|boolean { + return true + } + + abstract handle (uri: string, tab?: BaseTerminalTabComponent): void +} diff --git a/tabby-linkifier/src/config.ts b/tabby-linkifier/src/config.ts new file mode 100644 index 00000000..2700f171 --- /dev/null +++ b/tabby-linkifier/src/config.ts @@ -0,0 +1,12 @@ +import { ConfigProvider } from 'tabby-core' + +/** @hidden */ +export class ClickableLinksConfigProvider extends ConfigProvider { + defaults = { + clickableLinks: { + modifier: null, + }, + } + + platformDefaults = { } +} diff --git a/tabby-linkifier/src/decorator.ts b/tabby-linkifier/src/decorator.ts new file mode 100644 index 00000000..0c4e80b9 --- /dev/null +++ b/tabby-linkifier/src/decorator.ts @@ -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] + } +} diff --git a/tabby-linkifier/src/handlers.ts b/tabby-linkifier/src/handlers.ts new file mode 100644 index 00000000..1ec67cf7 --- /dev/null +++ b/tabby-linkifier/src/handlers.ts @@ -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 { + try { + await fs.access(uri) + return true + } catch { + return false + } + } + + async convert (uri: string, tab?: BaseTerminalTabComponent): Promise { + 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 { + const sanitizedUri = uri.replace(/"/g, '') + return super.convert(sanitizedUri, tab) + } +} diff --git a/tabby-linkifier/src/index.ts b/tabby-linkifier/src/index.ts new file mode 100644 index 00000000..beaf6c45 --- /dev/null +++ b/tabby-linkifier/src/index.ts @@ -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' diff --git a/tabby-linkifier/tsconfig.json b/tabby-linkifier/tsconfig.json new file mode 100644 index 00000000..e0611107 --- /dev/null +++ b/tabby-linkifier/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "baseUrl": "src", + } +} diff --git a/tabby-linkifier/tsconfig.typings.json b/tabby-linkifier/tsconfig.typings.json new file mode 100644 index 00000000..9188bdf7 --- /dev/null +++ b/tabby-linkifier/tsconfig.typings.json @@ -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/*"] + } + } +} diff --git a/tabby-linkifier/webpack.config.js b/tabby-linkifier/webpack.config.js new file mode 100644 index 00000000..2f08b443 --- /dev/null +++ b/tabby-linkifier/webpack.config.js @@ -0,0 +1,5 @@ +const config = require('../webpack.plugin.config') +module.exports = config({ + name: 'linkifier', + dirname: __dirname, +}) diff --git a/tabby-linkifier/yarn.lock b/tabby-linkifier/yarn.lock new file mode 100644 index 00000000..92ca8cf9 --- /dev/null +++ b/tabby-linkifier/yarn.lock @@ -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== diff --git a/tabby-local/README.md b/tabby-local/README.md index b3b7d2db..e44fa60e 100644 --- a/tabby-local/README.md +++ b/tabby-local/README.md @@ -1,5 +1,4 @@ -Tabby Local Plugin ---------------------- +# Tabby Local Plugin * local shells diff --git a/tabby-plugin-manager/src/services/pluginManager.service.ts b/tabby-plugin-manager/src/services/pluginManager.service.ts index dae064ff..5f89fbbf 100644 --- a/tabby-plugin-manager/src/services/pluginManager.service.ts +++ b/tabby-plugin-manager/src/services/pluginManager.service.ts @@ -3,13 +3,10 @@ import { compare as semverCompare } from 'semver' import { Observable, from, forkJoin, map } from 'rxjs' import { Injectable, Inject } from '@angular/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 BLACKLIST = [ - 'terminus-shell-selector', // superseded by profiles - 'terminus-scrollbar', // now useless -] @Injectable({ providedIn: 'root' }) export class PluginManagerService { @@ -69,7 +66,7 @@ export class PluginManagerService { })) ), 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 => { const mapping: Record = {} for (const p of plugins) { diff --git a/tabby-settings/README.md b/tabby-settings/README.md index 2bcb29a9..237ee4d3 100644 --- a/tabby-settings/README.md +++ b/tabby-settings/README.md @@ -1,5 +1,4 @@ -Tabby Settings Plugin ------------------------- +# Tabby Settings Plugin * tabbed settings interface diff --git a/tabby-settings/src/components/settingsTab.component.scss b/tabby-settings/src/components/settingsTab.component.scss index a871ada6..01c06bda 100644 --- a/tabby-settings/src/components/settingsTab.component.scss +++ b/tabby-settings/src/components/settingsTab.component.scss @@ -16,7 +16,7 @@ > .nav { padding: 20px 10px; - width: 212px; + width: 222px; flex: none; overflow-y: auto; flex-wrap: nowrap; diff --git a/tabby-terminal/README.md b/tabby-terminal/README.md index 5e4acbf0..8f4801e5 100644 --- a/tabby-terminal/README.md +++ b/tabby-terminal/README.md @@ -1,5 +1,4 @@ -Tabby Terminal Plugin ------------------------- +# Tabby Terminal Plugin * terminal tabs * terminal frontends diff --git a/tabby-terminal/src/components/terminalSettingsTab.component.pug b/tabby-terminal/src/components/terminalSettingsTab.component.pug index 2ada3139..4286d78e 100644 --- a/tabby-terminal/src/components/terminalSettingsTab.component.pug +++ b/tabby-terminal/src/components/terminalSettingsTab.component.pug @@ -1,169 +1,203 @@ -h3.mb-3 Terminal +div + h3.mb-3 Rendering -.form-line(*ngIf='hostApp.platform !== Platform.Web') - .header - .title Frontend - .description Switches terminal frontend implementation (experimental) + .form-line(*ngIf='hostApp.platform !== Platform.Web') + .header + .title Frontend + .description Switches terminal frontend implementation (experimental) - select.form-control( - [(ngModel)]='config.store.terminal.frontend', - (ngModelChange)='config.save()', - ) - option(value='xterm') xterm - option(value='xterm-webgl') xterm (WebGL) + select.form-control( + [(ngModel)]='config.store.terminal.frontend', + (ngModelChange)='config.save()', + ) + option(value='xterm') xterm + option(value='xterm-webgl') xterm (WebGL) -.form-line - .header - .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 +div.mt-4 + h3 Keyboard -.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 + .form-line + .header + .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 - .header - .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 + .header + .title Scroll on input + .description Scrolls the terminal to the bottom on user input + toggle( + [(ngModel)]='config.store.terminal.scrollOnInput', + (ngModelChange)='config.save()', + ) -.form-line - .header - .title Paste on middle-click +div.mt-4 + h3 Mouse - toggle( - [(ngModel)]='config.store.terminal.pasteOnMiddleClick', - (ngModelChange)='config.save()', - ) + .form-line + .header + .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') - .header - .title Auto-open a terminal on app start + .form-line + .header + .title Paste on middle-click - toggle( - [(ngModel)]='config.store.terminal.autoOpen', - (ngModelChange)='config.save()', - ) + toggle( + [(ngModel)]='config.store.terminal.pasteOnMiddleClick', + (ngModelChange)='config.save()', + ) -.form-line - .header - .title Restore terminal tabs on app start + .form-line + .header + .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( - [(ngModel)]='config.store.recoverTabs', - (ngModelChange)='config.save()', - ) + .form-line + .header + .title Require a key to click links + .description When enabled, links are only clickable while holding this key -.form-line - .header - .title Bracketed paste (requires shell support) - .description Prevents accidental execution of pasted commands - toggle( - [(ngModel)]='config.store.terminal.bracketedPaste', - (ngModelChange)='config.save()', - ) + select.form-control( + [(ngModel)]='config.store.clickableLinks.modifier', + (ngModelChange)='config.save()', + ) + option([value]='null') None + option(value='ctrlKey') Ctrl + option(value='altKey') {{altKeyName}} + option(value='shiftKey') Shift + option(value='metaKey') {{metaKeyName}} -.form-line - .header - .title Copy on select - toggle( - [(ngModel)]='config.store.terminal.copyOnSelect', - (ngModelChange)='config.save()', - ) +div.mt-4 + h3 Clipboard -.form-line - .header - .title Scroll on input - .description Scrolls the terminal to the bottom on user input - toggle( - [(ngModel)]='config.store.terminal.scrollOnInput', - (ngModelChange)='config.save()', - ) + .form-line + .header + .title Copy on select + toggle( + [(ngModel)]='config.store.terminal.copyOnSelect', + (ngModelChange)='config.save()', + ) -.form-line - .header - .title Use Alt key 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 + .header + .title Bracketed paste (requires shell support) + .description Prevents accidental execution of pasted commands + toggle( + [(ngModel)]='config.store.terminal.bracketedPaste', + (ngModelChange)='config.save()', + ) -.form-line - .header - .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()', - ) + .form-line + .header + .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 - .header - .title Warn on multi-line paste - .description Show a confirmation box when pasting multiple lines - toggle( - [(ngModel)]='config.store.terminal.warnOnMultilinePaste', - (ngModelChange)='config.save()', - ) +div.mt-4 + h3 Sound -.form-line(*ngIf='hostApp.platform === Platform.Windows') - .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()', - ) + .form-line + .header + .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")') + .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()', + ) diff --git a/tabby-terminal/src/components/terminalSettingsTab.component.ts b/tabby-terminal/src/components/terminalSettingsTab.component.ts index a200c06a..6ee2cbd7 100644 --- a/tabby-terminal/src/components/terminalSettingsTab.component.ts +++ b/tabby-terminal/src/components/terminalSettingsTab.component.ts @@ -1,5 +1,5 @@ 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 */ @Component({ @@ -7,6 +7,8 @@ import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby- }) export class TerminalSettingsTabComponent { Platform = Platform + altKeyName = altKeyName + metaKeyName = metaKeyName @HostBinding('class.content-box') true