project rename

This commit is contained in:
Eugene Pankov
2021-06-29 23:57:04 +02:00
parent c61be3d52b
commit 43cd3318da
609 changed files with 510 additions and 530 deletions

1
tabby-local/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

23
tabby-local/README.md Normal file
View File

@@ -0,0 +1,23 @@
Tabby Local Plugin
---------------------
* local shells
Using the API:
```ts
import { ShellProvider } from 'tabby-local'
```
Exporting your subclasses:
```ts
@NgModule({
...
providers: [
...
{ provide: ShellProvider, useClass: MyShellPlugin, multi: true },
...
]
})
```

47
tabby-local/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "tabby-local",
"version": "1.0.140",
"description": "Tabby's local shell plugin",
"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",
"dependencies": {
"hterm-umdjs": "1.4.1",
"opentype.js": "^1.3.3"
},
"devDependencies": {
"@types/deep-equal": "^1.0.0",
"@types/shell-escape": "^0.2.0",
"ansi-colors": "^4.1.1",
"dataurl": "0.1.0",
"deep-equal": "2.0.5",
"ps-node": "^0.1.6",
"runes": "^0.4.2",
"shell-escape": "^0.2.0",
"slugify": "^1.5.3",
"utils-decorators": "^1.8.3"
},
"peerDependencies": {
"@angular/animations": "^9.1.9",
"@angular/common": "^9.1.11",
"@angular/core": "^9.1.9",
"@angular/forms": "^9.1.11",
"@angular/platform-browser": "^9.1.11",
"@ng-bootstrap/ng-bootstrap": "^6.1.0",
"rxjs": "^6.5.5",
"tabby-core": "*",
"tabby-settings": "*",
"tabby-terminal": "*"
}
}

57
tabby-local/src/api.ts Normal file
View File

@@ -0,0 +1,57 @@
export interface Shell {
id: string
name?: string
command: string
args?: string[]
env: Record<string, string>
/**
* Base path to which shell's internal FS is relative
* Currently used for WSL only
*/
fsBase?: string
/**
* SVG icon
*/
icon?: string
hidden?: boolean
}
/**
* Extend to add support for more shells
*/
export abstract class ShellProvider {
abstract provide (): Promise<Shell[]>
}
export interface SessionOptions {
restoreFromPTYID?: string
name?: string
command: string
args?: string[]
cwd?: string
env?: Record<string, string>
width?: number
height?: number
pauseAfterExit?: boolean
runAsAdministrator?: boolean
}
export interface Profile {
name: string
color?: string
sessionOptions: SessionOptions
shell?: string
isBuiltin?: boolean
icon?: string
disableDynamicTitle?: boolean
}
export interface ChildProcess {
pid: number
ppid: number
command: string
}

View File

@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, ToolbarButton, ConfigService, SelectorOption, SelectorService } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
electron: ElectronService,
private selector: SelectorService,
private config: ConfigService,
private terminal: TerminalService,
) {
super()
}
async activate () {
const options: SelectorOption<void>[] = []
const profiles = await this.terminal.getProfiles({ skipDefault: !this.config.store.terminal.showDefaultProfiles })
for (const profile of profiles) {
options.push({
icon: profile.icon,
name: profile.name,
callback: () => this.terminal.openTab(profile),
})
}
await this.selector.show('Select profile', options)
}
provide (): ToolbarButton[] {
return [
{
icon: require('./icons/plus.svg'),
title: 'New terminal',
touchBarNSImage: 'NSTouchBarAddDetailTemplate',
click: () => {
this.terminal.openTab()
},
},
{
icon: require('./icons/profiles.svg'),
title: 'New terminal with profile',
click: () => this.activate(),
},
]
}
}

119
tabby-local/src/cli.ts Normal file
View File

@@ -0,0 +1,119 @@
import * as path from 'path'
import * as fs from 'mz/fs'
import { Injectable } from '@angular/core'
import { CLIHandler, CLIEvent, AppService, ConfigService, HostWindowService } from 'tabby-core'
import { TerminalService } from './services/terminal.service'
@Injectable()
export class TerminalCLIHandler extends CLIHandler {
firstMatchOnly = true
priority = 0
constructor (
private config: ConfigService,
private hostWindow: HostWindowService,
private terminal: TerminalService,
) {
super()
}
async handle (event: CLIEvent): Promise<boolean> {
const op = event.argv._[0]
if (op === 'open') {
this.handleOpenDirectory(path.resolve(event.cwd, event.argv.directory))
} else if (op === 'run') {
this.handleRunCommand(event.argv.command)
} else if (op === 'profile') {
this.handleOpenProfile(event.argv.profileName)
} else {
return false
}
return true
}
private async handleOpenDirectory (directory: string) {
if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) {
directory = directory.substring(0, directory.length - 1)
}
if (await fs.exists(directory)) {
if ((await fs.stat(directory)).isDirectory()) {
this.terminal.openTab(undefined, directory)
this.hostWindow.bringToFront()
}
}
}
private handleRunCommand (command: string[]) {
this.terminal.openTab({
name: '',
sessionOptions: {
command: command[0],
args: command.slice(1),
},
}, null, true)
this.hostWindow.bringToFront()
}
private handleOpenProfile (profileName: string) {
const profile = this.config.store.terminal.profiles.find(x => x.name === profileName)
if (!profile) {
console.error('Requested profile', profileName, 'not found')
return
}
this.terminal.openTabWithOptions(profile.sessionOptions)
this.hostWindow.bringToFront()
}
}
@Injectable()
export class OpenPathCLIHandler extends CLIHandler {
firstMatchOnly = true
priority = -100
constructor (
private terminal: TerminalService,
private hostWindow: HostWindowService,
) {
super()
}
async handle (event: CLIEvent): Promise<boolean> {
const op = event.argv._[0]
const opAsPath = op ? path.resolve(event.cwd, op) : null
if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) {
this.terminal.openTab(undefined, opAsPath)
this.hostWindow.bringToFront()
return true
}
return false
}
}
@Injectable()
export class AutoOpenTabCLIHandler extends CLIHandler {
firstMatchOnly = true
priority = -1000
constructor (
private app: AppService,
private config: ConfigService,
private terminal: TerminalService,
) {
super()
}
async handle (event: CLIEvent): Promise<boolean> {
if (!event.secondInstance && this.config.store.terminal.autoOpen) {
this.app.ready$.subscribe(() => {
this.terminal.openTab()
})
return true
}
return false
}
}

View File

@@ -0,0 +1,73 @@
.modal-body
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.name',
)
.form-group
label Command
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.command',
)
.form-group
label Arguments
.input-group(
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
)
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.args[i]',
)
.input-group-append
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
i.fas.fa-trash
.mt-2
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
i.fas.fa-plus.mr-2
| Add
.form-line(*ngIf='uac.isAvailable')
.header
.title Run as administrator
toggle(
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
)
.form-group
label Working directory
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.cwd',
)
.form-group
label Environment
environment-editor(
type='text',
[(model)]='profile.sessionOptions.env',
)
.form-group
label Tab color
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.color',
placeholder='#000000'
)
.form-line
.header
.title Disable dynamic tab title
.description Connection name will be used as a title instead
toggle([(ngModel)]='profile.disableDynamicTitle')
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save
button.btn.btn-outline-danger((click)='cancel()') Cancel

View File

@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { UACService } from '../services/uac.service'
import { Profile } from '../api'
/** @hidden */
@Component({
template: require('./editProfileModal.component.pug'),
})
export class EditProfileModalComponent {
profile: Profile
constructor (
public uac: UACService,
private modalInstance: NgbActiveModal,
) {
}
ngOnInit () {
this.profile.sessionOptions.env = this.profile.sessionOptions.env ?? {}
this.profile.sessionOptions.args = this.profile.sessionOptions.args ?? []
}
save () {
this.modalInstance.close(this.profile)
}
cancel () {
this.modalInstance.dismiss()
}
trackByIndex (index) {
return index
}
}

View File

@@ -0,0 +1,13 @@
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
.input-group
input.form-control.w-25([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
.input-group-append
.input-group-text =
input.form-control.w-50([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
.input-group-append
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
i.fas.fa-trash
button.btn.btn-secondary((click)='addEnvironmentVar()')
i.fas.fa-plus.mr-2
span Add

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,47 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Output, Input } from '@angular/core'
import { Subject } from 'rxjs'
/** @hidden */
@Component({
selector: 'environment-editor',
template: require('./environmentEditor.component.pug'),
styles: [require('./environmentEditor.component.scss')],
})
export class EnvironmentEditorComponent {
@Output() modelChange = new Subject<any>()
vars: { key: string, value: string }[] = []
private cachedModel: any
@Input() get model (): any {
return this.cachedModel
}
set model (value) {
this.vars = Object.entries(value).map(([k, v]) => ({ key: k, value: v as string }))
this.cachedModel = this.getModel()
}
getModel () {
const model = {}
for (const pair of this.vars) {
model[pair.key] = pair.value
}
return model
}
emitUpdate () {
this.cachedModel = this.getModel()
this.modelChange.next(this.cachedModel)
}
addEnvironmentVar () {
this.vars.push({ key: '', value: '' })
}
removeEnvironmentVar (key: string) {
this.vars = this.vars.filter(x => x.key !== key)
this.emitUpdate()
}
}

View File

@@ -0,0 +1,104 @@
h3.mb-3 Shell
.form-line
.header
.title Profile
.description Default profile for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.profile',
(ngModelChange)='config.save()',
)
option(
*ngFor='let profile of profiles',
[ngValue]='terminal.getProfileID(profile)'
) {{profile.name}}
.form-line(*ngIf='isConPTYAvailable')
.header
.title Use ConPTY
.description Enables the experimental Windows ConPTY API
toggle(
[(ngModel)]='config.store.terminal.useConPTY',
(ngModelChange)='config.save()'
)
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.useConPTY && isConPTYAvailable && !isConPTYStable')
.mr-auto Windows 10 build 18309 or above is recommended for ConPTY
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.profile.startsWith("WSL") && (!config.store.terminal.useConPTY)')
.mr-auto WSL terminal only supports TrueColor with ConPTY
.form-line(*ngIf='config.store.terminal.profile == "custom-shell"')
.header
.title Custom shell
input.form-control(
type='text',
[(ngModel)]='config.store.terminal.customShell',
(ngModelChange)='config.save()',
)
.form-line
.header
.title Working directory
.input-group
input.form-control(
type='text',
placeholder='Home directory',
[(ngModel)]='config.store.terminal.workingDirectory',
(ngModelChange)='config.save()',
)
.input-group-append
button.btn.btn-secondary((click)='pickWorkingDirectory()')
i.fas.fa-folder-open
.form-line
.header
.title Directory for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory',
(ngModelChange)='config.save()',
)
option([ngValue]='false') Same as active tab's directory
option([ngValue]='true') The working directory from above
.form-line.align-items-start
.header
.title Environment
.description Inject additional environment variables
environment-editor([(model)]='this.config.store.terminal.environment')
.form-line(*ngIf='config.store.terminal.profiles.length > 0')
.header
.title Show default profiles in the selector
.description If disabled, only custom profiles will show up in the profile selector
toggle(
[(ngModel)]='config.store.terminal.showDefaultProfiles',
(ngModelChange)='config.save()'
)
h3.mt-3 Saved Profiles
.list-group.list-group-flush.mt-3.mb-3
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let profile of config.store.terminal.profiles',
(click)='editProfile(profile)',
)
.mr-auto
div {{profile.name}}
.text-muted {{profile.sessionOptions.command}}
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteProfile(profile)')
i.fas.fa-trash
.pb-4(ngbDropdown, placement='top-left')
button.btn.btn-primary(ngbDropdownToggle)
i.fas.fa-fw.fa-plus
| New profile
div(ngbDropdownMenu)
button.dropdown-item(*ngFor='let shell of shells', (click)='newProfile(shell)') {{shell.name}}

View File

@@ -0,0 +1,93 @@
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { ConfigService, HostAppService, Platform, WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from 'tabby-core'
import { ElectronService, ElectronHostWindow } from 'tabby-electron'
import { EditProfileModalComponent } from './editProfileModal.component'
import { Shell, Profile } from '../api'
import { TerminalService } from '../services/terminal.service'
/** @hidden */
@Component({
template: require('./shellSettingsTab.component.pug'),
})
export class ShellSettingsTabComponent {
shells: Shell[] = []
profiles: Profile[] = []
Platform = Platform
isConPTYAvailable: boolean
isConPTYStable: boolean
private configSubscription: Subscription
constructor (
public config: ConfigService,
public hostApp: HostAppService,
public hostWindow: ElectronHostWindow,
public terminal: TerminalService,
private electron: ElectronService,
private ngbModal: NgbModal,
) {
config.store.terminal.environment = config.store.terminal.environment || {}
this.configSubscription = this.config.changed$.subscribe(() => {
this.reload()
})
this.reload()
this.isConPTYAvailable = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED)
this.isConPTYStable = isWindowsBuild(WIN_BUILD_CONPTY_STABLE)
}
async ngOnInit (): Promise<void> {
this.shells = (await this.terminal.shells$.toPromise())!
}
ngOnDestroy (): void {
this.configSubscription.unsubscribe()
}
async reload (): Promise<void> {
this.profiles = await this.terminal.getProfiles({ includeHidden: true })
}
async pickWorkingDirectory (): Promise<void> {
const profile = await this.terminal.getProfileByID(this.config.store.terminal.profile)
const shell = this.shells.find(x => x.id === profile?.shell)
if (!shell) {
return
}
const paths = (await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],
}
)).filePaths
this.config.store.terminal.workingDirectory = paths[0]
}
newProfile (shell: Shell): void {
const profile: Profile = {
name: shell.name ?? '',
shell: shell.id,
sessionOptions: this.terminal.optionsFromShell(shell),
}
this.config.store.terminal.profiles = [profile, ...this.config.store.terminal.profiles]
this.config.save()
this.reload()
}
editProfile (profile: Profile): void {
const modal = this.ngbModal.open(EditProfileModalComponent)
modal.componentInstance.profile = Object.assign({}, profile)
modal.result.then(result => {
Object.assign(profile, result)
this.config.save()
})
}
deleteProfile (profile: Profile): void {
this.config.store.terminal.profiles = this.config.store.terminal.profiles.filter(x => x !== profile)
this.config.save()
this.reload()
}
}

View File

@@ -0,0 +1,107 @@
import { Component, Input, Injector } from '@angular/core'
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'tabby-core'
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { SessionOptions } from '../api'
import { Session } from '../session'
/** @hidden */
@Component({
selector: 'terminalTab',
template: BaseTerminalTabComponent.template,
styles: BaseTerminalTabComponent.styles,
animations: BaseTerminalTabComponent.animations,
})
export class TerminalTabComponent extends BaseTerminalTabComponent {
@Input() sessionOptions: SessionOptions
session: Session|null = null
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (
injector: Injector,
) {
super(injector)
}
ngOnInit (): void {
this.logger = this.log.create('terminalTab')
this.session = new Session(this.injector)
const isConPTY = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) {
return
}
switch (hotkey) {
case 'home':
this.sendInput(isConPTY ? '\x1b[H' : '\x1bOH')
break
case 'end':
this.sendInput(isConPTY ? '\x1b[F' : '\x1bOF')
break
}
})
super.ngOnInit()
}
protected onFrontendReady (): void {
this.initializeSession(this.size.columns, this.size.rows)
this.savedStateIsLive = this.sessionOptions.restoreFromPTYID === this.session?.getPTYID()
super.onFrontendReady()
}
initializeSession (columns: number, rows: number): void {
this.session!.start({
...this.sessionOptions,
width: columns,
height: rows,
})
this.attachSessionHandlers(true)
this.recoveryStateChangedHint.next()
}
async getRecoveryToken (): Promise<any> {
const cwd = this.session ? await this.session.getWorkingDirectory() : null
return {
type: 'app:terminal-tab',
sessionOptions: {
...this.sessionOptions,
cwd: cwd ?? this.sessionOptions.cwd,
restoreFromPTYID: this.session?.getPTYID(),
},
savedState: this.frontend?.saveState(),
}
}
async getCurrentProcess (): Promise<BaseTabProcess|null> {
const children = await this.session?.getChildProcesses()
if (!children?.length) {
return null
}
return {
name: children[0].command,
}
}
async canClose (): Promise<boolean> {
const children = await this.session?.getChildProcesses()
if (!children?.length) {
return true
}
return (await this.platform.showMessageBox(
{
type: 'warning',
message: `"${children[0].command}" is still running. Close?`,
buttons: ['Cancel', 'Kill'],
defaultId: 1,
}
)).response === 1
}
ngOnDestroy (): void {
super.ngOnDestroy()
this.session?.destroy()
}
}

62
tabby-local/src/config.ts Normal file
View File

@@ -0,0 +1,62 @@
import { ConfigProvider, Platform } from 'tabby-core'
/** @hidden */
export class TerminalConfigProvider extends ConfigProvider {
defaults = {
hotkeys: {
'copy-current-path': [],
shell: {
__nonStructural: true,
},
profile: {
__nonStructural: true,
},
},
terminal: {
autoOpen: false,
customShell: '',
workingDirectory: '',
alwaysUseWorkingDirectory: false,
useConPTY: true,
showDefaultProfiles: true,
environment: {},
profiles: [],
},
}
platformDefaults = {
[Platform.macOS]: {
terminal: {
shell: 'default',
profile: 'user-default',
},
hotkeys: {
'new-tab': [
'⌘-T',
],
},
},
[Platform.Windows]: {
terminal: {
shell: 'clink',
profile: 'cmd-clink',
},
hotkeys: {
'new-tab': [
'Ctrl-Shift-T',
],
},
},
[Platform.Linux]: {
terminal: {
shell: 'default',
profile: 'user-default',
},
hotkeys: {
'new-tab': [
'Ctrl-Shift-T',
],
},
},
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core'
import { HotkeyDescription, HotkeyProvider } from 'tabby-core'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@Injectable()
export class LocalTerminalHotkeyProvider extends HotkeyProvider {
hotkeys: HotkeyDescription[] = [
{
id: 'new-tab',
name: 'New tab',
},
]
constructor (
private terminal: TerminalService,
) { super() }
async provide (): Promise<HotkeyDescription[]> {
const profiles = await this.terminal.getProfiles()
return [
...this.hotkeys,
...profiles.map(profile => ({
id: `profile.${this.terminal.getProfileID(profile)}`,
name: `New tab: ${profile.name}`,
})),
]
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="300" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" font-family="Roboto" font-size="14" text-anchor="middle" viewBox="0 0 65 57"><defs><style type="text/css"/></defs><use x=".5" y=".5" xlink:href="#A"/><symbol id="A" overflow="visible"><g fill="#0d597f" fill-rule="nonzero" stroke="none"><path d="M23.252 34.527v-6.745l-4.855 4.864c.474.333.968.635 1.48.906.463.243.87.434 1.303.58a7.97 7.97 0 0 0 1.13.304c.348.064.66.093.95.096m24.822-.562a1.17 1.17 0 0 0 .142.1c.123.078.252.145.385.203a2.93 2.93 0 0 0 .637.194c.296.06.598.088.9.087a5.84 5.84 0 0 0 .955-.087 7.24 7.24 0 0 0 1.138-.301c.453-.16.895-.354 1.32-.58.52-.274 1.02-.58 1.503-.918l-3.685-3.6-12.2-12.258-5.356 5.356-7.23-7.455L8.44 32.647c.48.337.98.644 1.5.918.47.246.9.434 1.317.58a7.18 7.18 0 0 0 1.135.301 5.53 5.53 0 0 0 .955.087 4.53 4.53 0 0 0 .9-.087 3.29 3.29 0 0 0 .637-.194c.134-.054.263-.12.385-.197l.145-.104 8.193-8.193 2.924-2.808 8.106 8.106 2.837 2.912c.046.037.094.07.145.1.122.078.25.145.385.2a3.49 3.49 0 0 0 .637.194c.255.052.556.087.903.087a5.84 5.84 0 0 0 .955-.087c.387-.068.768-.168 1.138-.3a9.94 9.94 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-6.508-6.37 1.2-1.2 5.63 5.63 3.283 3.254M47.996.02l15.998 27.714-15.99 27.694H15.998L0 27.714 15.998 0z"/><path d="M38.022 26.367L33.76 22.11l.304-.304 4.3 4.244z"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-windows fa-w-14 fa-2x" data-icon="windows" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#0ff" stroke="none" stroke-width="1" d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-windows fa-w-14 fa-2x" data-icon="windows" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#fff" stroke="none" stroke-width="1" d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-lambda fa-w-14 fa-2x" data-icon="lambda" data-prefix="fas" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#0ff" stroke="none" stroke-width="1" d="M424 384h-43.5L197.6 48.68A32.018 32.018 0 0 0 169.5 32H24C10.75 32 0 42.74 0 56v48c0 13.25 10.75 24 24 24h107.5l4.63 8.49L3.25 446.55C-3.53 462.38 8.08 480 25.31 480h52.23c9.6 0 18.28-5.72 22.06-14.55l95.02-221.72L314.4 463.32A32.018 32.018 0 0 0 342.5 480H424c13.25 0 24-10.75 24-24v-48c0-13.26-10.75-24-24-24z"/></svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-lambda fa-w-14 fa-2x" data-icon="lambda" data-prefix="fas" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#8ab91d" stroke="none" stroke-width="1" d="M424 384h-43.5L197.6 48.68A32.018 32.018 0 0 0 169.5 32H24C10.75 32 0 42.74 0 56v48c0 13.25 10.75 24 24 24h107.5l4.63 8.49L3.25 446.55C-3.53 462.38 8.08 480 25.31 480h52.23c9.6 0 18.28-5.72 22.06-14.55l95.02-221.72L314.4 463.32A32.018 32.018 0 0 0 342.5 480H424c13.25 0 24-10.75 24-24v-48c0-13.26-10.75-24-24-24z"/></svg>

After

Width:  |  Height:  |  Size: 570 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g stroke-linejoin="round" stroke-width="4"><path fill="#eee" d="M94,19l-28-9h-39c-9,0-19,9-19,19v47c0,9,10,19,19,19h39l28-10l-28-9h-26c-9,0-13-3-13-13v-22c0-10,4-13,13-13h26z"/><path fill="#0f0" d="M94,52l-41-11h-13v3c10,0,10,16,0,16v3h13z"/></g></svg>

After

Width:  |  Height:  |  Size: 315 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-git-alt fa-w-14 fa-3x" data-icon="git-alt" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#f05033" stroke="none" stroke-width="1" d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"/></svg>

After

Width:  |  Height:  |  Size: 718 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-plus fa-w-12 fa-3x" data-icon="plus" data-prefix="fal" focusable="false" role="img" viewBox="0 0 384 512"><path fill="#fff" stroke="none" stroke-width="1" d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z"/></svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-terminal fa-w-20 fa-2x" data-icon="terminal" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512"><path fill="purple" stroke="none" stroke-width="1" d="M257.981 272.971L63.638 467.314c-9.373 9.373-24.569 9.373-33.941 0L7.029 444.647c-9.357-9.357-9.375-24.522-.04-33.901L161.011 256 6.99 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L257.981 239.03c9.373 9.372 9.373 24.568 0 33.941zM640 456v-32c0-13.255-10.745-24-24-24H312c-13.255 0-24 10.745-24 24v32c0 13.255 10.745 24 24 24h304c13.255 0 24-10.745 24-24z"/></svg>

After

Width:  |  Height:  |  Size: 662 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-terminal fa-w-20 fa-2x" data-icon="terminal" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512"><path fill="#0ff" stroke="none" stroke-width="1" d="M257.981 272.971L63.638 467.314c-9.373 9.373-24.569 9.373-33.941 0L7.029 444.647c-9.357-9.357-9.375-24.522-.04-33.901L161.011 256 6.99 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L257.981 239.03c9.373 9.372 9.373 24.568 0 33.941zM640 456v-32c0-13.255-10.745-24-24-24H312c-13.255 0-24 10.745-24 24v32c0 13.255 10.745 24 24 24h304c13.255 0 24-10.745 24-24z"/></svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-window-restore fa-w-16 fa-3x" data-icon="window-restore" data-prefix="fal" focusable="false" role="img" viewBox="0 0 512 512"><path fill="#fff" stroke="none" stroke-width="1" d="M464 0H144c-26.5 0-48 21.5-48 48v48H48c-26.5 0-48 21.5-48 48v320c0 26.5 21.5 48 48 48h320c26.5 0 48-21.5 48-48v-48h48c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM32 144c0-8.8 7.2-16 16-16h320c8.8 0 16 7.2 16 16v80H32v-80zm352 320c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V256h352v208zm96-96c0 8.8-7.2 16-16 16h-48V144c0-26.5-21.5-48-48-48H128V48c0-8.8 7.2-16 16-16h320c8.8 0 16 7.2 16 16v320z"/></svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg8" width="256" height="256" version="1.1" viewBox="0 0 256 256"><metadata id="metadata5"/><g id="layer1" transform="matrix(6.9999999,0,0,6.9999736,16,-7617.6082)" style="fill:#73ba25;fill-opacity:1"><g style="fill:#73ba25;fill-opacity:1" id="g838" transform="matrix(0.26458333,0,0,0.26458333,-10.590624,-38.473045)"><circle id="path872" cx="507.464" cy="3582.83" r="0" style="opacity:.3;fill:#73ba25;fill-opacity:1;stroke:none;stroke-width:1.90573967;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/><path id="path819" d="m 100.5002,4267.059 a 60.472077,60.472442 0 0 0 -47.607132,23.2988 c 7.375955,1.9706 12.596534,3.6642 14.160064,4.1895 0.0245,-0.9593 0.183589,-9.5391 0.183589,-9.5391 0,0 0.0202,-0.1964 0.124999,-0.2988 0.13497,-0.1318 0.330078,-0.092 0.330078,-0.092 1.939989,0.281 43.348482,6.4189 60.802382,16.5899 2.15548,1.261 3.21941,2.6017 4.5488,3.9609 4.82477,4.99 11.19998,25.7389 11.88469,30.0176 0.0269,0.1681 -0.18083,0.3507 -0.26953,0.4199 h -0.002 c -0.4957,0.3868 -1.03554,0.789 -1.57616,1.1484 -4.12998,2.7709 -13.64449,9.4312 -25.85142,8.3438 -10.96493,-0.97 -25.290388,-7.2597 -42.560284,-18.6387 1.69799,3.9756 3.371,7.9635 5.04489,11.9492 2.500985,1.299 26.640524,13.5997 38.554464,13.3594 9.59593,-0.1999 19.85892,-4.8804 23.96469,-7.3516 0,0 0.90227,-0.5436 1.29491,-0.2402 0.4295,0.3318 0.31068,0.8402 0.20898,1.3594 -0.25259,1.1786 -0.82764,3.3289 -1.21873,4.3496 l -0.33008,0.832 c -0.46999,1.2592 -0.92111,2.4296 -1.79101,3.1504 -2.41868,2.1993 -6.27908,3.9491 -12.32804,6.5781 -9.34995,4.09 -24.51938,6.6911 -38.603293,6.6016 -5.04437,-0.1123 -9.91781,-0.672 -14.197174,-1.1719 -8.782187,-0.9915 -15.927854,-1.7959 -20.285038,1.3555 a 60.472077,60.472442 0 0 0 45.517305,20.7734 60.472077,60.472442 0 0 0 60.47229,-60.4726 60.472077,60.472442 0 0 0 -60.47229,-60.4727 z m 13.4882,35.0879 c -4.73327,-0.1509 -9.24668,1.5194 -12.70695,4.75 -3.458684,3.2199 -5.437952,7.6097 -5.613251,12.3399 -0.326998,9.7581 7.334241,17.9803 17.083881,18.3398 4.75477,0.1596 9.25839,-1.5118 12.71867,-4.7617 3.44988,-3.2099 5.42915,-7.5999 5.61325,-12.3301 0.335,-9.7494 -7.33546,-17.9889 -17.0956,-18.3379 z m -0.14844,5.2188 c 6.82096,0.242 12.16127,5.972 11.93157,12.791 -0.1053,3.2885 -1.49253,6.3369 -3.90231,8.5976 -2.41329,2.2502 -5.56743,3.4203 -8.87691,3.3203 -6.80516,-0.251 -12.14564,-5.9877 -11.91594,-12.8085 0.1,-3.3008 1.51475,-6.3495 3.91403,-8.5997 2.39919,-2.2502 5.53828,-3.42 8.84956,-3.3007 z m 2.02147,6.2011 c -3.03067,0 -5.47848,1.631 -5.47848,3.6602 0,2.01 2.44781,3.6504 5.47848,3.6504 3.02888,0 5.4863,-1.6405 5.4863,-3.6504 0,-2.0292 -2.45572,-3.6602 -5.4863,-3.6602 z" style="opacity:1;fill:#73ba25;fill-opacity:1;stroke:none;stroke-width:1.90559804;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 256 256"><g><path fill="#DD4814" d="M255.637396,127.683191 C255.637396,198.196551 198.47207,255.363378 127.954205,255.363378 C57.4348387,255.363378 0.27026393,198.196551 0.27026393,127.683191 C0.27026393,57.1653255 57.4355894,0 127.954205,0 C198.472821,0 255.637396,57.1653255 255.637396,127.683191 L255.637396,127.683191 Z"/><path fill="#FFF" d="M41.1334194,110.63254 C31.7139707,110.63254 24.0827683,118.264493 24.0827683,127.683191 C24.0827683,137.097384 31.7139707,144.728587 41.1334194,144.728587 C50.5476129,144.728587 58.1788152,137.097384 58.1788152,127.683191 C58.1788152,118.264493 50.5476129,110.63254 41.1334194,110.63254 L41.1334194,110.63254 Z M162.848282,188.111202 C154.694569,192.820551 151.898839,203.240727 156.608938,211.389935 C161.313032,219.543648 171.733208,222.338628 179.886921,217.629279 C188.039883,212.925185 190.835613,202.505009 186.126264,194.350545 C181.42217,186.202088 170.995988,183.407109 162.848282,188.111202 L162.848282,188.111202 Z M78.1618299,127.683191 C78.1618299,110.836739 86.5295015,95.9534545 99.3332551,86.9409032 L86.8703343,66.0667683 C71.9555191,76.0365044 60.8581818,91.271132 56.2464282,109.113806 C61.6276833,113.504845 65.0720469,120.189372 65.0720469,127.68244 C65.0720469,135.171003 61.6276833,141.855531 56.2464282,146.246569 C60.852176,164.094499 71.9495132,179.329877 86.8703343,189.299613 L99.3332551,168.420223 C86.5295015,159.412927 78.1618299,144.530393 78.1618299,127.683191 L78.1618299,127.683191 Z M127.954205,77.8855601 C153.967109,77.8855601 175.30895,97.8302874 177.549138,123.265877 L201.839859,122.907777 C200.644692,104.129689 192.441431,87.2719765 179.836622,74.875871 C173.354792,77.3247625 165.86773,76.9501466 159.396411,73.2197537 C152.91383,69.4788504 148.849361,63.1681877 147.738276,56.3177478 C141.438123,54.5790499 134.807648,53.6271202 127.952704,53.6271202 C116.168446,53.6271202 105.026815,56.3950733 95.1344047,61.2913548 L106.979472,82.5175836 C113.351695,79.5521877 120.460387,77.8855601 127.954205,77.8855601 L127.954205,77.8855601 Z M127.954205,177.475566 C120.460387,177.475566 113.351695,175.808188 106.980223,172.843543 L95.1351554,194.069021 C105.027566,198.971308 116.169196,201.740012 127.954205,201.740012 C134.80915,201.740012 141.439625,200.787331 147.739026,199.043378 C148.850111,192.192938 152.916082,185.888282 159.397161,182.140622 C165.872985,178.404223 173.355543,178.036364 179.837372,180.485255 C192.442182,168.08915 200.645443,151.231437 201.84061,132.453349 L177.543883,132.095249 C175.30895,157.537595 153.967859,177.475566 127.954205,177.475566 L127.954205,177.475566 Z M162.842276,67.2446686 C170.995988,71.9532669 181.416915,69.1642933 186.121009,61.0105806 C190.830358,52.856868 188.041384,42.4359413 179.886921,37.7258416 C171.733208,33.0217478 161.313032,35.8167273 156.602182,43.9704399 C151.898839,52.1196481 154.693818,62.5405748 162.842276,67.2446686 L162.842276,67.2446686 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

134
tabby-local/src/index.ts Normal file
View File

@@ -0,0 +1,134 @@
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TabbyCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler, ConfigService } from 'tabby-core'
import TabbyTerminalModule from 'tabby-terminal'
import TabbyElectronPlugin from 'tabby-electron'
import { SettingsTabProvider } from 'tabby-settings'
import { TerminalTabComponent } from './components/terminalTab.component'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
import { TerminalService } from './services/terminal.service'
import { DockMenuService } from './services/dockMenu.service'
import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider'
import { ShellProvider } from './api'
import { ShellSettingsTabProvider } from './settings'
import { TerminalConfigProvider } from './config'
import { LocalTerminalHotkeyProvider } from './hotkeys'
import { NewTabContextMenu, SaveAsProfileContextMenu } from './tabContextMenu'
import { CmderShellProvider } from './shells/cmder'
import { CustomShellProvider } from './shells/custom'
import { Cygwin32ShellProvider } from './shells/cygwin32'
import { Cygwin64ShellProvider } from './shells/cygwin64'
import { GitBashShellProvider } from './shells/gitBash'
import { LinuxDefaultShellProvider } from './shells/linuxDefault'
import { MacOSDefaultShellProvider } from './shells/macDefault'
import { POSIXShellsProvider } from './shells/posix'
import { PowerShellCoreShellProvider } from './shells/powershellCore'
import { WindowsDefaultShellProvider } from './shells/winDefault'
import { WindowsStockShellsProvider } from './shells/windowsStock'
import { WSLShellProvider } from './shells/wsl'
import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from './cli'
/** @hidden */
@NgModule({
imports: [
BrowserModule,
FormsModule,
NgbModule,
ToastrModule,
TabbyCorePlugin,
TabbyElectronPlugin,
TabbyTerminalModule,
],
providers: [
{ provide: SettingsTabProvider, useClass: ShellSettingsTabProvider, multi: true },
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
{ provide: HotkeyProvider, useClass: LocalTerminalHotkeyProvider, multi: true },
{ provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true },
{ provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
{ provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
{ provide: ShellProvider, useClass: WindowsStockShellsProvider, multi: true },
{ provide: ShellProvider, useClass: PowerShellCoreShellProvider, multi: true },
{ provide: ShellProvider, useClass: CmderShellProvider, multi: true },
{ provide: ShellProvider, useClass: CustomShellProvider, multi: true },
{ provide: ShellProvider, useClass: Cygwin32ShellProvider, multi: true },
{ provide: ShellProvider, useClass: Cygwin64ShellProvider, multi: true },
{ provide: ShellProvider, useClass: GitBashShellProvider, multi: true },
{ provide: ShellProvider, useClass: POSIXShellsProvider, multi: true },
{ provide: ShellProvider, useClass: WSLShellProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: NewTabContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true },
{ provide: CLIHandler, useClass: TerminalCLIHandler, multi: true },
{ provide: CLIHandler, useClass: OpenPathCLIHandler, multi: true },
{ provide: CLIHandler, useClass: AutoOpenTabCLIHandler, multi: true },
// For WindowsDefaultShellProvider
PowerShellCoreShellProvider,
WSLShellProvider,
WindowsStockShellsProvider,
],
entryComponents: [
TerminalTabComponent,
ShellSettingsTabComponent,
EditProfileModalComponent,
] as any[],
declarations: [
TerminalTabComponent,
ShellSettingsTabComponent,
EditProfileModalComponent,
EnvironmentEditorComponent,
] as any[],
exports: [
TerminalTabComponent,
EnvironmentEditorComponent,
],
})
export default class LocalTerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
private constructor (
hotkeys: HotkeysService,
terminal: TerminalService,
hostApp: HostAppService,
dockMenu: DockMenuService,
config: ConfigService,
) {
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey === 'new-tab') {
terminal.openTab()
}
if (hotkey === 'new-window') {
hostApp.newWindow()
}
if (hotkey.startsWith('profile.')) {
const profile = await terminal.getProfileByID(hotkey.split('.')[1])
if (profile) {
terminal.openTabWithOptions(profile.sessionOptions)
}
}
})
config.ready$.toPromise().then(() => {
dockMenu.update()
})
}
}
export { TerminalTabComponent }
export { TerminalService, ShellProvider }
export * from './api'

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from 'tabby-core'
import { TerminalTabComponent } from './components/terminalTab.component'
/** @hidden */
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
return recoveryToken.type === 'app:terminal-tab'
}
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
return {
type: TerminalTabComponent,
options: {
sessionOptions: recoveryToken.sessionOptions,
savedState: recoveryToken.savedState,
},
}
}
duplicate (recoveryToken: RecoveryToken): RecoveryToken {
return {
...recoveryToken,
sessionOptions: {
...recoveryToken.sessionOptions,
restoreFromPTYID: null,
},
savedState: null,
}
}
}

View File

@@ -0,0 +1,47 @@
import { NgZone, Injectable } from '@angular/core'
import { ConfigService, HostAppService, Platform } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { TerminalService } from './terminal.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class DockMenuService {
appVersion: string
private constructor (
private electron: ElectronService,
private config: ConfigService,
private hostApp: HostAppService,
private zone: NgZone,
private terminalService: TerminalService,
) {
config.changed$.subscribe(() => this.update())
}
update (): void {
if (this.hostApp.platform === Platform.Windows) {
this.electron.app.setJumpList(this.config.store.terminal.profiles.length ? [{
type: 'custom',
name: 'Profiles',
items: this.config.store.terminal.profiles.map(profile => ({
type: 'task',
program: process.execPath,
args: `profile "${profile.name}"`,
title: profile.name,
iconPath: process.execPath,
iconIndex: 0,
})),
}] : null as any)
}
if (this.hostApp.platform === Platform.macOS) {
this.electron.app.dock.setMenu(this.electron.Menu.buildFromTemplate(
this.config.store.terminal.profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(profile.sessionOptions)
}),
}))
))
}
}
}

View File

@@ -0,0 +1,150 @@
import * as fs from 'mz/fs'
import slugify from 'slugify'
import { Observable, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core'
import { AppService, Logger, LogService, ConfigService, SplitTabComponent } from 'tabby-core'
import { TerminalTabComponent } from '../components/terminalTab.component'
import { ShellProvider, Shell, SessionOptions, Profile } from '../api'
import { UACService } from './uac.service'
@Injectable({ providedIn: 'root' })
export class TerminalService {
private shells = new AsyncSubject<Shell[]>()
private logger: Logger
/**
* A fresh list of all available shells
*/
get shells$ (): Observable<Shell[]> { return this.shells }
/** @hidden */
private constructor (
private app: AppService,
private config: ConfigService,
private uac: UACService,
@Inject(ShellProvider) private shellProviders: ShellProvider[],
log: LogService,
) {
this.logger = log.create('terminal')
config.ready$.toPromise().then(() => {
this.reloadShells()
config.changed$.subscribe(() => {
this.reloadShells()
})
})
}
async getProfiles ({ includeHidden, skipDefault }: { includeHidden?: boolean, skipDefault?: boolean } = {}): Promise<Profile[]> {
const shells = (await this.shells$.toPromise())!
return [
...this.config.store.terminal.profiles,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
...skipDefault ? [] : shells.filter(x => includeHidden || !x.hidden).map(shell => ({
name: shell.name,
shell: shell.id,
icon: shell.icon,
sessionOptions: this.optionsFromShell(shell),
isBuiltin: true,
})),
]
}
getProfileID (profile: Profile): string {
return slugify(profile.name, { remove: /[:.]/g }).toLowerCase()
}
async getProfileByID (id: string): Promise<Profile|null> {
const profiles = await this.getProfiles({ includeHidden: true })
return profiles.find(x => this.getProfileID(x) === id) ?? null
}
/**
* Launches a new terminal with a specific shell and CWD
* @param pause Wait for a keypress when the shell exits
*/
async openTab (profile?: Profile|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> {
if (!profile) {
profile = await this.getProfileByID(this.config.store.terminal.profile)
if (!profile) {
profile = (await this.getProfiles({ includeHidden: true }))[0]
}
}
cwd = cwd ?? profile.sessionOptions.cwd
if (cwd && !fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = null
}
if (!cwd) {
if (!this.config.store.terminal.alwaysUseWorkingDirectory) {
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
cwd = await this.app.activeTab.session.getWorkingDirectory()
}
if (this.app.activeTab instanceof SplitTabComponent) {
const focusedTab = this.app.activeTab.getFocusedTab()
if (focusedTab instanceof TerminalTabComponent && focusedTab.session) {
cwd = await focusedTab.session.getWorkingDirectory()
}
}
}
cwd = cwd ?? this.config.store.terminal.workingDirectory
}
this.logger.info(`Starting profile ${profile.name}`, profile)
const sessionOptions = {
...profile.sessionOptions,
pauseAfterExit: pause,
cwd: cwd ?? undefined,
}
const tab = this.openTabWithOptions(sessionOptions)
if (profile.color) {
(this.app.getParentTab(tab) ?? tab).color = profile.color
}
if (profile.disableDynamicTitle) {
tab.enableDynamicTitle = false
tab.setTitle(profile.name)
}
return tab
}
optionsFromShell (shell: Shell): SessionOptions {
return {
command: shell.command,
args: shell.args ?? [],
env: shell.env,
}
}
/**
* Open a terminal with custom session options
*/
openTabWithOptions (sessionOptions: SessionOptions): TerminalTabComponent {
if (sessionOptions.runAsAdministrator && this.uac.isAvailable) {
sessionOptions = this.uac.patchSessionOptionsForUAC(sessionOptions)
}
this.logger.info('Using session options:', sessionOptions)
return this.app.openNewTab(
TerminalTabComponent,
{ sessionOptions }
) as TerminalTabComponent
}
private async getShells (): Promise<Shell[]> {
const shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
return shellLists.reduce((a, b) => a.concat(b), [])
}
private async reloadShells () {
this.shells = new AsyncSubject<Shell[]>()
const shells = await this.getShells()
this.logger.debug('Shells list:', shells)
this.shells.next(shells)
this.shells.complete()
}
}

View File

@@ -0,0 +1,41 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { SessionOptions } from '../api'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class UACService {
isAvailable = false
private constructor (
private electron: ElectronService,
) {
this.isAvailable = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED)
}
patchSessionOptionsForUAC (sessionOptions: SessionOptions): SessionOptions {
let helperPath = path.join(
path.dirname(this.electron.app.getPath('exe')),
'resources',
'extras',
'UAC.exe',
)
if (process.env.TABBY_DEV) {
helperPath = path.join(
path.dirname(this.electron.app.getPath('exe')),
'..', '..', '..',
'extras',
'UAC.exe',
)
}
const options = { ...sessionOptions }
options.args = [options.command, ...options.args ?? []]
options.command = helperPath
return options
}
}

352
tabby-local/src/session.ts Normal file
View File

@@ -0,0 +1,352 @@
import * as psNode from 'ps-node'
import * as fs from 'mz/fs'
import * as os from 'os'
import { Injector } from '@angular/core'
import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA } from 'tabby-core'
import { BaseSession } from 'tabby-terminal'
import { ipcRenderer } from 'electron'
import { getWorkingDirectoryFromPID } from 'native-process-working-directory'
import { SessionOptions, ChildProcess } from './api'
/* eslint-disable block-scoped-var */
try {
var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
try {
var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi
const OSC1337Prefix = Buffer.from('\x1b]1337;')
const OSC1337Suffix = Buffer.from('\x07')
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class PTYProxy {
private id: string
private subscriptions: Map<string, any> = new Map()
static spawn (...options: any[]): PTYProxy {
return new PTYProxy(null, ...options)
}
static restore (id: string): PTYProxy|null {
if (ipcRenderer.sendSync('pty:exists', id)) {
return new PTYProxy(id)
}
return null
}
private constructor (id: string|null, ...options: any[]) {
if (id) {
this.id = id
} else {
this.id = ipcRenderer.sendSync('pty:spawn', ...options)
}
}
getPTYID (): string {
return this.id
}
getPID (): number {
return ipcRenderer.sendSync('pty:get-pid', this.id)
}
subscribe (event: string, handler: (..._: any[]) => void): void {
const key = `pty:${this.id}:${event}`
const newHandler = (_event, ...args) => handler(...args)
this.subscriptions.set(key, newHandler)
ipcRenderer.on(key, newHandler)
}
ackData (length: number): void {
ipcRenderer.send('pty:ack-data', this.id, length)
}
unsubscribeAll (): void {
for (const k of this.subscriptions.keys()) {
ipcRenderer.off(k, this.subscriptions.get(k))
}
}
resize (columns: number, rows: number): void {
ipcRenderer.send('pty:resize', this.id, columns, rows)
}
write (data: Buffer): void {
ipcRenderer.send('pty:write', this.id, data)
}
kill (signal?: string): void {
ipcRenderer.send('pty:kill', this.id, signal)
}
}
/** @hidden */
export class Session extends BaseSession {
private pty: PTYProxy|null = null
private ptyClosed = false
private pauseAfterExit = false
private guessedCWD: string|null = null
private reportedCWD: string
private initialCWD: string|null = null
private config: ConfigService
private hostApp: HostAppService
private bootstrapData: BootstrapData
constructor (injector: Injector) {
super()
this.config = injector.get(ConfigService)
this.hostApp = injector.get(HostAppService)
this.bootstrapData = injector.get(BOOTSTRAP_DATA)
}
start (options: SessionOptions): void {
this.name = options.name ?? ''
let pty: PTYProxy|null = null
if (options.restoreFromPTYID) {
pty = PTYProxy.restore(options.restoreFromPTYID)
options.restoreFromPTYID = undefined
}
if (!pty) {
const env = {
...process.env,
TERM: 'xterm-256color',
TERM_PROGRAM: 'Tabby',
...options.env,
...this.config.store.terminal.environment || {},
}
if (this.hostApp.platform === Platform.Windows) {
env.COMSPEC = this.bootstrapData.executable
}
delete env['']
if (this.hostApp.platform === Platform.macOS && !process.env.LC_ALL) {
const locale = process.env.LC_CTYPE ?? 'en_US.UTF-8'
Object.assign(env, {
LANG: locale,
LC_ALL: locale,
LC_MESSAGES: locale,
LC_NUMERIC: locale,
LC_COLLATE: locale,
LC_MONETARY: locale,
})
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let cwd = options.cwd || process.env.HOME
if (!fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = undefined
}
pty = PTYProxy.spawn(options.command, options.args ?? [], {
name: 'xterm-256color',
cols: options.width ?? 80,
rows: options.height ?? 30,
encoding: null,
cwd,
env: env,
// `1` instead of `true` forces ConPTY even if unstable
useConpty: (isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY ? 1 : false) as any,
})
this.guessedCWD = cwd ?? null
}
this.pty = pty
this.truePID = this.pty.getPID()
setTimeout(async () => {
// Retrieve any possible single children now that shell has fully started
let processes = await this.getChildProcesses()
while (processes.length === 1) {
this.truePID = processes[0].pid
processes = await this.getChildProcesses()
}
this.initialCWD = await this.getWorkingDirectory()
}, 2000)
this.open = true
this.pty.subscribe('data', (array: Uint8Array) => {
this.pty!.ackData(array.length)
let data = Buffer.from(array)
data = this.processOSC1337(data)
this.emitOutput(data)
if (this.hostApp.platform === Platform.Windows) {
this.guessWindowsCWD(data.toString())
}
})
this.pty.subscribe('exit', () => {
if (this.pauseAfterExit) {
return
} else if (this.open) {
this.destroy()
}
})
this.pty.subscribe('close', () => {
this.ptyClosed = true
if (this.pauseAfterExit) {
this.emitOutput(Buffer.from('\r\nPress any key to close\r\n'))
} else if (this.open) {
this.destroy()
}
})
this.pauseAfterExit = options.pauseAfterExit ?? false
this.destroyed$.subscribe(() => this.pty!.unsubscribeAll())
}
getPTYID (): string|null {
return this.pty?.getPTYID() ?? null
}
resize (columns: number, rows: number): void {
this.pty?.resize(columns, rows)
}
write (data: Buffer): void {
if (this.ptyClosed) {
this.destroy()
}
if (this.open) {
this.pty?.write(data)
}
}
kill (signal?: string): void {
this.pty?.kill(signal)
}
async getChildProcesses (): Promise<ChildProcess[]> {
if (!this.truePID) {
return []
}
if (this.hostApp.platform === Platform.macOS) {
const processes = await macOSNativeProcessList.getProcessList()
return processes.filter(x => x.ppid === this.truePID).map(p => ({
pid: p.pid,
ppid: p.ppid,
command: p.name,
}))
}
if (this.hostApp.platform === Platform.Windows) {
return new Promise<ChildProcess[]>(resolve => {
windowsProcessTree.getProcessTree(this.truePID, tree => {
resolve(tree ? tree.children.map(child => ({
pid: child.pid,
ppid: tree.pid,
command: child.name,
})) : [])
})
})
}
return new Promise<ChildProcess[]>((resolve, reject) => {
psNode.lookup({ ppid: this.truePID }, (err, processes) => {
if (err) {
return reject(err)
}
resolve(processes as ChildProcess[])
})
})
}
async gracefullyKillProcess (): Promise<void> {
if (this.hostApp.platform === Platform.Windows) {
this.kill()
} else {
await new Promise<void>((resolve) => {
this.kill('SIGTERM')
setImmediate(() => {
try {
process.kill(this.pty!.getPID(), 0)
// still alive
setTimeout(() => {
this.kill('SIGKILL')
resolve()
}, 1000)
} catch {
resolve()
}
})
})
}
}
supportsWorkingDirectory (): boolean {
return !!(this.truePID || this.reportedCWD || this.guessedCWD)
}
async getWorkingDirectory (): Promise<string|null> {
if (this.reportedCWD) {
return this.reportedCWD
}
if (!this.truePID) {
return null
}
let cwd: string|null = null
try {
cwd = getWorkingDirectoryFromPID(this.truePID)
} catch (exc) {
console.error(exc)
}
try {
cwd = await fs.realpath(cwd)
} catch {}
if (this.hostApp.platform === Platform.Windows && (cwd === this.initialCWD || cwd === process.env.WINDIR)) {
// shell doesn't truly change its process' CWD
cwd = null
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
cwd = cwd || this.guessedCWD
try {
await fs.access(cwd)
} catch {
return null
}
return cwd
}
private guessWindowsCWD (data: string) {
const match = windowsDirectoryRegex.exec(data)
if (match) {
this.guessedCWD = match[0]
}
}
private processOSC1337 (data: Buffer) {
if (data.includes(OSC1337Prefix)) {
const preData = data.subarray(0, data.indexOf(OSC1337Prefix))
const params = data.subarray(data.indexOf(OSC1337Prefix) + OSC1337Prefix.length)
const postData = params.subarray(params.indexOf(OSC1337Suffix) + OSC1337Suffix.length)
const paramString = params.subarray(0, params.indexOf(OSC1337Suffix)).toString()
if (paramString.startsWith('CurrentDir=')) {
this.reportedCWD = paramString.split('=')[1]
if (this.reportedCWD.startsWith('~')) {
this.reportedCWD = os.homedir() + this.reportedCWD.substring(1)
}
data = Buffer.concat([preData, postData])
}
}
return data
}
}

View File

@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core'
import { SettingsTabProvider } from 'tabby-settings'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
/** @hidden */
@Injectable()
export class ShellSettingsTabProvider extends SettingsTabProvider {
id = 'terminal-shell'
icon = 'list-ul'
title = 'Shell'
getComponentType (): any {
return ShellSettingsTabComponent
}
}

View File

@@ -0,0 +1,57 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class CmderShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
if (!process.env.CMDER_ROOT) {
return []
}
return [
{
id: 'cmder',
name: 'Cmder',
command: 'cmd.exe',
args: [
'/k',
path.join(process.env.CMDER_ROOT, 'vendor', 'init.bat'),
],
icon: require('../icons/cmder.svg'),
env: {
TERM: 'cygwin',
},
},
{
id: 'cmderps',
name: 'Cmder PowerShell',
command: 'powershell.exe',
args: [
'-ExecutionPolicy',
'Bypass',
'-nologo',
'-noprofile',
'-noexit',
'-command',
`Invoke-Expression '. ''${path.join(process.env.CMDER_ROOT, 'vendor', 'profile.ps1')}'''`,
],
icon: require('../icons/cmder-powershell.svg'),
env: {},
},
]
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core'
import { ConfigService } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class CustomShellProvider extends ShellProvider {
constructor (
private config: ConfigService,
) {
super()
}
async provide (): Promise<Shell[]> {
const args = this.config.store.terminal.customShell.split(' ')
return [{
id: 'custom',
name: 'Custom shell',
command: args[0],
args: args.slice(1),
env: {},
}]
}
}

View File

@@ -0,0 +1,44 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
/** @hidden */
@Injectable()
export class Cygwin32ShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
const cygwinPath = wnr.getRegistryValue(wnr.HK.LM, 'Software\\WOW6432Node\\Cygwin\\setup', 'rootdir')
if (!cygwinPath) {
return []
}
return [{
id: 'cygwin32',
name: 'Cygwin (32 bit)',
command: path.join(cygwinPath, 'bin', 'bash.exe'),
args: ['--login', '-i'],
icon: require('../icons/cygwin.svg'),
env: {
TERM: 'cygwin',
},
}]
}
}

View File

@@ -0,0 +1,44 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
/** @hidden */
@Injectable()
export class Cygwin64ShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
const cygwinPath = wnr.getRegistryValue(wnr.HK.LM, 'Software\\Cygwin\\setup', 'rootdir')
if (!cygwinPath) {
return []
}
return [{
id: 'cygwin64',
name: 'Cygwin',
command: path.join(cygwinPath, 'bin', 'bash.exe'),
args: ['--login', '-i'],
icon: require('../icons/cygwin.svg'),
env: {
TERM: 'cygwin',
},
}]
}
}

View File

@@ -0,0 +1,48 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
/** @hidden */
@Injectable()
export class GitBashShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
let gitBashPath = wnr.getRegistryValue(wnr.HK.LM, 'Software\\GitForWindows', 'InstallPath')
if (!gitBashPath) {
gitBashPath = wnr.getRegistryValue(wnr.HK.CU, 'Software\\GitForWindows', 'InstallPath')
}
if (!gitBashPath) {
return []
}
return [{
id: 'git-bash',
name: 'Git-Bash',
command: path.join(gitBashPath, 'bin', 'bash.exe'),
args: ['--login', '-i'],
icon: require('../icons/git-bash.svg'),
env: {
TERM: 'cygwin',
},
}]
}
}

View File

@@ -0,0 +1,45 @@
import * as fs from 'mz/fs'
import { Injectable } from '@angular/core'
import { HostAppService, Platform, LogService, Logger } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class LinuxDefaultShellProvider extends ShellProvider {
private logger: Logger
constructor (
private hostApp: HostAppService,
log: LogService,
) {
super()
this.logger = log.create('linuxDefaultShell')
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Linux) {
return []
}
const line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
.split('\n').find(x => x.startsWith(`${process.env.LOGNAME}:`))
if (!line) {
this.logger.warn('Could not detect user shell')
return [{
id: 'default',
name: 'User default',
command: '/bin/sh',
env: {},
}]
} else {
return [{
id: 'default',
name: 'User default',
command: line.split(':')[6],
args: ['--login'],
hidden: true,
env: {},
}]
}
}
}

View File

@@ -0,0 +1,43 @@
import { exec } from 'mz/child_process'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class MacOSDefaultShellProvider extends ShellProvider {
private cachedShell?: string
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.macOS) {
return []
}
return [{
id: 'default',
name: 'User default',
command: await this.getDefaultShellCached(),
args: ['--login'],
hidden: true,
env: {},
}]
}
private async getDefaultShellCached () {
if (!this.cachedShell) {
this.cachedShell = await this.getDefaultShell()
}
return this.cachedShell!
}
private async getDefaultShell () {
const shellEntry = (await exec(`/usr/bin/dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
return shellEntry.split(' ')[1].trim()
}
}

View File

@@ -0,0 +1,33 @@
import * as fs from 'mz/fs'
import slugify from 'slugify'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class POSIXShellsProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform === Platform.Windows) {
return []
}
return (await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
.split('\n')
.map(x => x.trim())
.filter(x => x && !x.startsWith('#'))
.map(x => ({
id: slugify(x),
name: x.split('/')[2],
command: x,
args: ['-l'],
env: {},
}))
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
/** @hidden */
@Injectable()
export class PowerShellCoreShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
const pwshPath = wnr.getRegistryValue(wnr.HK.LM, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\pwsh.exe', '')
if (!pwshPath) {
return []
}
return [{
id: 'powershell-core',
name: 'PowerShell Core',
command: pwshPath,
args: ['-nologo'],
icon: require('../icons/powershell-core.svg'),
env: {
TERM: 'cygwin',
},
}]
}
}

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
import { WSLShellProvider } from './wsl'
import { PowerShellCoreShellProvider } from './powershellCore'
import { WindowsStockShellsProvider } from './windowsStock'
/** @hidden */
@Injectable()
export class WindowsDefaultShellProvider extends ShellProvider {
private providers: ShellProvider[]
constructor (
psc: PowerShellCoreShellProvider,
wsl: WSLShellProvider,
stock: WindowsStockShellsProvider,
private hostApp: HostAppService,
) {
super()
this.providers = [
psc,
wsl,
stock,
]
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
// Figure out a sensible default
const shellLists = await Promise.all(this.providers.map(x => x.provide()))
for (const list of shellLists) {
if (list.length) {
const shell = list[list.length - 1]
return [{
...shell,
id: 'default',
name: `Default (${shell.name})`,
hidden: true,
env: {},
}]
}
}
return []
}
}

View File

@@ -0,0 +1,71 @@
import * as path from 'path'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class WindowsStockShellsProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
private electron: ElectronService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
let clinkPath = path.join(
path.dirname(this.electron.app.getPath('exe')),
'resources',
'extras',
'clink',
`clink_${process.arch}.exe`,
)
if (process.env.TABBY_DEV) {
clinkPath = path.join(
path.dirname(this.electron.app.getPath('exe')),
'..', '..', '..',
'extras',
'clink',
`clink_${process.arch}.exe`,
)
}
return [
{
id: 'clink',
name: 'CMD (clink)',
command: 'cmd.exe',
args: ['/k', clinkPath, 'inject'],
env: {
// Tell clink not to emulate ANSI handling
WT_SESSION: '0',
},
icon: require('../icons/clink.svg'),
},
{
id: 'cmd',
name: 'CMD (stock)',
command: 'cmd.exe',
env: {},
icon: require('../icons/cmd.svg'),
},
{
id: 'powershell',
name: 'PowerShell',
command: 'powershell.exe',
args: ['-nologo'],
icon: require('../icons/powershell.svg'),
env: {
TERM: 'cygwin',
},
},
]
}
}

View File

@@ -0,0 +1,111 @@
import * as fs from 'mz/fs'
import slugify from 'slugify'
import { Injectable } from '@angular/core'
import { HostAppService, Platform, isWindowsBuild, WIN_BUILD_WSL_EXE_DISTRO_FLAG } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch { }
// WSL Distribution List
// https://docs.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice
/* eslint-disable quote-props */
const wslIconMap: Record<string, string> = {
'Alpine': require('../icons/alpine.svg'),
'Debian': require('../icons/debian.svg'),
'kali-linux': require('../icons/linux.svg'),
'SLES-12': require('../icons/suse.svg'),
'openSUSE-Leap-15-1': require('../icons/suse.svg'),
'Ubuntu-16.04': require('../icons/ubuntu.svg'),
'Ubuntu-18.04': require('../icons/ubuntu.svg'),
'Ubuntu': require('../icons/ubuntu.svg'),
'Linux': require('../icons/linux.svg'),
}
/* eslint-enable quote-props */
/** @hidden */
@Injectable()
export class WSLShellProvider extends ShellProvider {
constructor (
private hostApp: HostAppService,
) {
super()
}
async provide (): Promise<Shell[]> {
if (this.hostApp.platform !== Platform.Windows) {
return []
}
const bashPath = `${process.env.windir}\\system32\\bash.exe`
const wslPath = `${process.env.windir}\\system32\\wsl.exe`
const lxssPath = 'Software\\Microsoft\\Windows\\CurrentVersion\\Lxss'
const lxss = wnr.getRegistryKey(wnr.HK.CU, lxssPath)
const shells: Shell[] = []
if (null != lxss && null != lxss.DefaultDistribution) {
const defaultDistKey = wnr.getRegistryKey(wnr.HK.CU, lxssPath + '\\' + String(lxss.DefaultDistribution.value))
if (defaultDistKey?.DistributionName) {
const shell: Shell = {
id: 'wsl',
name: 'WSL / Default distro',
command: wslPath,
env: {
TERM: 'xterm-color',
COLORTERM: 'truecolor',
},
icon: wslIconMap[defaultDistKey.DistributionName.value],
}
shells.push(shell)
}
}
if (!lxss || !lxss.DefaultDistribution || !isWindowsBuild(WIN_BUILD_WSL_EXE_DISTRO_FLAG)) {
if (await fs.exists(bashPath)) {
return [{
id: 'wsl',
name: 'WSL / Bash on Windows',
icon: wslIconMap.Linux,
command: bashPath,
env: {
TERM: 'xterm-color',
COLORTERM: 'truecolor',
},
}]
} else {
return []
}
}
for (const child of wnr.listRegistrySubkeys(wnr.HK.CU, lxssPath) as string[]) {
const childKey = wnr.getRegistryKey(wnr.HK.CU, lxssPath + '\\' + child)
if (!childKey.DistributionName) {
continue
}
const wslVersion = (childKey.Flags?.value || 0) & 8 ? 2 : 1
const name = childKey.DistributionName.value
const fsBase = wslVersion === 2 ? `\\\\wsl$\\${name}` : childKey.BasePath.value as string + '\\rootfs'
const slug = slugify(name, { remove: /[:.]/g })
const shell: Shell = {
id: `wsl-${slug}`,
name: `WSL / ${name}`,
command: wslPath,
args: ['-d', name],
fsBase,
env: {
TERM: 'xterm-color',
COLORTERM: 'truecolor',
},
icon: wslIconMap[name],
}
shells.push(shell)
}
return shells
}
}

View File

@@ -0,0 +1,129 @@
import { Injectable } from '@angular/core'
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService, MenuItemOptions } from 'tabby-core'
import { TerminalTabComponent } from './components/terminalTab.component'
import { UACService } from './services/uac.service'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@Injectable()
export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
constructor (
private config: ConfigService,
private notifications: NotificationsService,
) {
super()
}
async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
if (!(tab instanceof TerminalTabComponent)) {
return []
}
const items: MenuItemOptions[] = [
{
label: 'Save as profile',
click: async () => {
const profile = {
sessionOptions: {
...tab.sessionOptions,
cwd: await tab.session?.getWorkingDirectory() ?? tab.sessionOptions.cwd,
},
name: tab.sessionOptions.command,
}
this.config.store.terminal.profiles = [
...this.config.store.terminal.profiles,
profile,
]
this.config.save()
this.notifications.info('Saved')
},
},
]
return items
}
}
/** @hidden */
@Injectable()
export class NewTabContextMenu extends TabContextMenuItemProvider {
weight = 10
constructor (
public config: ConfigService,
private terminalService: TerminalService,
private uac: UACService,
) {
super()
}
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
const profiles = await this.terminalService.getProfiles()
const items: MenuItemOptions[] = [
{
label: 'New terminal',
click: () => {
this.terminalService.openTabWithOptions((tab as any).sessionOptions)
},
},
{
label: 'New with profile',
submenu: profiles.map(profile => ({
label: profile.name,
click: async () => {
let workingDirectory = this.config.store.terminal.workingDirectory
if (this.config.store.terminal.alwaysUseWorkingDirectory !== true && tab instanceof TerminalTabComponent) {
workingDirectory = await tab.session?.getWorkingDirectory()
}
await this.terminalService.openTab(profile, workingDirectory)
},
})),
},
]
if (this.uac.isAvailable) {
items.push({
label: 'New admin tab',
submenu: profiles.map(profile => ({
label: profile.name,
click: () => {
this.terminalService.openTabWithOptions({
...profile.sessionOptions,
runAsAdministrator: true,
})
},
})),
})
}
if (tab instanceof TerminalTabComponent && tabHeader && this.uac.isAvailable) {
items.push({
label: 'Duplicate as administrator',
click: () => {
this.terminalService.openTabWithOptions({
...tab.sessionOptions,
runAsAdministrator: true,
})
},
})
}
if (tab instanceof TerminalTabComponent && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) {
items.push({
label: 'Focus all panes',
click: () => {
tab.focusAllPanes()
},
})
}
if (tab instanceof TerminalTabComponent && tab.session?.supportsWorkingDirectory()) {
items.push({
label: 'Copy current path',
click: () => tab.copyCurrentPath(),
})
}
return items
}
}

View File

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

View File

@@ -0,0 +1,16 @@
{
"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: 'local',
dirname: __dirname,
})

482
tabby-local/yarn.lock Normal file
View File

@@ -0,0 +1,482 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/deep-equal@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
"@types/shell-escape@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2"
integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g==
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
connected-domain@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93"
integrity sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM=
dataurl@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199"
integrity sha1-H0c0/t3sBf/kRXR5eNhnWcSzMZk=
deep-equal@2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9"
integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==
dependencies:
call-bind "^1.0.0"
es-get-iterator "^1.1.1"
get-intrinsic "^1.0.1"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.1.1"
isarray "^2.0.5"
object-is "^1.1.4"
object-keys "^1.1.1"
object.assign "^4.1.2"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.3"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
dependencies:
object-keys "^1.0.12"
es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.2"
is-negative-zero "^2.0.0"
is-regex "^1.1.1"
object-inspect "^1.8.0"
object-keys "^1.1.1"
object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-abstract@^1.18.0-next.2:
version "1.18.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
dependencies:
call-bind "^1.0.2"
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
get-intrinsic "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.2"
is-callable "^1.2.3"
is-negative-zero "^2.0.1"
is-regex "^1.1.2"
is-string "^1.0.5"
object-inspect "^1.9.0"
object-keys "^1.1.1"
object.assign "^4.1.2"
string.prototype.trimend "^1.0.4"
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.0"
es-get-iterator@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.0"
has-symbols "^1.0.1"
is-arguments "^1.1.0"
is-map "^2.0.2"
is-set "^2.0.2"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
hterm-umdjs@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957"
integrity sha512-r5JOmdDK1bZCmp3cKcuGRLVeum33H+pzD119ZxmQou+QUVe6SAVSz03HvKWVhM2Ao1Biv+fkhFDmnsaRPq0tFg==
is-arguments@^1.0.4, is-arguments@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==
dependencies:
call-bind "^1.0.0"
is-bigint@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==
is-boolean-object@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8"
integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==
dependencies:
call-bind "^1.0.2"
is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
is-callable@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
is-map@^2.0.1, is-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-negative-zero@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
is-negative-zero@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-number-object@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb"
integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==
is-regex@^1.1.1, is-regex@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
dependencies:
call-bind "^1.0.2"
has-symbols "^1.0.2"
is-set@^2.0.1, is-set@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
is-string@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
dependencies:
has-symbols "^1.0.1"
is-symbol@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
dependencies:
has-symbols "^1.0.2"
is-typed-array@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e"
integrity sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==
dependencies:
available-typed-arrays "^1.0.2"
call-bind "^1.0.2"
es-abstract "^1.18.0-next.2"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-inspect@^1.9.0:
version "1.10.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
object-is@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object.assign@^4.1.1, object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
has-symbols "^1.0.1"
object-keys "^1.1.1"
opentype.js@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6"
integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==
dependencies:
string.prototype.codepointat "^0.2.1"
tiny-inflate "^1.0.3"
ps-node@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3"
integrity sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM=
dependencies:
table-parser "^0.1.3"
regexp.prototype.flags@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
runes@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355"
integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==
shell-escape@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133"
integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=
side-channel@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
slugify@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.5.3.tgz#36e009864f5476bfd5db681222643d92339c890d"
integrity sha512-/HkjRdwPY3yHJReXu38NiusZw2+LLE2SrhkWJtmlPDB1fqFSvioYj62NkPcrKiNCgRLeGcGK7QBvr1iQwybeXw==
string.prototype.codepointat@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"
integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
string.prototype.trimend@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz#6ddd9a8796bc714b489a3ae22246a208f37bfa46"
integrity sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
string.prototype.trimstart@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz#22d45da81015309cd0cdd79787e8919fc5c613e7"
integrity sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
string.prototype.trimstart@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
table-parser@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0"
integrity sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=
dependencies:
connected-domain "^1.0.0"
tiny-inflate@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
tinyqueue@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
unbox-primitive@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
dependencies:
function-bind "^1.1.1"
has-bigints "^1.0.1"
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
utils-decorators@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/utils-decorators/-/utils-decorators-1.8.3.tgz#7ec9c2b4a943658de34cb2533bf2fd9c4b1cd61b"
integrity sha512-QtoRQikWeYtMZsT5ChOz5HNzYxLGCEBUELQ1J8+WIuSQo+1D2bPwIY08DKzw88YSl2HXes60RxfafYhgXnlVJA==
dependencies:
tinyqueue "^2.0.3"
which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
dependencies:
is-bigint "^1.0.1"
is-boolean-object "^1.1.0"
is-number-object "^1.0.4"
is-string "^1.0.5"
is-symbol "^1.0.3"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-typed-array@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff"
integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==
dependencies:
available-typed-arrays "^1.0.2"
call-bind "^1.0.0"
es-abstract "^1.18.0-next.1"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"