mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-28 04:48:34 +00:00
Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0f6855d978 | ||
![]() |
3102f39706 | ||
![]() |
46ac0a6caf | ||
![]() |
68e1db040a | ||
![]() |
e34772b8b8 | ||
![]() |
cf6558ec6a | ||
![]() |
f5f88d3d9d | ||
![]() |
64955bfcd6 | ||
![]() |
9fa9021a81 | ||
![]() |
43183401b7 | ||
![]() |
880b9ce82b | ||
![]() |
3584af524b | ||
![]() |
af174933d6 | ||
![]() |
c4490717c0 | ||
![]() |
70b6be7301 | ||
![]() |
8d5b0fe863 | ||
![]() |
03045eb952 | ||
![]() |
f7b0272be5 | ||
![]() |
4fa16c8a20 | ||
![]() |
855a7bbe14 | ||
![]() |
2b3694f517 | ||
![]() |
101177a865 | ||
![]() |
8b33f98c79 | ||
![]() |
98e52f50a9 | ||
![]() |
7551201796 | ||
![]() |
3fe2dccb94 | ||
![]() |
f53eb31274 | ||
![]() |
81663f351a | ||
![]() |
bf5d037cff | ||
![]() |
53d9af3279 | ||
![]() |
b7dd354313 | ||
![]() |
d8bc9ce859 | ||
![]() |
1bb9358f77 | ||
![]() |
fa77ff3995 | ||
![]() |
1ae8d9c643 | ||
![]() |
a560f0c96e | ||
![]() |
434bacf185 | ||
![]() |
79de7ec015 | ||
![]() |
dfdb3b051b | ||
![]() |
9fbf9136fc | ||
![]() |
25fdba7104 | ||
![]() |
c91707e94f | ||
![]() |
d665eef430 | ||
![]() |
4579e839cd | ||
![]() |
6e952180ec | ||
![]() |
a947254ca8 | ||
![]() |
1eb4a7fc26 | ||
![]() |
0471fcec15 | ||
![]() |
4110d09dab | ||
![]() |
144924e579 | ||
![]() |
6902ccdb95 | ||
![]() |
7ed5aff168 | ||
![]() |
acf418b52f |
@@ -121,3 +121,8 @@ rules:
|
||||
'@typescript-eslint/no-unsafe-argument': off
|
||||
'@typescript-eslint/restrict-plus-operands': off
|
||||
'@typescript-eslint/space-infix-ops': off
|
||||
'@typescript-eslint/no-type-alias':
|
||||
- error
|
||||
- allowAliases: in-unions-and-intersections
|
||||
allowLiterals: always
|
||||
allowCallbacks: always
|
||||
|
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.2.0
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.2.0
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2.2.0
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.2.0
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Installing Node
|
||||
uses: actions/setup-node@v2.2.0
|
||||
uses: actions/setup-node@v2.4.0
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
|
@@ -103,10 +103,10 @@ Tabby will run as a portable app on Windows, if you create a `data` folder in th
|
||||
Plugins and themes can be installed directly from the Settings view inside Tabby.
|
||||
|
||||
* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - makes paths and URLs in the terminal clickable
|
||||
* [docker](https://github.com/Eugeny/tabby-docker) - connect to Docker containers
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||
* [save-output](https://github.com/Eugeny/tabby-save-output) - record terminal output into a file
|
||||
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to hterm tabs
|
||||
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
|
||||
|
||||
<a name="themes"></a>
|
||||
|
@@ -3,6 +3,7 @@ import * as promiseIpc from 'electron-promise-ipc'
|
||||
import * as remote from '@electron/remote/main'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { Subject, throttleTime } from 'rxjs'
|
||||
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
@@ -19,6 +20,7 @@ export class Application {
|
||||
private tray?: Tray
|
||||
private ptyManager = new PTYManager()
|
||||
private windows: Window[] = []
|
||||
private globalHotkey$ = new Subject<void>()
|
||||
userPluginsPath: string
|
||||
|
||||
constructor () {
|
||||
@@ -33,12 +35,14 @@ export class Application {
|
||||
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
|
||||
globalShortcut.unregisterAll()
|
||||
for (const spec of specs) {
|
||||
globalShortcut.register(spec, () => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
globalShortcut.register(spec, () => this.globalHotkey$.next())
|
||||
}
|
||||
})
|
||||
|
||||
this.globalHotkey$.pipe(throttleTime(100)).subscribe(() => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
|
||||
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
|
||||
return pluginManager.install(this.userPluginsPath, name, version)
|
||||
})
|
||||
|
@@ -27,7 +27,9 @@ app.on('activate', () => {
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
process.on('uncaughtException' as any, err => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as nodePTY from 'node-pty'
|
||||
import * as nodePTY from '@tabby-gang/node-pty'
|
||||
import { StringDecoder } from './stringDecoder'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ipcMain } from 'electron'
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as glasstron from 'glasstron'
|
||||
|
||||
import { Subject, Observable, debounceTime } from 'rxjs'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions } from 'electron'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage } from 'electron'
|
||||
import ElectronConfig = require('electron-config')
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -28,6 +28,8 @@ abstract class GlasstronWindow extends BrowserWindow {
|
||||
|
||||
const macOSVibrancyType = process.platform === 'darwin' ? compareVersions.compare(macOSRelease().version, '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null
|
||||
|
||||
const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`)
|
||||
|
||||
export class Window {
|
||||
ready: Promise<void>
|
||||
private visible = new Subject<boolean>()
|
||||
@@ -39,6 +41,7 @@ export class Window {
|
||||
private lastVibrancy: { enabled: boolean, type?: string } | null = null
|
||||
private disableVibrancyWhileDragging = false
|
||||
private configStore: any
|
||||
private touchBarControl: any
|
||||
|
||||
get visible$ (): Observable<boolean> { return this.visible }
|
||||
get closed$ (): Observable<void> { return this.closed }
|
||||
@@ -127,7 +130,15 @@ export class Window {
|
||||
this.window.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
this.window.webContents.setZoomFactor(1)
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
if (process.platform === 'darwin') {
|
||||
this.touchBarControl = new TouchBar.TouchBarSegmentedControl({
|
||||
segments: [],
|
||||
change: index => this.send('touchbar-selection', index),
|
||||
})
|
||||
this.window.setTouchBar(new TouchBar({
|
||||
items: [this.touchBarControl],
|
||||
}))
|
||||
} else {
|
||||
this.window.setMenu(null)
|
||||
}
|
||||
|
||||
@@ -357,6 +368,14 @@ export class Window {
|
||||
this.window.close()
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-touch-bar', (_event, segments, selectedIndex) => {
|
||||
this.touchBarControl.segments = segments.map(s => ({
|
||||
label: s.label,
|
||||
icon: s.hasActivity ? activityIcon : undefined,
|
||||
}))
|
||||
this.touchBarControl.selectedIndex = selectedIndex
|
||||
})
|
||||
|
||||
this.window.webContents.on('new-window', event => event.preventDefault())
|
||||
|
||||
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^12.1.2",
|
||||
"@angular/cdk": "^12.2.0",
|
||||
"@electron/remote": "1.2.0",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
@@ -26,12 +26,12 @@
|
||||
"keytar": "^7.7.0",
|
||||
"mz": "^2.7.0",
|
||||
"native-process-working-directory": "^1.0.2",
|
||||
"node-pty": "^0.10.1",
|
||||
"@tabby-gang/node-pty": "^0.11.0-beta.200",
|
||||
"npm": "6",
|
||||
"rxjs": "^7.2.0",
|
||||
"source-map-support": "^0.5.19",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"yargs": "^17.0.1"
|
||||
"yargs": "^17.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"macos-native-processlist": "^2.0.0",
|
||||
|
@@ -58,7 +58,7 @@ nodeModule.prototype.require = function (query: string) {
|
||||
return originalModuleRequire.call(this, query)
|
||||
}
|
||||
|
||||
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type ProgressCallback = (current: number, total: number) => void
|
||||
|
||||
export function initModuleLookup (userPluginsPath: string): void {
|
||||
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
|
@@ -44,7 +44,8 @@ module.exports = {
|
||||
glasstron: 'commonjs glasstron',
|
||||
mz: 'commonjs mz',
|
||||
npm: 'commonjs npm',
|
||||
'node-pty': 'commonjs node-pty',
|
||||
'node:os': 'commonjs os',
|
||||
'@tabby-gang/node-pty': 'commonjs @tabby-gang/node-pty',
|
||||
path: 'commonjs path',
|
||||
util: 'commonjs util',
|
||||
'source-map-support': 'commonjs source-map-support',
|
||||
|
@@ -2,10 +2,10 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@angular/cdk@^12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.1.2.tgz#5c2407324d860737374d873bd4381bf7f90f8a61"
|
||||
integrity sha512-ALupZejZDsVYcbNZcEH1cV8SDgVBL40FAwDnlSZxCgd0HOBHH0ZqQV+8z0uCQeMatoNM+SwmJ8Y1JXYh9Bqfiw==
|
||||
"@angular/cdk@^12.2.0":
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.0.tgz#7c6de53522ef7cf911d86e187f3df2a90e8fee49"
|
||||
integrity sha512-Dts+KIMz6EdzQxaWBFcNwgWAHVPkI5pnOGMidKKVOmjezSUN6mhfBKq8emgsddJMRAqz/1VHMAEaRkp0VoBKiA==
|
||||
dependencies:
|
||||
tslib "^2.2.0"
|
||||
optionalDependencies:
|
||||
@@ -96,6 +96,13 @@
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
|
||||
"@tabby-gang/node-pty@^0.11.0-beta.200":
|
||||
version "0.11.0-beta.200"
|
||||
resolved "https://registry.yarnpkg.com/@tabby-gang/node-pty/-/node-pty-0.11.0-beta.200.tgz#485cd6d85a04f4b272b81a9862578d7fc38cdfb5"
|
||||
integrity sha512-32ANParjnd38SzvICaLYvEBlTZAE2sqsgEZPK6ITgd38FcCsS/yvvsDZcjkclbxApnMM2rJDaYjsZMa0lr9Iyg==
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
"@types/mz@2.7.4":
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
|
||||
@@ -2095,13 +2102,6 @@ node-gyp@^5.0.2, node-gyp@^5.1.0:
|
||||
tar "^4.4.12"
|
||||
which "^1.3.1"
|
||||
|
||||
node-pty@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d"
|
||||
integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg==
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz"
|
||||
@@ -3408,12 +3408,7 @@ tough-cookie@~2.5.0:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
|
||||
tslib@^2.2.0:
|
||||
tslib@^2.0.0, tslib@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
@@ -3750,10 +3745,10 @@ yargs@^14.2.3:
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^15.0.1"
|
||||
|
||||
yargs@^17.0.1:
|
||||
version "17.0.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
|
||||
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
|
||||
yargs@^17.1.0:
|
||||
version "17.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf"
|
||||
integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
|
14
package.json
14
package.json
@@ -7,7 +7,7 @@
|
||||
"@angular/forms": "^12.0.0",
|
||||
"@angular/platform-browser": "^12.0.0",
|
||||
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||
"@sentry/cli": "^1.67.2",
|
||||
"@sentry/electron": "^2.5.1",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@types/node": "16.0.1",
|
||||
"@types/sortablejs": "^1.10.7",
|
||||
"@types/webpack-env": "^1.16.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"apply-loader": "2.0.0",
|
||||
"axios": "^0.21.1",
|
||||
@@ -28,20 +28,20 @@
|
||||
"core-js": "^3.15.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "^6.2.0",
|
||||
"electron": "13.1.8",
|
||||
"electron": "13.1.9",
|
||||
"electron-builder": "22.10.5",
|
||||
"electron-download": "^4.1.1",
|
||||
"electron-installer-snap": "^5.1.0",
|
||||
"electron-notarize": "^1.0.1",
|
||||
"electron-rebuild": "^2.3.5",
|
||||
"electron-rebuild": "^3.1.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"graceful-fs": "^4.2.8",
|
||||
"html-loader": "2.1.2",
|
||||
"json-loader": "0.5.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"macos-release": "^2.5.0",
|
||||
"macos-release": "^3.0.0",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-toastr": "^14.0.0",
|
||||
"node-abi": "^2.30.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"typedoc": "^0.21.5",
|
||||
"typescript": "^4.3.5",
|
||||
"val-loader": "4.0.0",
|
||||
"webpack": "^5.48.0",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"yaml-loader": "0.6.0",
|
||||
|
@@ -13,6 +13,10 @@ const configs = [
|
||||
;(async () => {
|
||||
for (const c of configs) {
|
||||
log.info('build', c)
|
||||
await promisify(webpack)(require(c))
|
||||
const stats = await promisify(webpack)(require(c))
|
||||
console.log(stats.toString({ colors: true }))
|
||||
if (stats.hasErrors()) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
@@ -20,7 +20,7 @@ sh.cd('web')
|
||||
sh.exec(`${npx} yarn install --force`)
|
||||
sh.cd('..')
|
||||
|
||||
vars.builtinPlugins.forEach(plugin => {
|
||||
vars.allPackages.forEach(plugin => {
|
||||
log.info('deps', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec(`${npx} yarn install --force`)
|
||||
|
@@ -9,7 +9,7 @@ sh.exec(`${sentryCli} releases new ${vars.version}`)
|
||||
if (process.platform === 'darwin') {
|
||||
for (const path of [
|
||||
'app/node_modules/@serialport/bindings/build/Release/bindings.node',
|
||||
'app/node_modules/node-pty/build/Release/pty.node',
|
||||
'app/node_modules/@tabby-gang/node-pty/build/Release/pty.node',
|
||||
'app/node_modules/fontmanager-redux/build/Release/fontmanager.node',
|
||||
'app/node_modules/macos-native-processlist/build/Release/native.node',
|
||||
]) {
|
||||
|
@@ -19,7 +19,6 @@
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^3.1.2",
|
||||
"deep-equal": "^2.0.5",
|
||||
"deepmerge": "^4.1.1",
|
||||
"electron-updater": "^4.0.6",
|
||||
|
@@ -4,5 +4,6 @@ export interface SelectorOption<T> {
|
||||
result?: T
|
||||
icon?: string
|
||||
freeInputPattern?: string
|
||||
color?: string
|
||||
callback?: (string?) => void
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
title-bar(
|
||||
*ngIf='ready && !hostWindow.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullScreen'
|
||||
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullscreen'
|
||||
)
|
||||
|
||||
.content(
|
||||
@@ -10,7 +10,7 @@ title-bar(
|
||||
)
|
||||
.tab-bar
|
||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
|
||||
&& !hostWindow.isFullScreen \
|
||||
&& !hostWindow.isFullscreen \
|
||||
&& config.store.appearance.frame == "thin" \
|
||||
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||
.tabs(
|
||||
|
@@ -7,7 +7,7 @@
|
||||
(ngModelChange)='onFilterChange()'
|
||||
)
|
||||
|
||||
.list-group(*ngIf='filteredOptions.length')
|
||||
.list-group.list-group-light(*ngIf='filteredOptions.length')
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
#item,
|
||||
(click)='selectOption(option)',
|
||||
@@ -16,11 +16,13 @@
|
||||
)
|
||||
i.icon(
|
||||
class='fa-fw {{option.icon}}',
|
||||
style='color: {{option.color}}',
|
||||
*ngIf='!iconIsSVG(option.icon)'
|
||||
)
|
||||
.icon(
|
||||
[fastHtmlBind]='option.icon',
|
||||
style='color: {{option.color}}',
|
||||
*ngIf='iconIsSVG(option.icon)'
|
||||
)
|
||||
.mr-2.title {{getOptionText(option)}}
|
||||
.text-muted {{option.description}}
|
||||
.title.mr-2 {{getOptionText(option)}}
|
||||
.description.no-wrap.text-muted {{option.description}}
|
||||
|
@@ -16,6 +16,11 @@
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
input {
|
||||
|
@@ -6,8 +6,8 @@ import { TabsService, NewTabParameters } from '../services/tabs.service'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
|
||||
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type SplitOrientation = 'v' | 'h'
|
||||
export type SplitDirection = 'r' | 't' | 'b' | 'l'
|
||||
|
||||
/**
|
||||
* Describes a horizontal or vertical split row or column
|
||||
@@ -126,16 +126,21 @@ export interface SplitSpannerInfo {
|
||||
/**
|
||||
* Represents a tab drop zone
|
||||
*/
|
||||
export interface SplitDropZoneInfo {
|
||||
container?: SplitContainer
|
||||
position?: number
|
||||
relativeTo?: BaseTabComponent|SplitContainer
|
||||
side?: SplitDirection
|
||||
export type SplitDropZoneInfo = {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
} & ({
|
||||
type: 'absolute'
|
||||
container: SplitContainer
|
||||
position: number
|
||||
} | {
|
||||
type: 'relative'
|
||||
relativeTo?: BaseTabComponent|SplitContainer
|
||||
side: SplitDirection
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||
@@ -585,10 +590,10 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
return
|
||||
}
|
||||
|
||||
if (zone.container) {
|
||||
this.add(tab, zone.container.children[zone.position!], zone.container.orientation === 'h' ? 'r' : 'b')
|
||||
if (zone.type === 'relative') {
|
||||
this.add(tab, zone.relativeTo ?? null, zone.side)
|
||||
} else {
|
||||
this.add(tab, null, zone.side!)
|
||||
this.add(tab, zone.container.children[zone.position], zone.container.orientation === 'h' ? 'r' : 'b')
|
||||
}
|
||||
this.tabAdopted.next(tab)
|
||||
}
|
||||
@@ -649,6 +654,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: y + thickness,
|
||||
w: thickness,
|
||||
h: h - thickness * 2,
|
||||
type: 'relative',
|
||||
side: 'l',
|
||||
})
|
||||
this._dropZones.push({
|
||||
@@ -656,6 +662,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: y - thickness / 2,
|
||||
w,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
side: 't',
|
||||
})
|
||||
this._dropZones.push({
|
||||
@@ -663,6 +670,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: y + thickness,
|
||||
w: thickness,
|
||||
h: h - thickness * 2,
|
||||
type: 'relative',
|
||||
side: 'r',
|
||||
})
|
||||
this._dropZones.push({
|
||||
@@ -670,6 +678,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: y + h - thickness / 2,
|
||||
w,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
side: 'b',
|
||||
})
|
||||
}
|
||||
@@ -714,6 +723,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
if (i !== root.ratios.length - 1) {
|
||||
// Spanner area
|
||||
this._dropZones.push({
|
||||
type: 'relative',
|
||||
relativeTo: root.children[i],
|
||||
side: root.orientation === 'v' ? 'b': 'r',
|
||||
x: root.orientation === 'v' ? childX + thickness : childX + offset - thickness / 2,
|
||||
@@ -730,6 +740,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: childY + thickness,
|
||||
w: thickness,
|
||||
h: childH - thickness * 2,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'l',
|
||||
})
|
||||
@@ -738,6 +749,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: childY + thickness,
|
||||
w: thickness,
|
||||
h: childH - thickness * 2,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'r',
|
||||
})
|
||||
@@ -747,6 +759,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: childY,
|
||||
w: childW - thickness * 2,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 't',
|
||||
})
|
||||
@@ -755,6 +768,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||
y: childY + childH - thickness,
|
||||
w: childW - thickness * 2,
|
||||
h: thickness,
|
||||
type: 'relative',
|
||||
relativeTo: child,
|
||||
side: 'b',
|
||||
})
|
||||
|
@@ -34,7 +34,7 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
|
||||
) {
|
||||
super(element)
|
||||
this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
|
||||
this.isActive = !!tab && tab !== this.parent && tab !== this.dropZone.container?.children[this.dropZone.position!]
|
||||
this.isActive = !!tab && tab !== this.parent && (this.dropZone.type === 'relative' || tab !== this.dropZone.container.children[this.dropZone.position])
|
||||
this.layout()
|
||||
})
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ div
|
||||
h1.tabby-title Tabby
|
||||
sup α
|
||||
|
||||
.list-group
|
||||
.list-group.list-group-light
|
||||
a.list-group-item.list-group-item-action.d-flex(
|
||||
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
|
||||
(click)='button.click()',
|
||||
@@ -13,10 +13,10 @@ div
|
||||
|
||||
footer.d-flex.align-items-center
|
||||
.btn-group.mr-auto
|
||||
button.btn.btn-secondary((click)='homeBase.openGitHub()')
|
||||
button.btn.btn-dark((click)='homeBase.openGitHub()')
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
||||
button.btn.btn-dark((click)='homeBase.reportBug()')
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
|
||||
|
@@ -60,6 +60,7 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
modal.result.then(result => {
|
||||
this.tab.setTitle(result)
|
||||
this.tab.customTitle = result
|
||||
this.app.emitTabsChanged()
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { PartialProfile, Profile } from './api'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -193,9 +194,13 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
return [
|
||||
...this.hotkeys,
|
||||
...profiles.map(profile => ({
|
||||
id: `profile.${profile.id}`,
|
||||
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
||||
name: `New tab: ${profile.name}`,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return profile.id!.replace(/\./g, '-')
|
||||
}
|
||||
}
|
||||
|
@@ -151,8 +151,9 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
|
||||
hotkeys.hotkey$.subscribe(async (hotkey) => {
|
||||
if (hotkey.startsWith('profile.')) {
|
||||
const id = hotkey.split('.')[1]
|
||||
const profile = (await profilesService.getProfiles()).find(x => x.id === id)
|
||||
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||
const profiles = await profilesService.getProfiles()
|
||||
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
|
||||
if (profile) {
|
||||
profilesService.openNewTabForProfile(profile)
|
||||
}
|
||||
|
@@ -174,6 +174,9 @@ export class AppService {
|
||||
* @param inputs Properties to be assigned on the new tab component instance
|
||||
*/
|
||||
openNewTab <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
|
||||
if (params.type as any === SplitTabComponent) {
|
||||
return this.openNewTabRaw(params)
|
||||
}
|
||||
const splitTab = this.tabsService.create({ type: SplitTabComponent })
|
||||
const tab = this.tabsService.create(params)
|
||||
splitTab.addTab(tab, null, 'r')
|
||||
|
@@ -2,7 +2,7 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import * as mixpanel from 'mixpanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ConfigService } from './config.service'
|
||||
import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
|
||||
import { PlatformService, BOOTSTRAP_DATA, BootstrapData, HostAppService } from '../api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HomeBaseService {
|
||||
@@ -13,6 +13,7 @@ export class HomeBaseService {
|
||||
private constructor (
|
||||
private config: ConfigService,
|
||||
private platform: PlatformService,
|
||||
private hostApp: HostAppService,
|
||||
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
|
||||
) {
|
||||
this.appVersion = platform.getAppVersion()
|
||||
@@ -28,20 +29,11 @@ export class HomeBaseService {
|
||||
|
||||
reportBug (): void {
|
||||
let body = `Version: ${this.appVersion}\n`
|
||||
body += `Platform: ${process.platform} ${this.platform.getOSRelease()}\n`
|
||||
const label = {
|
||||
aix: 'OS: IBM AIX',
|
||||
android: 'OS: Android',
|
||||
darwin: 'OS: macOS',
|
||||
freebsd: 'OS: FreeBSD',
|
||||
linux: 'OS: Linux',
|
||||
openbsd: 'OS: OpenBSD',
|
||||
sunos: 'OS: Solaris',
|
||||
win32: 'OS: Windows',
|
||||
}[process.platform]
|
||||
body += `Platform: ${this.hostApp.platform} ${process.arch} ${this.platform.getOSRelease()}\n`
|
||||
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
||||
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
|
||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n`
|
||||
body += `Frontend: ${this.config.store.terminal?.frontend}\n\n`
|
||||
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}`)
|
||||
}
|
||||
|
||||
enableAnalytics (): void {
|
||||
|
@@ -72,20 +72,21 @@ export class HotkeysService {
|
||||
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
|
||||
hostApp: HostAppService,
|
||||
) {
|
||||
const events = ['keydown', 'keyup']
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
|
||||
this._keyEvent.next(nativeEvent)
|
||||
this.pushKeyEvent(eventType, nativeEvent)
|
||||
if (hostApp.platform === Platform.Web && this.matchActiveHotkey(true) !== null) {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
}
|
||||
})
|
||||
})
|
||||
this.config.ready$.toPromise().then(async () => {
|
||||
const hotkeys = await this.getHotkeyDescriptions()
|
||||
this.hotkeyDescriptions = hotkeys
|
||||
const events = ['keydown', 'keyup']
|
||||
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
|
||||
this._keyEvent.next(nativeEvent)
|
||||
this.pushKeyEvent(eventType, nativeEvent)
|
||||
if (hostApp.platform === Platform.Web && this.matchActiveHotkey(true) !== null) {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// deprecated
|
||||
@@ -121,7 +122,7 @@ export class HotkeysService {
|
||||
}
|
||||
|
||||
for (const [key, time] of this.pressedKeyTimestamps.entries()) {
|
||||
if (time < performance.now() - 5000) {
|
||||
if (time < performance.now() - 2000) {
|
||||
this.removePressedKey(key)
|
||||
}
|
||||
}
|
||||
|
@@ -84,8 +84,9 @@ export class ProfilesService {
|
||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||
return {
|
||||
icon: profile.icon,
|
||||
name: profile.group ? `${fullProfile.group} / ${fullProfile.name}` : fullProfile.name,
|
||||
icon: profile.icon,
|
||||
color: profile.color,
|
||||
description: this.providerForProfile(fullProfile)?.getDescription(fullProfile),
|
||||
}
|
||||
}
|
||||
@@ -99,6 +100,7 @@ export class ProfilesService {
|
||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
icon: 'fas fa-history',
|
||||
color: p.color,
|
||||
callback: async () => {
|
||||
if (p.id) {
|
||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||
|
@@ -34,6 +34,7 @@ export class TabRecoveryService {
|
||||
const token = await tab.getRecoveryToken()
|
||||
if (token) {
|
||||
token.tabTitle = tab.title
|
||||
token.tabCustomTitle = tab.customTitle
|
||||
if (tab.color) {
|
||||
token.tabColor = tab.color
|
||||
}
|
||||
@@ -55,6 +56,7 @@ export class TabRecoveryService {
|
||||
tab.inputs = tab.inputs ?? {}
|
||||
tab.inputs.color = token.tabColor ?? null
|
||||
tab.inputs.title = token.tabTitle || ''
|
||||
tab.inputs.customTitle = token.tabCustomTitle || ''
|
||||
tab.inputs.disableDynamicTitle = token.disableDynamicTitle
|
||||
return tab
|
||||
} catch (error) {
|
||||
|
@@ -2,7 +2,6 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabRecoveryService } from './tabRecovery.service'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
||||
export interface TabComponentType<T extends BaseTabComponent> {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||
new (...args: any[]): T
|
||||
|
@@ -13,6 +13,7 @@ import { TabsService } from './services/tabs.service'
|
||||
import { HotkeysService } from './services/hotkeys.service'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SplitLayoutProfilesService } from './profiles'
|
||||
import { TAB_COLORS } from './utils'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
@@ -89,16 +90,6 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
@@ -127,8 +118,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
},
|
||||
{
|
||||
label: 'Color',
|
||||
sublabel: COLORS.find(x => x.value === tab.color)?.name,
|
||||
submenu: COLORS.map(color => ({
|
||||
sublabel: TAB_COLORS.find(x => x.value === tab.color)?.name,
|
||||
submenu: TAB_COLORS.map(color => ({
|
||||
label: color.name,
|
||||
type: 'radio',
|
||||
checked: tab.color === color.value,
|
||||
|
@@ -230,26 +230,25 @@ hotkey-input-modal {
|
||||
}
|
||||
}
|
||||
|
||||
.list-group.list-group-flush .list-group-item:not(.list-group-item-action) {
|
||||
.list-group.list-group-flush .list-group-item {
|
||||
background: transparent;
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.list-group-item-action {
|
||||
&:hover, &.active {
|
||||
background: $list-group-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-light {
|
||||
.list-group-item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
border-top: 1px solid rgba(0, 21, 43, .4);
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.list-group-item-action {
|
||||
&:hover, &.active {
|
||||
background: $list-group-hover-bg;
|
||||
|
@@ -26,8 +26,8 @@ $purple: #613d7c !default;
|
||||
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
||||
$content-bg-solid: #1D272D;
|
||||
|
||||
$table-bg: rgba(255,255,255,.05);
|
||||
$table-bg-hover: rgba(255,255,255,.1);
|
||||
$table-bg: rgba(255,255,255,.025);
|
||||
$table-bg-hover: rgba(255,255,255,.05);
|
||||
$table-border-color: rgba(255,255,255,.1);
|
||||
|
||||
$theme-colors: (
|
||||
@@ -88,7 +88,7 @@ $list-group-item-padding-y: 0.8rem;
|
||||
$list-group-item-padding-x: 1rem;
|
||||
|
||||
$list-group-hover-bg: $table-bg-hover;
|
||||
$list-group-active-bg: rgba(255,255,255,.2);
|
||||
$list-group-active-bg: rgba(255,255,255,.05);
|
||||
$list-group-active-color: $component-active-color;
|
||||
$list-group-active-border-color: translate;
|
||||
|
||||
|
@@ -54,3 +54,13 @@ export class ResettableTimeout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TAB_COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
@@ -50,11 +50,6 @@ call-bind@^1.0.0, call-bind@^1.0.2:
|
||||
function-bind "^1.1.1"
|
||||
get-intrinsic "^1.0.2"
|
||||
|
||||
core-js@^3.1.2:
|
||||
version "3.15.2"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.2.tgz#740660d2ff55ef34ce664d7e2455119c5bdd3d61"
|
||||
integrity sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==
|
||||
|
||||
debug@4:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage, PowerSaveBlocker } from 'electron'
|
||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, PowerSaveBlocker } from 'electron'
|
||||
import * as remote from '@electron/remote'
|
||||
|
||||
export interface MessageBoxResponse {
|
||||
@@ -15,7 +15,6 @@ export class ElectronService {
|
||||
dialog: Dialog
|
||||
clipboard: Clipboard
|
||||
globalShortcut: GlobalShortcut
|
||||
nativeImage: typeof NativeImage
|
||||
screen: Screen
|
||||
remote: Remote
|
||||
process: any
|
||||
@@ -38,7 +37,6 @@ export class ElectronService {
|
||||
this.screen = remote.screen
|
||||
this.dialog = remote.dialog
|
||||
this.globalShortcut = remote.globalShortcut
|
||||
this.nativeImage = remote.nativeImage
|
||||
this.autoUpdater = remote.autoUpdater
|
||||
this.powerSaveBlocker = remote.powerSaveBlocker
|
||||
this.TouchBar = remote.TouchBar
|
||||
|
@@ -12,9 +12,9 @@ export interface Bounds {
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ElectronHostWindow extends HostWindowService {
|
||||
get isFullscreen (): boolean { return this._isFullScreen}
|
||||
get isFullscreen (): boolean { return this._isFullscreen }
|
||||
|
||||
private _isFullScreen = false
|
||||
private _isFullscreen = false
|
||||
|
||||
constructor (
|
||||
zone: NgZone,
|
||||
@@ -23,28 +23,26 @@ export class ElectronHostWindow extends HostWindowService {
|
||||
) {
|
||||
super()
|
||||
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => {
|
||||
this._isFullScreen = true
|
||||
this._isFullscreen = true
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => {
|
||||
this._isFullScreen = false
|
||||
this._isFullscreen = false
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-shown', () => {
|
||||
zone.run(() => this.windowShown.next())
|
||||
})
|
||||
electron.ipcRenderer.on('host:window-shown', () => zone.run(() => this.windowShown.next()))
|
||||
|
||||
electron.ipcRenderer.on('host:window-close-request', () => {
|
||||
zone.run(() => this.windowCloseRequest.next())
|
||||
})
|
||||
electron.ipcRenderer.on('host:window-close-request', () => zone.run(() => {
|
||||
this.windowCloseRequest.next()
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-moved', () => {
|
||||
zone.run(() => this.windowMoved.next())
|
||||
})
|
||||
electron.ipcRenderer.on('host:window-moved', () => zone.run(() => {
|
||||
this.windowMoved.next()
|
||||
}))
|
||||
|
||||
electron.ipcRenderer.on('host:window-focused', () => {
|
||||
zone.run(() => this.windowFocused.next())
|
||||
})
|
||||
electron.ipcRenderer.on('host:window-focused', () => zone.run(() => {
|
||||
this.windowFocused.next()
|
||||
}))
|
||||
}
|
||||
|
||||
getWindow (): BrowserWindow {
|
||||
@@ -64,7 +62,7 @@ export class ElectronHostWindow extends HostWindowService {
|
||||
}
|
||||
|
||||
toggleFullscreen (): void {
|
||||
this.getWindow().setFullScreen(!this._isFullScreen)
|
||||
this.getWindow().setFullScreen(!this._isFullscreen)
|
||||
}
|
||||
|
||||
minimize (): void {
|
||||
|
@@ -1,56 +1,29 @@
|
||||
import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { AppService, HostAppService, Platform } from 'tabby-core'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { ElectronHostWindow } from './hostWindow.service'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TouchbarService {
|
||||
private tabsSegmentedControl: TouchBarSegmentedControl
|
||||
private tabSegments: SegmentedControlSegment[] = []
|
||||
|
||||
private constructor (
|
||||
private app: AppService,
|
||||
private hostApp: HostAppService,
|
||||
private hostWindow: ElectronHostWindow,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
if (this.hostApp.platform !== Platform.macOS) {
|
||||
return
|
||||
}
|
||||
app.tabsChanged$.subscribe(() => this.updateTabs())
|
||||
app.activeTabChange$.subscribe(() => this.updateTabs())
|
||||
app.tabsChanged$.subscribe(() => this.update())
|
||||
app.activeTabChange$.subscribe(() => this.update())
|
||||
|
||||
const activityIconPath = `${electron.app.getAppPath()}/assets/activity.png`
|
||||
const activityIcon = this.electron.nativeImage.createFromPath(activityIconPath)
|
||||
app.tabOpened$.subscribe(tab => {
|
||||
tab.titleChange$.subscribe(title => {
|
||||
const segment = this.tabSegments[app.tabs.indexOf(tab)]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (segment) {
|
||||
segment.label = this.shortenTitle(title)
|
||||
this.tabsSegmentedControl.segments = this.tabSegments
|
||||
}
|
||||
})
|
||||
tab.activity$.subscribe(hasActivity => {
|
||||
const showIcon = this.app.activeTab !== tab && hasActivity
|
||||
const segment = this.tabSegments[app.tabs.indexOf(tab)]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (segment) {
|
||||
segment.icon = showIcon ? activityIcon : undefined
|
||||
}
|
||||
})
|
||||
tab.titleChange$.subscribe(() => this.update())
|
||||
tab.activity$.subscribe(() => this.update())
|
||||
})
|
||||
}
|
||||
|
||||
updateTabs (): void {
|
||||
this.tabSegments = this.app.tabs.map(tab => ({
|
||||
label: this.shortenTitle(tab.title),
|
||||
ipcRenderer.on('touchbar-selection', (_event, index) => this.zone.run(() => {
|
||||
this.app.selectTab(this.app.tabs[index])
|
||||
}))
|
||||
this.tabsSegmentedControl.segments = this.tabSegments
|
||||
this.tabsSegmentedControl.selectedIndex = this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : 0
|
||||
}
|
||||
|
||||
update (): void {
|
||||
@@ -58,20 +31,12 @@ export class TouchbarService {
|
||||
return
|
||||
}
|
||||
|
||||
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
|
||||
segments: this.tabSegments,
|
||||
selectedIndex: this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined,
|
||||
change: (selectedIndex) => this.zone.run(() => {
|
||||
this.app.selectTab(this.app.tabs[selectedIndex])
|
||||
}),
|
||||
})
|
||||
const tabSegments = this.app.tabs.map(tab => ({
|
||||
label: this.shortenTitle(tab.title),
|
||||
hasActivity: this.app.activeTab !== tab && tab.hasActivity,
|
||||
}))
|
||||
|
||||
const touchBar = new this.electron.TouchBar({
|
||||
items: [
|
||||
this.tabsSegmentedControl,
|
||||
],
|
||||
})
|
||||
this.hostWindow.setTouchBar(touchBar)
|
||||
ipcRenderer.send('window-set-touch-bar', tabSegments, this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined)
|
||||
}
|
||||
|
||||
private shortenTitle (title: string): string {
|
||||
|
@@ -17,7 +17,6 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"opentype.js": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -174,11 +174,6 @@ has@^1.0.3:
|
||||
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"
|
||||
|
@@ -46,7 +46,10 @@
|
||||
input.form-control.w-50(
|
||||
type='text',
|
||||
[(ngModel)]='profile.color',
|
||||
placeholder='#000000'
|
||||
placeholder='#000000',
|
||||
alwaysVisibleTypeahead,
|
||||
[ngbTypeahead]='colorsAutocomplete',
|
||||
[resultFormatter]='colorsFormatter'
|
||||
)
|
||||
|
||||
.form-line
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
|
||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService } from 'tabby-core'
|
||||
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
|
||||
|
||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||
const iconsClassList = Object.keys(iconsData).map(
|
||||
@@ -39,6 +39,20 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||
)].sort() as string[]
|
||||
}
|
||||
|
||||
colorsAutocomplete = text$ => text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map((q: string) =>
|
||||
TAB_COLORS
|
||||
.filter(x => !q || x.name.toLowerCase().startsWith(q.toLowerCase()))
|
||||
.map(x => x.value)
|
||||
)
|
||||
)
|
||||
|
||||
colorsFormatter = value => {
|
||||
return TAB_COLORS.find(x => x.value === value)?.name ?? value
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this._profile = this.profile
|
||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile)
|
||||
|
@@ -16,7 +16,7 @@
|
||||
div(*ngIf='!sftp') Connecting
|
||||
div(*ngIf='sftp')
|
||||
div(*ngIf='fileList === null') Loading
|
||||
.list-group.list-group-light(*ngIf='fileList !== null')
|
||||
.list-group.list-group-flush(*ngIf='fileList !== null')
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
*ngIf='path !== "/"',
|
||||
(click)='goUp()'
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
|
||||
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent, PartialProfile } from 'tabby-core'
|
||||
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
|
||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
|
||||
@@ -20,7 +20,7 @@ export class SSHProfileSettingsComponent {
|
||||
|
||||
supportedAlgorithms = supportedAlgorithms
|
||||
algorithms: Record<string, Record<string, boolean>> = {}
|
||||
jumpHosts: SSHProfile[]
|
||||
jumpHosts: PartialProfile<SSHProfile>[]
|
||||
@ViewChild('loginScriptsSettings') loginScriptsSettings: LoginScriptsSettingsComponent|null
|
||||
|
||||
constructor (
|
||||
|
@@ -2,7 +2,7 @@ import colors from 'ansi-colors'
|
||||
import { Component, Injector, HostListener } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { Platform, RecoveryToken } from 'tabby-core'
|
||||
import { PartialProfile, Platform, ProfilesService, RecoveryToken } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHSession } from '../session/ssh'
|
||||
@@ -32,6 +32,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
injector: Injector,
|
||||
public ssh: SSHService,
|
||||
private ngbModal: NgbModal,
|
||||
private profilesService: ProfilesService,
|
||||
) {
|
||||
super(injector)
|
||||
}
|
||||
@@ -78,13 +79,16 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
|
||||
async setupOneSession (session: SSHSession): Promise<void> {
|
||||
if (session.profile.options.jumpHost) {
|
||||
const jumpConnection: SSHProfile|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
|
||||
const jumpConnection: PartialProfile<SSHProfile>|null = this.config.store.profiles.find(x => x.id === session.profile.options.jumpHost)
|
||||
|
||||
if (!jumpConnection) {
|
||||
throw new Error(`${session.profile.options.host}: jump host "${session.profile.options.jumpHost}" not found in your config`)
|
||||
}
|
||||
|
||||
const jumpSession = new SSHSession(this.injector, jumpConnection)
|
||||
const jumpSession = new SSHSession(
|
||||
this.injector,
|
||||
this.profilesService.getConfigProxyForProfile(jumpConnection)
|
||||
)
|
||||
|
||||
await this.setupOneSession(jumpSession)
|
||||
|
||||
|
@@ -170,11 +170,13 @@ export class TelnetSession extends BaseSession {
|
||||
}
|
||||
if (command === TelnetCommands.DO) {
|
||||
if (option === TelnetOptions.NEGO_WINDOW_SIZE) {
|
||||
this.resize(0, 0)
|
||||
this.emitSize()
|
||||
this.emitTelnet(TelnetCommands.WILL, option)
|
||||
} else if (option === TelnetOptions.ECHO) {
|
||||
this.echoEnabled = true
|
||||
this.emitTelnet(TelnetCommands.WILL, option)
|
||||
} else if (option === TelnetOptions.TERMINAL_TYPE) {
|
||||
this.emitTelnet(TelnetCommands.WILL, option)
|
||||
this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')]))
|
||||
} else {
|
||||
this.logger.debug('(!) Unhandled option')
|
||||
@@ -210,10 +212,18 @@ export class TelnetSession extends BaseSession {
|
||||
this.lastHeight = h
|
||||
}
|
||||
if (this.lastWidth && this.lastHeight && this.telnetProtocol) {
|
||||
this.emitSize()
|
||||
}
|
||||
}
|
||||
|
||||
private emitSize () {
|
||||
if (this.lastWidth && this.lastHeight) {
|
||||
this.emitTelnetSuboption(TelnetOptions.NEGO_WINDOW_SIZE, Buffer.from([
|
||||
this.lastWidth >> 8, this.lastWidth & 0xff,
|
||||
this.lastHeight >> 8, this.lastHeight & 0xff,
|
||||
]))
|
||||
} else {
|
||||
this.emitTelnet(TelnetCommands.WONT, TelnetOptions.NEGO_WINDOW_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,6 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"opentype.js": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -32,7 +31,7 @@
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"utils-decorators": "^1.8.1",
|
||||
"xterm": "^4.9.0-beta.7",
|
||||
"xterm": "npm:@tabby-gang/xterm@4.14.0-beta.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-ligatures": "^0.5.0",
|
||||
"xterm-addon-search": "^0.8.0",
|
||||
|
@@ -6,9 +6,9 @@ import { Subject, Observable, interval, debounce } from 'rxjs'
|
||||
import { PassThrough, Readable, Writable } from 'stream'
|
||||
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
|
||||
|
||||
export type InputMode = null | 'local-echo' | 'readline' | 'readline-hex' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type OutputMode = null | 'hex' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type NewlineMode = null | 'cr' | 'lf' | 'crlf' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type InputMode = null | 'local-echo' | 'readline' | 'readline-hex'
|
||||
export type OutputMode = null | 'hex'
|
||||
export type NewlineMode = null | 'cr' | 'lf' | 'crlf'
|
||||
|
||||
export interface StreamProcessingOptions {
|
||||
inputMode?: InputMode
|
||||
|
@@ -9,7 +9,6 @@ h3.mb-3 Terminal
|
||||
[(ngModel)]='config.store.terminal.frontend',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(value='hterm') hterm
|
||||
option(value='xterm') xterm
|
||||
option(value='xterm-webgl') xterm (WebGL)
|
||||
|
||||
|
@@ -1,120 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/** @hidden */
|
||||
export const hterm = require('hterm-umdjs')
|
||||
|
||||
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
|
||||
|
||||
/** @hidden */
|
||||
export const preferenceManager = new hterm.hterm.PreferenceManager('default')
|
||||
|
||||
hterm.hterm.VT.ESC['k'] = function (parseState) {
|
||||
parseState.resetArguments()
|
||||
|
||||
function parseOSC (ps) {
|
||||
if (!this.parseUntilStringTerminator_(ps) || ps.func === parseOSC) {
|
||||
return
|
||||
}
|
||||
|
||||
this.terminal.setWindowTitle(ps.args[0])
|
||||
}
|
||||
parseState.func = parseOSC
|
||||
}
|
||||
|
||||
preferenceManager.set('background-color', '#1D272D')
|
||||
preferenceManager.set('color-palette-overrides', {
|
||||
0: '#1D272D',
|
||||
})
|
||||
|
||||
hterm.hterm.Terminal.prototype.showOverlay = () => null
|
||||
|
||||
hterm.hterm.Terminal.prototype.setCSS = function (css) {
|
||||
const doc = this.scrollPort_.document_
|
||||
if (!doc.querySelector('#user-css')) {
|
||||
const node = doc.createElement('style')
|
||||
node.id = 'user-css'
|
||||
doc.head.appendChild(node)
|
||||
}
|
||||
doc.querySelector('#user-css').innerText = css
|
||||
}
|
||||
|
||||
const oldCharWidthDisregardAmbiguous = hterm.lib.wc.charWidthDisregardAmbiguous
|
||||
hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => {
|
||||
if ((codepoint >= 0x1f300 && codepoint <= 0x1f64f) ||
|
||||
(codepoint >= 0x1f680 && codepoint <= 0x1f6ff)) {
|
||||
return 2
|
||||
}
|
||||
return oldCharWidthDisregardAmbiguous(codepoint)
|
||||
}
|
||||
|
||||
hterm.hterm.Terminal.prototype.applyCursorShape = function () {
|
||||
const modes = [
|
||||
[hterm.hterm.Terminal.cursorShape.BLOCK, true],
|
||||
[this.defaultCursorShape || hterm.hterm.Terminal.cursorShape.BLOCK, false],
|
||||
[hterm.hterm.Terminal.cursorShape.BLOCK, false],
|
||||
[hterm.hterm.Terminal.cursorShape.UNDERLINE, true],
|
||||
[hterm.hterm.Terminal.cursorShape.UNDERLINE, false],
|
||||
[hterm.hterm.Terminal.cursorShape.BEAM, true],
|
||||
[hterm.hterm.Terminal.cursorShape.BEAM, false],
|
||||
]
|
||||
const modeNumber = this.cursorMode || 1
|
||||
if (modeNumber >= modes.length) {
|
||||
console.warn('Unknown cursor style: ' + modeNumber)
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.setCursorShape(modes[modeNumber][0])
|
||||
this.setCursorBlink(modes[modeNumber][1])
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.setCursorVisible(true)
|
||||
})
|
||||
}
|
||||
|
||||
hterm.hterm.VT.CSI[' q'] = function (parseState) {
|
||||
const arg = parseState.args[0]
|
||||
this.terminal.cursorMode = arg
|
||||
this.terminal.applyCursorShape()
|
||||
}
|
||||
|
||||
hterm.hterm.VT.OSC['4'] = function (parseState) {
|
||||
const args: string[] = parseState.args[0].split(';')
|
||||
|
||||
const pairCount = args.length / 2
|
||||
const colorPalette = this.terminal.getTextAttributes().colorPalette
|
||||
const responseArray: string[] = []
|
||||
|
||||
for (let pairNumber = 0; pairNumber < pairCount; ++pairNumber) {
|
||||
const colorIndex = parseInt(args[pairNumber * 2])
|
||||
let colorValue = args[pairNumber * 2 + 1]
|
||||
|
||||
if (colorIndex >= colorPalette.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (colorValue === '?') {
|
||||
colorValue = hterm.lib.colors.rgbToX11(colorPalette[colorIndex])
|
||||
if (colorValue) {
|
||||
responseArray.push(colorIndex.toString() + ';' + colorValue)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
colorValue = hterm.lib.colors.x11ToCSS(colorValue)
|
||||
if (colorValue) {
|
||||
this.terminal.colorPaletteOverrides[colorIndex] = colorValue
|
||||
colorPalette[colorIndex] = colorValue
|
||||
}
|
||||
}
|
||||
|
||||
if (responseArray.length) {
|
||||
this.terminal.io.sendString('\x1b]4;' + responseArray.join(';') + '\x07')
|
||||
}
|
||||
}
|
||||
|
||||
const _collapseToEnd = Selection.prototype.collapseToEnd
|
||||
Selection.prototype.collapseToEnd = function () {
|
||||
try {
|
||||
_collapseToEnd.apply(this)
|
||||
} catch (e) { }
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
x-screen {
|
||||
transition: 0.125s ease background;
|
||||
background: transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
x-row > span {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
|
||||
&.wc-node {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "monospace-fallback";
|
||||
src: url(../fonts/SourceCodePro.otf) format("opentype");
|
||||
}
|
@@ -1,302 +0,0 @@
|
||||
import { Injector } from '@angular/core'
|
||||
import { ConfigService, getCSSFontFamily, ThemesService } from 'tabby-core'
|
||||
import { Frontend, SearchOptions } from './frontend'
|
||||
import { hterm, preferenceManager } from './hterm'
|
||||
|
||||
/** @hidden */
|
||||
export class HTermFrontend extends Frontend {
|
||||
term: any
|
||||
io: any
|
||||
private htermIframe: HTMLElement
|
||||
private initialized = false
|
||||
private configuredFontSize = 0
|
||||
private configuredLinePadding = 0
|
||||
private configuredBackgroundColor = 'transparent'
|
||||
private zoom = 0
|
||||
|
||||
private configService: ConfigService
|
||||
private themesService: ThemesService
|
||||
|
||||
constructor (injector: Injector) {
|
||||
super(injector)
|
||||
this.configService = injector.get(ConfigService)
|
||||
this.themesService = injector.get(ThemesService)
|
||||
}
|
||||
|
||||
async attach (host: HTMLElement): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
this.init()
|
||||
this.initialized = true
|
||||
preferenceManager.set('background-color', 'transparent')
|
||||
this.term.decorate(host)
|
||||
this.htermIframe = this.term.scrollPort_.iframe_
|
||||
} else {
|
||||
host.appendChild(this.htermIframe)
|
||||
}
|
||||
}
|
||||
|
||||
getSelection (): string {
|
||||
return this.term.getSelectionText()
|
||||
}
|
||||
|
||||
copySelection (): void {
|
||||
this.term.copySelectionToClipboard()
|
||||
}
|
||||
|
||||
selectAll (): void {
|
||||
const content = this.term.getDocument().body.children[0]
|
||||
const selection = content.ownerDocument.defaultView.getSelection()
|
||||
selection.setBaseAndExtent(content, 0, content, 1)
|
||||
}
|
||||
|
||||
clearSelection (): void {
|
||||
this.term.getDocument().getSelection().removeAllRanges()
|
||||
}
|
||||
|
||||
focus (): void {
|
||||
setTimeout(() => {
|
||||
this.term.scrollPort_.resize()
|
||||
this.term.scrollPort_.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
write (data: string): void {
|
||||
this.io.writeUTF8(data)
|
||||
}
|
||||
|
||||
clear (): void {
|
||||
this.term.wipeContents()
|
||||
this.term.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
configure (): void {
|
||||
const config = this.configService.store
|
||||
|
||||
this.configuredFontSize = config.terminal.fontSize
|
||||
this.configuredLinePadding = config.terminal.linePadding
|
||||
this.setFontSize()
|
||||
|
||||
preferenceManager.set('font-family', getCSSFontFamily(config))
|
||||
preferenceManager.set('enable-bold', true)
|
||||
// preferenceManager.set('audible-bell-sound', '')
|
||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
|
||||
preferenceManager.set('enable-clipboard-notice', false)
|
||||
preferenceManager.set('receive-encoding', 'raw')
|
||||
preferenceManager.set('send-encoding', 'raw')
|
||||
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
|
||||
preferenceManager.set('scrollbar-visible', process.platform === 'darwin')
|
||||
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
|
||||
preferenceManager.set('pass-meta-v', false)
|
||||
preferenceManager.set('alt-is-meta', config.terminal.altIsMeta)
|
||||
preferenceManager.set('alt-sends-what', 'browser-key')
|
||||
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
|
||||
preferenceManager.set('pass-alt-number', true)
|
||||
preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
|
||||
preferenceManager.set('clear-selection-after-copy', true)
|
||||
preferenceManager.set('scroll-on-output', false)
|
||||
preferenceManager.set('scroll-on-keystroke', config.terminal.scrollOnInput)
|
||||
|
||||
if (config.terminal.colorScheme.foreground) {
|
||||
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
|
||||
}
|
||||
|
||||
if (config.terminal.background === 'colorScheme') {
|
||||
if (config.terminal.colorScheme.background) {
|
||||
preferenceManager.set('background-color', config.terminal.colorScheme.background)
|
||||
}
|
||||
} else {
|
||||
preferenceManager.set('background-color', config.appearance.vibrancy ? 'transparent' : this.themesService.findCurrentTheme().terminalBackground)
|
||||
}
|
||||
|
||||
this.configuredBackgroundColor = preferenceManager.get('background-color')
|
||||
|
||||
if (!this.term) {
|
||||
return
|
||||
}
|
||||
|
||||
let css = require('./hterm.userCSS.scss') // eslint-disable-line
|
||||
if (!config.terminal.ligatures) {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 0;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
`
|
||||
} else {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 1;
|
||||
font-variant-ligatures: initial;
|
||||
}
|
||||
`
|
||||
}
|
||||
css += config.appearance.css
|
||||
this.term.setCSS(css)
|
||||
|
||||
if (config.terminal.colorScheme.colors) {
|
||||
preferenceManager.set(
|
||||
'color-palette-overrides',
|
||||
Object.assign([], config.terminal.colorScheme.colors, this.term.colorPaletteOverrides)
|
||||
)
|
||||
}
|
||||
if (config.terminal.colorScheme.cursor) {
|
||||
preferenceManager.set('cursor-color', config.terminal.colorScheme.cursor)
|
||||
}
|
||||
|
||||
this.term.setBracketedPaste(config.terminal.bracketedPaste)
|
||||
this.term.defaultCursorShape = {
|
||||
block: hterm.hterm.Terminal.cursorShape.BLOCK,
|
||||
underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
|
||||
beam: hterm.hterm.Terminal.cursorShape.BEAM,
|
||||
}[config.terminal.cursor]
|
||||
this.term.applyCursorShape()
|
||||
this.term.setCursorBlink(config.terminal.cursorBlink)
|
||||
if (config.terminal.cursorBlink) {
|
||||
this.term.onCursorBlink_()
|
||||
}
|
||||
}
|
||||
|
||||
setZoom (zoom: number): void {
|
||||
this.zoom = zoom
|
||||
this.setFontSize()
|
||||
}
|
||||
|
||||
visualBell (): void {
|
||||
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
|
||||
setTimeout(() => {
|
||||
preferenceManager.set('background-color', this.configuredBackgroundColor)
|
||||
}, 125)
|
||||
}
|
||||
|
||||
scrollToBottom (): void {
|
||||
this.term.scrollEnd()
|
||||
}
|
||||
|
||||
findNext (_term: string, _searchOptions?: SearchOptions): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
findPrevious (_term: string, _searchOptions?: SearchOptions): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
saveState (): any { }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
restoreState (_state: string): void { }
|
||||
|
||||
private setFontSize () {
|
||||
const size = this.configuredFontSize * Math.pow(1.1, this.zoom)
|
||||
preferenceManager.set('font-size', size)
|
||||
if (this.term) {
|
||||
setTimeout(() => {
|
||||
this.term.scrollPort_.characterSize = this.term.scrollPort_.measureCharacterSize()
|
||||
this.term.setFontSize(size)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private init () {
|
||||
this.term = new hterm.hterm.Terminal()
|
||||
this.term.colorPaletteOverrides = []
|
||||
this.term.onTerminalReady = () => {
|
||||
this.term.installKeyboard()
|
||||
this.term.scrollPort_.setCtrlVPaste(true)
|
||||
this.io = this.term.io.push()
|
||||
this.io.onVTKeystroke = this.io.sendString = data => this.input.next(Buffer.from(data, 'utf-8'))
|
||||
this.io.onTerminalResize = (columns, rows) => {
|
||||
this.resize.next({ columns, rows })
|
||||
}
|
||||
this.ready.next()
|
||||
this.ready.complete()
|
||||
|
||||
this.term.scrollPort_.document_.addEventListener('dragOver', event => {
|
||||
this.dragOver.next(event)
|
||||
})
|
||||
|
||||
this.term.scrollPort_.document_.addEventListener('drop', event => {
|
||||
this.drop.next(event)
|
||||
})
|
||||
}
|
||||
this.term.setWindowTitle = title => this.title.next(title)
|
||||
|
||||
const _setAlternateMode = this.term.setAlternateMode.bind(this.term)
|
||||
this.term.setAlternateMode = (state) => {
|
||||
_setAlternateMode(state)
|
||||
this.alternateScreenActive.next(state)
|
||||
}
|
||||
|
||||
this.term.primaryScreen_.syncSelectionCaret = () => null
|
||||
this.term.alternateScreen_.syncSelectionCaret = () => null
|
||||
this.term.primaryScreen_.terminal = this.term
|
||||
this.term.alternateScreen_.terminal = this.term
|
||||
|
||||
this.term.scrollPort_.onPaste_ = (event) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const _resize = this.term.scrollPort_.resize.bind(this.term.scrollPort_)
|
||||
this.term.scrollPort_.resize = () => {
|
||||
if (this.enableResizing) {
|
||||
_resize()
|
||||
}
|
||||
}
|
||||
|
||||
const _onMouse = this.term.onMouse_.bind(this.term)
|
||||
this.term.onMouse_ = (event) => {
|
||||
this.mouseEvent.next(event)
|
||||
if (event.type === 'mousedown' && event.which === 3) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.type === 'mousewheel' && event.altKey) {
|
||||
event.preventDefault()
|
||||
}
|
||||
_onMouse(event)
|
||||
}
|
||||
|
||||
this.term.ringBell = () => this.bell.next()
|
||||
|
||||
for (const screen of [this.term.primaryScreen_, this.term.alternateScreen_]) {
|
||||
const _insertString = screen.insertString.bind(screen)
|
||||
screen.insertString = (data) => {
|
||||
_insertString(data)
|
||||
this.contentUpdated.next()
|
||||
}
|
||||
|
||||
const _deleteChars = screen.deleteChars.bind(screen)
|
||||
screen.deleteChars = (count) => {
|
||||
const ret = _deleteChars(count)
|
||||
this.contentUpdated.next()
|
||||
return ret
|
||||
}
|
||||
|
||||
const _expandSelection = screen.expandSelection.bind(screen)
|
||||
screen.expandSelection = (selection) => {
|
||||
// Drop whitespace at the end of selection
|
||||
const range = selection.getRangeAt(0)
|
||||
if (range.endOffset > 0 && range.endContainer.nodeType === 3 && range.endContainer.textContent !== '') {
|
||||
while (/[\s\S]+\s$/.test(range.endContainer.textContent.substr(0, range.endOffset))) {
|
||||
range.setEnd(range.endContainer, range.endOffset - 1)
|
||||
}
|
||||
}
|
||||
_expandSelection(selection)
|
||||
}
|
||||
}
|
||||
|
||||
const _measureCharacterSize = this.term.scrollPort_.measureCharacterSize.bind(this.term.scrollPort_)
|
||||
this.term.scrollPort_.measureCharacterSize = () => {
|
||||
const size = _measureCharacterSize()
|
||||
size.height += this.configuredLinePadding
|
||||
return size
|
||||
}
|
||||
|
||||
const _onCursorBlink = this.term.onCursorBlink_.bind(this.term)
|
||||
this.term.onCursorBlink_ = () => {
|
||||
this.term.cursorNode_.style.opacity = '0'
|
||||
_onCursorBlink()
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
import TabbyCorePlugin, { ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler } from 'tabby-core'
|
||||
import TabbyCorePlugin, { ConfigProvider, HotkeyProvider, TabContextMenuItemProvider, CLIHandler } from 'tabby-core'
|
||||
import { SettingsTabProvider } from 'tabby-settings'
|
||||
|
||||
import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component'
|
||||
@@ -29,9 +29,7 @@ import { TerminalConfigProvider } from './config'
|
||||
import { TerminalHotkeyProvider } from './hotkeys'
|
||||
import { CopyPasteContextMenu, MiscContextMenu, LegacyContextMenu } from './tabContextMenu'
|
||||
|
||||
import { hterm } from './frontends/hterm'
|
||||
import { Frontend } from './frontends/frontend'
|
||||
import { HTermFrontend } from './frontends/htermFrontend'
|
||||
import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend'
|
||||
import { TerminalCLIHandler } from './cli'
|
||||
|
||||
@@ -83,37 +81,10 @@ import { TerminalCLIHandler } from './cli'
|
||||
LoginScriptsSettingsComponent,
|
||||
],
|
||||
})
|
||||
export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
constructor (
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
const events = [
|
||||
{
|
||||
name: 'keydown',
|
||||
htermHandler: 'onKeyDown_',
|
||||
},
|
||||
{
|
||||
name: 'keyup',
|
||||
htermHandler: 'onKeyUp_',
|
||||
},
|
||||
]
|
||||
events.forEach(event => {
|
||||
const oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
|
||||
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
|
||||
hotkeys.pushKeyEvent(event.name, nativeEvent)
|
||||
if (hotkeys.matchActiveHotkey(true) !== null) {
|
||||
oldHandler.bind(this)(nativeEvent)
|
||||
} else {
|
||||
nativeEvent.stopPropagation()
|
||||
nativeEvent.preventDefault()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
export default class TerminalModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
||||
export { TerminalFrontendService, TerminalDecorator, TerminalContextMenuItemProvider, TerminalColorSchemeProvider }
|
||||
export { Frontend, XTermFrontend, XTermWebGLFrontend, HTermFrontend }
|
||||
export { Frontend, XTermFrontend, XTermWebGLFrontend }
|
||||
export { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
|
||||
export * from './api/interfaces'
|
||||
export * from './api/streamProcessing'
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Injectable, Injector } from '@angular/core'
|
||||
import { ConfigService } from 'tabby-core'
|
||||
import { Frontend } from '../frontends/frontend'
|
||||
import { HTermFrontend } from '../frontends/htermFrontend'
|
||||
import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend'
|
||||
import { BaseSession } from '../session'
|
||||
|
||||
@@ -17,12 +16,11 @@ export class TerminalFrontendService {
|
||||
|
||||
getFrontend (session?: BaseSession|null): Frontend {
|
||||
if (!session) {
|
||||
const frontend: Frontend = new {
|
||||
const cls = {
|
||||
xterm: XTermFrontend,
|
||||
'xterm-webgl': XTermWebGLFrontend,
|
||||
hterm: HTermFrontend,
|
||||
}[this.config.store.terminal.frontend](this.injector)
|
||||
return frontend
|
||||
}[this.config.store.terminal.frontend] ?? XTermFrontend
|
||||
return new cls(this.injector)
|
||||
}
|
||||
if (!this.containers.has(session)) {
|
||||
this.containers.set(
|
||||
|
@@ -3,7 +3,6 @@ module.exports = config({
|
||||
name: 'terminal',
|
||||
dirname: __dirname,
|
||||
externals: [
|
||||
'hterm-umdjs',
|
||||
'opentype.js',
|
||||
],
|
||||
})
|
||||
|
@@ -239,11 +239,6 @@ hexer@^1.5.0:
|
||||
process "^0.10.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
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"
|
||||
@@ -603,10 +598,10 @@ xterm-addon-webgl@^0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.11.1.tgz#33dd250ab52e9f51d2ff52396447962e6f53e24c"
|
||||
integrity sha512-xF6DnEoV+rPtzetMBXBZVe1kLKtus7AKdEcyfq2eMHQzhaRvC+pfnU+XiCXC85kueguqu2UkBHXZs5mihK9jOQ==
|
||||
|
||||
xterm@^4.9.0-beta.7:
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.13.0.tgz#7998de1e2ad92c4796fe45807be4f31061f3d9d1"
|
||||
integrity sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw==
|
||||
"xterm@npm:@tabby-gang/xterm@4.14.0-beta.0":
|
||||
version "4.14.0-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@tabby-gang/xterm/-/xterm-4.14.0-beta.0.tgz#6a446917179db1d1c864cd094baddcd05881e71c"
|
||||
integrity sha512-Aj1oXfJlkjjcaqxQJDshzUeiB6Cc/K3PA+gVuAtVcuFfscjArZvhSrGbaWgioUhYk5xcamxgh55fnXsvFEf9sA==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -86,24 +86,6 @@ Tabby.registerModule('crypto', {
|
||||
return a.equals(b)
|
||||
},
|
||||
})
|
||||
Tabby.registerMock('hterm-umdjs', {
|
||||
hterm: {
|
||||
PreferenceManager: class { set () {} },
|
||||
VT: {
|
||||
ESC: {},
|
||||
CSI: {},
|
||||
OSC: {},
|
||||
},
|
||||
Terminal: class {},
|
||||
Keyboard: class {},
|
||||
},
|
||||
lib: {
|
||||
wc: {},
|
||||
Storage: {
|
||||
Memory: class {},
|
||||
},
|
||||
},
|
||||
})
|
||||
Tabby.registerMock('dns', {})
|
||||
Tabby.registerMock('socksv5', {})
|
||||
Tabby.registerMock('util', require('util/'))
|
||||
|
Reference in New Issue
Block a user