mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-31 06:26:59 +00:00
Compare commits
158 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 | ||
![]() |
78f25a7679 | ||
![]() |
0c4d8b0784 | ||
![]() |
e2c8093b97 | ||
![]() |
c497a71361 | ||
![]() |
ec2982b1c4 | ||
![]() |
be0aeefdb3 | ||
![]() |
eadd8d563e | ||
![]() |
08f1ad4c75 | ||
![]() |
426606ba06 | ||
![]() |
7b59ba4b73 | ||
![]() |
0471fcec15 | ||
![]() |
4110d09dab | ||
![]() |
533837f5b7 | ||
![]() |
144924e579 | ||
![]() |
6902ccdb95 | ||
![]() |
7ed5aff168 | ||
![]() |
3f0db97a68 | ||
![]() |
231594d709 | ||
![]() |
e4ae114c71 | ||
![]() |
20000d16f8 | ||
![]() |
5e0a9b2e52 | ||
![]() |
fa70447223 | ||
![]() |
acf418b52f | ||
![]() |
28b84e38ca | ||
![]() |
3c4a078fa5 | ||
![]() |
52f4e88420 | ||
![]() |
16d9045a80 | ||
![]() |
07d7d8daba | ||
![]() |
b2b9476298 | ||
![]() |
cf7f3dffe3 | ||
![]() |
621005eb27 | ||
![]() |
d46e1de8aa | ||
![]() |
c44f3c5f25 | ||
![]() |
b3f9d48609 | ||
![]() |
edd7e9c7b7 | ||
![]() |
ab8061ab39 | ||
![]() |
c1a1f53707 | ||
![]() |
04097a0ef5 | ||
![]() |
85be974e64 | ||
![]() |
0df5fb4a34 | ||
![]() |
920b2b85b3 | ||
![]() |
4e4788bf57 | ||
![]() |
9aa60a9d0d | ||
![]() |
451ac51520 | ||
![]() |
04084aef33 | ||
![]() |
4198ca3fae | ||
![]() |
3b09dfa145 | ||
![]() |
923b559857 | ||
![]() |
58682b6bf1 | ||
![]() |
88c4198145 | ||
![]() |
a6c535414f | ||
![]() |
6ebb7723ff | ||
![]() |
07dd6600dc | ||
![]() |
cc6cfec907 | ||
![]() |
4ecfcfda36 | ||
![]() |
c5681b1376 | ||
![]() |
1fc57018e3 | ||
![]() |
8b8bacdf69 | ||
![]() |
3aaa419f8b | ||
![]() |
94819019ec | ||
![]() |
7b37035f75 | ||
![]() |
a5ef3507c3 | ||
![]() |
b9c6d30678 | ||
![]() |
009556f984 | ||
![]() |
87007d5ae3 | ||
![]() |
61ea2c77c8 | ||
![]() |
c5dbccf807 | ||
![]() |
ab4bf45c10 | ||
![]() |
61853428de | ||
![]() |
ae8c0128cb | ||
![]() |
744e731a22 | ||
![]() |
bb34e21791 | ||
![]() |
74f91b7cb3 | ||
![]() |
7bcf3dbabe | ||
![]() |
273111fb05 | ||
![]() |
8ba067d90e | ||
![]() |
b68f71ec62 | ||
![]() |
7100d12818 | ||
![]() |
f041f0f07a | ||
![]() |
6deb9ab48a | ||
![]() |
1e1c05c138 | ||
![]() |
8cfc20a81c | ||
![]() |
c853c96ae9 | ||
![]() |
85fe9eb4ec | ||
![]() |
cf5af26d6e | ||
![]() |
90e56e7605 | ||
![]() |
1c4e527db6 | ||
![]() |
75a0aadce4 | ||
![]() |
01e3e91e51 | ||
![]() |
7514fa41a1 | ||
![]() |
69115fb77a | ||
![]() |
99ab8dacd4 | ||
![]() |
e30d2cd85b | ||
![]() |
657915b1fe | ||
![]() |
90149def0a | ||
![]() |
1926eca929 | ||
![]() |
f20ba3e8bc | ||
![]() |
6f972ab4cc | ||
![]() |
129bc8a9f1 | ||
![]() |
4673aa498e | ||
![]() |
a2e0db2a16 | ||
![]() |
8def92eb5e | ||
![]() |
5b7e8f73b5 | ||
![]() |
7fa29b4b37 | ||
![]() |
a859baac97 | ||
![]() |
b7a676f668 | ||
![]() |
26d81f10a6 | ||
![]() |
be4cc804a2 | ||
![]() |
1b253ccb0a | ||
![]() |
8bfc1dc302 | ||
![]() |
8c8c49055b |
@@ -433,6 +433,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "al-wi",
|
||||||
|
"name": "Alexander Wiedemann",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/11092199?v=4",
|
||||||
|
"profile": "https://github.com/al-wi",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
@@ -121,3 +121,8 @@ rules:
|
|||||||
'@typescript-eslint/no-unsafe-argument': off
|
'@typescript-eslint/no-unsafe-argument': off
|
||||||
'@typescript-eslint/restrict-plus-operands': off
|
'@typescript-eslint/restrict-plus-operands': off
|
||||||
'@typescript-eslint/space-infix-ops': 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
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Installing Node
|
- name: Installing Node
|
||||||
uses: actions/setup-node@v2.2.0
|
uses: actions/setup-node@v2.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Installing Node
|
- name: Installing Node
|
||||||
uses: actions/setup-node@v2.2.0
|
uses: actions/setup-node@v2.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v2.2.0
|
uses: actions/setup-node@v2.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Installing Node
|
- name: Installing Node
|
||||||
uses: actions/setup-node@v2.2.0
|
uses: actions/setup-node@v2.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Installing Node
|
- name: Installing Node
|
||||||
uses: actions/setup-node@v2.2.0
|
uses: actions/setup-node@v2.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
|
19
.github/workflows/release.yml
vendored
Normal file
19
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: "tagged-release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tagged-release:
|
||||||
|
name: "Tagged Release"
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
|
with:
|
||||||
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
prerelease: false
|
||||||
|
draft: true
|
12
README.md
12
README.md
@@ -7,12 +7,19 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://ko-fi.com/J3J8KWTF">
|
<a href="https://ko-fi.com/J3J8KWTF">
|
||||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg">
|
<img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=2" width="150">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
### Downloads:
|
||||||
|
|
||||||
|
* [Latest release](https://github.com/Eugeny/tabby/releases/latest)
|
||||||
|
* [Nightly build](https://nightly.link/Eugeny/tabby/workflows/build/master)
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
**Tabby** (formerly **Terminus**) is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
**Tabby** (formerly **Terminus**) is a highly configurable terminal emulator, SSH and serial client for Windows, macOS and Linux
|
||||||
|
|
||||||
* Integrated SSH client and connection manager
|
* Integrated SSH client and connection manager
|
||||||
@@ -96,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.
|
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
|
* [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
|
* [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
|
* [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
|
* [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
|
* [sync-config](https://github.com/starxg/terminus-sync-config) - sync the config to Gist or Gitee
|
||||||
|
|
||||||
<a name="themes"></a>
|
<a name="themes"></a>
|
||||||
@@ -187,6 +194,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center"><a href="https://git.io/JnP49"><img src="https://avatars.githubusercontent.com/u/63876444?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Logic Machine</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=logicmachine123" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://git.io/JnP49"><img src="https://avatars.githubusercontent.com/u/63876444?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Logic Machine</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=logicmachine123" title="Documentation">📖</a></td>
|
||||||
<td align="center"><a href="https://github.com/cypherbits"><img src="https://avatars.githubusercontent.com/u/10424900?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cypherbits</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=cypherbits" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/cypherbits"><img src="https://avatars.githubusercontent.com/u/10424900?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cypherbits</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=cypherbits" title="Documentation">📖</a></td>
|
||||||
<td align="center"><a href="https://modulolotus.net"><img src="https://avatars.githubusercontent.com/u/946421?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matthew Davidson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=KingMob" title="Code">💻</a></td>
|
<td align="center"><a href="https://modulolotus.net"><img src="https://avatars.githubusercontent.com/u/946421?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matthew Davidson</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=KingMob" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/al-wi"><img src="https://avatars.githubusercontent.com/u/11092199?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Wiedemann</b></sub></a><br /><a href="https://github.com/Eugeny/tabby/commits?author=al-wi" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import * as promiseIpc from 'electron-promise-ipc'
|
|||||||
import * as remote from '@electron/remote/main'
|
import * as remote from '@electron/remote/main'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import { Subject, throttleTime } from 'rxjs'
|
||||||
|
|
||||||
import { loadConfig } from './config'
|
import { loadConfig } from './config'
|
||||||
import { Window, WindowOptions } from './window'
|
import { Window, WindowOptions } from './window'
|
||||||
@@ -19,6 +20,7 @@ export class Application {
|
|||||||
private tray?: Tray
|
private tray?: Tray
|
||||||
private ptyManager = new PTYManager()
|
private ptyManager = new PTYManager()
|
||||||
private windows: Window[] = []
|
private windows: Window[] = []
|
||||||
|
private globalHotkey$ = new Subject<void>()
|
||||||
userPluginsPath: string
|
userPluginsPath: string
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -33,12 +35,14 @@ export class Application {
|
|||||||
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
|
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
|
||||||
globalShortcut.unregisterAll()
|
globalShortcut.unregisterAll()
|
||||||
for (const spec of specs) {
|
for (const spec of specs) {
|
||||||
globalShortcut.register(spec, () => {
|
globalShortcut.register(spec, () => this.globalHotkey$.next())
|
||||||
this.onGlobalHotkey()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.globalHotkey$.pipe(throttleTime(100)).subscribe(() => {
|
||||||
|
this.onGlobalHotkey()
|
||||||
|
})
|
||||||
|
|
||||||
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
|
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
|
||||||
return pluginManager.install(this.userPluginsPath, name, version)
|
return pluginManager.install(this.userPluginsPath, name, version)
|
||||||
})
|
})
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'v8-compile-cache'
|
||||||
import './portable'
|
import './portable'
|
||||||
import 'source-map-support/register'
|
import 'source-map-support/register'
|
||||||
import './sentry'
|
import './sentry'
|
||||||
@@ -26,7 +27,9 @@ app.on('activate', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
app.quit()
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('uncaughtException' as any, err => {
|
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 { StringDecoder } from './stringDecoder'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as glasstron from 'glasstron'
|
import * as glasstron from 'glasstron'
|
||||||
|
|
||||||
import { Subject, Observable, debounceTime } from 'rxjs'
|
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 ElectronConfig = require('electron-config')
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
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 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 {
|
export class Window {
|
||||||
ready: Promise<void>
|
ready: Promise<void>
|
||||||
private visible = new Subject<boolean>()
|
private visible = new Subject<boolean>()
|
||||||
@@ -39,6 +41,7 @@ export class Window {
|
|||||||
private lastVibrancy: { enabled: boolean, type?: string } | null = null
|
private lastVibrancy: { enabled: boolean, type?: string } | null = null
|
||||||
private disableVibrancyWhileDragging = false
|
private disableVibrancyWhileDragging = false
|
||||||
private configStore: any
|
private configStore: any
|
||||||
|
private touchBarControl: any
|
||||||
|
|
||||||
get visible$ (): Observable<boolean> { return this.visible }
|
get visible$ (): Observable<boolean> { return this.visible }
|
||||||
get closed$ (): Observable<void> { return this.closed }
|
get closed$ (): Observable<void> { return this.closed }
|
||||||
@@ -127,7 +130,15 @@ export class Window {
|
|||||||
this.window.webContents.setVisualZoomLevelLimits(1, 1)
|
this.window.webContents.setVisualZoomLevelLimits(1, 1)
|
||||||
this.window.webContents.setZoomFactor(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)
|
this.window.setMenu(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +368,14 @@ export class Window {
|
|||||||
this.window.close()
|
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())
|
this.window.webContents.on('new-window', event => event.preventDefault())
|
||||||
|
|
||||||
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
"watch": "webpack --progress --color --watch"
|
"watch": "webpack --progress --color --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^12.2.0",
|
||||||
"@electron/remote": "1.2.0",
|
"@electron/remote": "1.2.0",
|
||||||
"any-promise": "^1.3.0",
|
"any-promise": "^1.3.0",
|
||||||
"electron-config": "2.0.0",
|
"electron-config": "2.0.0",
|
||||||
@@ -25,11 +26,12 @@
|
|||||||
"keytar": "^7.7.0",
|
"keytar": "^7.7.0",
|
||||||
"mz": "^2.7.0",
|
"mz": "^2.7.0",
|
||||||
"native-process-working-directory": "^1.0.2",
|
"native-process-working-directory": "^1.0.2",
|
||||||
"node-pty": "^0.10.1",
|
"@tabby-gang/node-pty": "^0.11.0-beta.200",
|
||||||
"npm": "6",
|
"npm": "6",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"yargs": "^17.0.1"
|
"v8-compile-cache": "^2.3.0",
|
||||||
|
"yargs": "^17.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"macos-native-processlist": "^2.0.0",
|
"macos-native-processlist": "^2.0.0",
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'v8-compile-cache'
|
||||||
import '../lib/lru'
|
import '../lib/lru'
|
||||||
import 'source-sans-pro/source-sans-pro.css'
|
import 'source-sans-pro/source-sans-pro.css'
|
||||||
import 'source-code-pro/source-code-pro.css'
|
import 'source-code-pro/source-code-pro.css'
|
||||||
|
@@ -28,7 +28,6 @@ body {
|
|||||||
|
|
||||||
.form-line {
|
.form-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@@ -58,7 +58,7 @@ nodeModule.prototype.require = function (query: string) {
|
|||||||
return originalModuleRequire.call(this, query)
|
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 {
|
export function initModuleLookup (userPluginsPath: string): void {
|
||||||
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
||||||
|
@@ -40,27 +40,14 @@ module.exports = {
|
|||||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||||
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||||
{
|
{
|
||||||
test: /\.(png|svg)$/,
|
test: /\.(png|svg|ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
use: {
|
type: 'asset',
|
||||||
loader: 'url-loader',
|
|
||||||
options: {
|
|
||||||
limit: 999999,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: 'fonts/[name].[ext]',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
'@electron/remote': 'commonjs @electron/remote',
|
'@electron/remote': 'commonjs @electron/remote',
|
||||||
|
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||||
child_process: 'commonjs child_process',
|
child_process: 'commonjs child_process',
|
||||||
electron: 'commonjs electron',
|
electron: 'commonjs electron',
|
||||||
fs: 'commonjs fs',
|
fs: 'commonjs fs',
|
||||||
|
@@ -34,6 +34,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
|
'v8-compile-cache': 'commonjs v8-compile-cache',
|
||||||
'any-promise': 'commonjs any-promise',
|
'any-promise': 'commonjs any-promise',
|
||||||
electron: 'commonjs electron',
|
electron: 'commonjs electron',
|
||||||
'electron-config': 'commonjs electron-config',
|
'electron-config': 'commonjs electron-config',
|
||||||
@@ -43,7 +44,8 @@ module.exports = {
|
|||||||
glasstron: 'commonjs glasstron',
|
glasstron: 'commonjs glasstron',
|
||||||
mz: 'commonjs mz',
|
mz: 'commonjs mz',
|
||||||
npm: 'commonjs npm',
|
npm: 'commonjs npm',
|
||||||
'node-pty': 'commonjs node-pty',
|
'node:os': 'commonjs os',
|
||||||
|
'@tabby-gang/node-pty': 'commonjs @tabby-gang/node-pty',
|
||||||
path: 'commonjs path',
|
path: 'commonjs path',
|
||||||
util: 'commonjs util',
|
util: 'commonjs util',
|
||||||
'source-map-support': 'commonjs source-map-support',
|
'source-map-support': 'commonjs source-map-support',
|
||||||
|
@@ -2,6 +2,15 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@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:
|
||||||
|
parse5 "^5.0.0"
|
||||||
|
|
||||||
"@electron/remote@1.2.0":
|
"@electron/remote@1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
||||||
@@ -87,6 +96,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.3.1"
|
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":
|
"@types/mz@2.7.4":
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
|
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-2.7.4.tgz#f9d1535cb5171199b28ae6abd6ec29e856551401"
|
||||||
@@ -2086,13 +2102,6 @@ node-gyp@^5.0.2, node-gyp@^5.1.0:
|
|||||||
tar "^4.4.12"
|
tar "^4.4.12"
|
||||||
which "^1.3.1"
|
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:
|
noop-logger@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz"
|
||||||
@@ -2558,6 +2567,11 @@ parse-json@^2.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
error-ex "^1.2.0"
|
error-ex "^1.2.0"
|
||||||
|
|
||||||
|
parse5@^5.0.0:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||||
|
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||||
|
|
||||||
path-exists@^3.0.0:
|
path-exists@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
||||||
@@ -3394,10 +3408,10 @@ tough-cookie@~2.5.0:
|
|||||||
psl "^1.1.28"
|
psl "^1.1.28"
|
||||||
punycode "^2.1.1"
|
punycode "^2.1.1"
|
||||||
|
|
||||||
tslib@^2.0.0:
|
tslib@^2.0.0, tslib@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||||
|
|
||||||
tslib@~2.1.0:
|
tslib@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
@@ -3519,6 +3533,11 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3:
|
|||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
|
v8-compile-cache@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
|
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||||
|
|
||||||
validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
|
validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"
|
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"
|
||||||
@@ -3726,10 +3745,10 @@ yargs@^14.2.3:
|
|||||||
y18n "^4.0.0"
|
y18n "^4.0.0"
|
||||||
yargs-parser "^15.0.1"
|
yargs-parser "^15.0.1"
|
||||||
|
|
||||||
yargs@^17.0.1:
|
yargs@^17.1.0:
|
||||||
version "17.0.1"
|
version "17.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf"
|
||||||
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
|
integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg==
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui "^7.0.2"
|
cliui "^7.0.2"
|
||||||
escalade "^3.1.1"
|
escalade "^3.1.1"
|
||||||
|
37
package.json
37
package.json
@@ -7,9 +7,9 @@
|
|||||||
"@angular/forms": "^12.0.0",
|
"@angular/forms": "^12.0.0",
|
||||||
"@angular/platform-browser": "^12.0.0",
|
"@angular/platform-browser": "^12.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^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",
|
"@ng-bootstrap/ng-bootstrap": "^10.0.0",
|
||||||
"@sentry/cli": "^1.67.1",
|
"@sentry/cli": "^1.67.2",
|
||||||
"@sentry/electron": "^2.5.1",
|
"@sentry/electron": "^2.5.1",
|
||||||
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
"@tabby-gang/to-string-loader": "^1.1.7-beta.2",
|
||||||
"@types/electron-config": "^3.2.2",
|
"@types/electron-config": "^3.2.2",
|
||||||
@@ -19,34 +19,34 @@
|
|||||||
"@types/node": "16.0.1",
|
"@types/node": "16.0.1",
|
||||||
"@types/sortablejs": "^1.10.7",
|
"@types/sortablejs": "^1.10.7",
|
||||||
"@types/webpack-env": "^1.16.2",
|
"@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.3",
|
"@typescript-eslint/parser": "^4.28.5",
|
||||||
"apply-loader": "2.0.0",
|
"apply-loader": "2.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"clone-deep": "^4.0.1",
|
"clone-deep": "^4.0.1",
|
||||||
"compare-versions": "^3.6.0",
|
"compare-versions": "^3.6.0",
|
||||||
"core-js": "^3.15.2",
|
"core-js": "^3.15.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "5.2.6",
|
"css-loader": "^6.2.0",
|
||||||
"electron": "13.1.7",
|
"electron": "13.1.9",
|
||||||
"electron-builder": "22.10.5",
|
"electron-builder": "22.10.5",
|
||||||
"electron-download": "^4.1.1",
|
"electron-download": "^4.1.1",
|
||||||
"electron-installer-snap": "^5.1.0",
|
"electron-installer-snap": "^5.1.0",
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.1",
|
||||||
"electron-rebuild": "^2.3.5",
|
"electron-rebuild": "^3.1.1",
|
||||||
"eslint": "^7.30.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-import": "^2.23.4",
|
"eslint-plugin-import": "^2.23.4",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"graceful-fs": "^4.2.6",
|
"graceful-fs": "^4.2.8",
|
||||||
"html-loader": "2.1.2",
|
"html-loader": "2.1.2",
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"lru-cache": "^6.0.0",
|
"lru-cache": "^6.0.0",
|
||||||
"macos-release": "^2.5.0",
|
"macos-release": "^3.0.0",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-toastr": "^14.0.0",
|
"ngx-toastr": "^14.0.0",
|
||||||
"node-abi": "^2.30.0",
|
"node-abi": "^2.30.0",
|
||||||
"node-sass": "^6.0.1",
|
"node-sass": "^6.0.1",
|
||||||
"npmlog": "4.1.2",
|
"npmlog": "5.0.0",
|
||||||
"npx": "^10.2.2",
|
"npx": "^10.2.2",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
@@ -58,21 +58,20 @@
|
|||||||
"sass-loader": "^12.1.0",
|
"sass-loader": "^12.1.0",
|
||||||
"shell-quote": "^1.7.2",
|
"shell-quote": "^1.7.2",
|
||||||
"shelljs": "0.8.4",
|
"shelljs": "0.8.4",
|
||||||
"slugify": "^1.5.3",
|
"slugify": "^1.6.0",
|
||||||
"sortablejs": "^1.14.0",
|
"sortablejs": "^1.14.0",
|
||||||
"source-code-pro": "^2.38.0",
|
"source-code-pro": "^2.38.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-sans-pro": "3.6.0",
|
"source-sans-pro": "3.6.0",
|
||||||
"ssh2": "^1.1.0",
|
"ssh2": "^1.2.0",
|
||||||
"style-loader": "^3.1.0",
|
"style-loader": "^3.2.1",
|
||||||
"svg-inline-loader": "^0.8.2",
|
"svg-inline-loader": "^0.8.2",
|
||||||
"ts-loader": "^9.2.3",
|
"ts-loader": "^9.2.3",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"typedoc": "^0.21.4",
|
"typedoc": "^0.21.5",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"val-loader": "4.0.0",
|
"val-loader": "4.0.0",
|
||||||
"webpack": "^5.43.0",
|
"webpack": "^5.50.0",
|
||||||
"webpack-bundle-analyzer": "^4.4.2",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^4.7.0",
|
"webpack-cli": "^4.7.0",
|
||||||
"yaml-loader": "0.6.0",
|
"yaml-loader": "0.6.0",
|
||||||
@@ -90,7 +89,7 @@
|
|||||||
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
|
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
|
||||||
"start:prod": "electron app --debug",
|
"start:prod": "electron app --debug",
|
||||||
"prod": "cross-env TABBY_DEV=1 electron app",
|
"prod": "cross-env TABBY_DEV=1 electron app",
|
||||||
"docs": "typedoc --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
|
"docs": "typedoc --emit --out docs/api --tsconfig tabby-core/src/tsconfig.typings.json tabby-core/src/index.ts && typedoc --emit --out docs/api/terminal --tsconfig tabby-terminal/tsconfig.typings.json tabby-terminal/src/index.ts && typedoc --emit --out docs/api/local --tsconfig tabby-local/tsconfig.typings.json tabby-local/src/index.ts && typedoc --emit --out docs/api/settings --tsconfig tabby-settings/tsconfig.typings.json tabby-settings/src/index.ts",
|
||||||
"lint": "eslint --ext ts */src */lib",
|
"lint": "eslint --ext ts */src */lib",
|
||||||
"postinstall": "node ./scripts/install-deps.js",
|
"postinstall": "node ./scripts/install-deps.js",
|
||||||
"patch": "patch-package; cd web; patch-package"
|
"patch": "patch-package; cd web; patch-package"
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
diff --git a/node_modules/ssh2/lib/protocol/Protocol.js b/node_modules/ssh2/lib/protocol/Protocol.js
|
|
||||||
index b4d1ee0..1e3ac66 100644
|
|
||||||
--- a/node_modules/ssh2/lib/protocol/Protocol.js
|
|
||||||
+++ b/node_modules/ssh2/lib/protocol/Protocol.js
|
|
||||||
@@ -254,8 +254,8 @@ class Protocol {
|
|
||||||
);
|
|
||||||
if (greeting)
|
|
||||||
this._onWrite(greeting);
|
|
||||||
- this._onWrite(this._identRaw);
|
|
||||||
- this._onWrite(CRLF);
|
|
||||||
+ this._onWrite(Buffer.concat([this._identRaw, CRLF]));
|
|
||||||
+ // this._onWrite(CRLF);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_destruct(reason) {
|
|
@@ -13,6 +13,10 @@ const configs = [
|
|||||||
;(async () => {
|
;(async () => {
|
||||||
for (const c of configs) {
|
for (const c of configs) {
|
||||||
log.info('build', c)
|
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.exec(`${npx} yarn install --force`)
|
||||||
sh.cd('..')
|
sh.cd('..')
|
||||||
|
|
||||||
vars.builtinPlugins.forEach(plugin => {
|
vars.allPackages.forEach(plugin => {
|
||||||
log.info('deps', plugin)
|
log.info('deps', plugin)
|
||||||
sh.cd(plugin)
|
sh.cd(plugin)
|
||||||
sh.exec(`${npx} yarn install --force`)
|
sh.exec(`${npx} yarn install --force`)
|
||||||
|
@@ -9,7 +9,7 @@ sh.exec(`${sentryCli} releases new ${vars.version}`)
|
|||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
for (const path of [
|
for (const path of [
|
||||||
'app/node_modules/@serialport/bindings/build/Release/bindings.node',
|
'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/fontmanager-redux/build/Release/fontmanager.node',
|
||||||
'app/node_modules/macos-native-processlist/build/Release/native.node',
|
'app/node_modules/macos-native-processlist/build/Release/native.node',
|
||||||
]) {
|
]) {
|
||||||
|
@@ -17,14 +17,14 @@ exports.builtinPlugins = [
|
|||||||
'tabby-core',
|
'tabby-core',
|
||||||
'tabby-settings',
|
'tabby-settings',
|
||||||
'tabby-terminal',
|
'tabby-terminal',
|
||||||
'tabby-electron',
|
|
||||||
'tabby-local',
|
|
||||||
'tabby-web',
|
'tabby-web',
|
||||||
'tabby-community-color-schemes',
|
'tabby-community-color-schemes',
|
||||||
'tabby-plugin-manager',
|
|
||||||
'tabby-ssh',
|
'tabby-ssh',
|
||||||
'tabby-serial',
|
'tabby-serial',
|
||||||
'tabby-telnet',
|
'tabby-telnet',
|
||||||
|
'tabby-electron',
|
||||||
|
'tabby-local',
|
||||||
|
'tabby-plugin-manager',
|
||||||
]
|
]
|
||||||
|
|
||||||
exports.allPackages = [
|
exports.allPackages = [
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-community-color-schemes",
|
"name": "tabby-community-color-schemes",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Community color schemes for Tabby",
|
"description": "Community color schemes for Tabby",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-core",
|
"name": "tabby-core",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Tabby core",
|
"description": "Tabby core",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
@@ -19,13 +19,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^4.0.0",
|
"@types/js-yaml": "^4.0.0",
|
||||||
"bootstrap": "^4.1.3",
|
"bootstrap": "^4.1.3",
|
||||||
"core-js": "^3.1.2",
|
|
||||||
"deep-equal": "^2.0.5",
|
"deep-equal": "^2.0.5",
|
||||||
"deepmerge": "^4.1.1",
|
"deepmerge": "^4.1.1",
|
||||||
"electron-updater": "^4.0.6",
|
"electron-updater": "^4.0.6",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
"mixpanel": "^0.13.0",
|
"mixpanel": "^0.13.0",
|
||||||
"ng2-dnd": "^5.0.2",
|
|
||||||
"ngx-filesize": "^2.0.16",
|
"ngx-filesize": "^2.0.16",
|
||||||
"ngx-perfect-scrollbar": "^10.1.0",
|
"ngx-perfect-scrollbar": "^10.1.0",
|
||||||
"readable-stream": "3.6.0",
|
"readable-stream": "3.6.0",
|
||||||
|
@@ -25,6 +25,7 @@ export { DockingService, Screen } from '../services/docking.service'
|
|||||||
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
|
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
|
||||||
export { HomeBaseService } from '../services/homeBase.service'
|
export { HomeBaseService } from '../services/homeBase.service'
|
||||||
export { HotkeysService } from '../services/hotkeys.service'
|
export { HotkeysService } from '../services/hotkeys.service'
|
||||||
|
export { KeyEventData, KeyName, Keystroke } from '../services/hotkeys.util'
|
||||||
export { NotificationsService } from '../services/notifications.service'
|
export { NotificationsService } from '../services/notifications.service'
|
||||||
export { ThemesService } from '../services/themes.service'
|
export { ThemesService } from '../services/themes.service'
|
||||||
export { ProfilesService } from '../services/profiles.service'
|
export { ProfilesService } from '../services/profiles.service'
|
||||||
|
@@ -13,6 +13,7 @@ export interface MessageBoxOptions {
|
|||||||
detail?: string
|
detail?: string
|
||||||
buttons: string[]
|
buttons: string[]
|
||||||
defaultId?: number
|
defaultId?: number
|
||||||
|
cancelId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageBoxResult {
|
export interface MessageBoxResult {
|
||||||
|
@@ -46,6 +46,10 @@ export abstract class ProfileProvider<P extends Profile> {
|
|||||||
|
|
||||||
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
|
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
|
||||||
|
|
||||||
|
getSuggestedName (profile: PartialProfile<P>): string|null {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
abstract getDescription (profile: PartialProfile<P>): string
|
abstract getDescription (profile: PartialProfile<P>): string
|
||||||
|
|
||||||
quickConnect (query: string): PartialProfile<P>|null {
|
quickConnect (query: string): PartialProfile<P>|null {
|
||||||
|
@@ -4,5 +4,6 @@ export interface SelectorOption<T> {
|
|||||||
result?: T
|
result?: T
|
||||||
icon?: string
|
icon?: string
|
||||||
freeInputPattern?: string
|
freeInputPattern?: string
|
||||||
|
color?: string
|
||||||
callback?: (string?) => void
|
callback?: (string?) => void
|
||||||
}
|
}
|
||||||
|
@@ -36,10 +36,12 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
await this.profilesService.openNewTabForProfile(profile)
|
await this.profilesService.openNewTabForProfile(profile)
|
||||||
|
|
||||||
let recentProfiles = this.config.store.recentProfiles
|
let recentProfiles = this.config.store.recentProfiles
|
||||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||||
recentProfiles.unshift(profile)
|
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||||
if (recentProfiles.length > 5) {
|
recentProfiles.unshift(profile)
|
||||||
recentProfiles.pop()
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
} else {
|
||||||
|
recentProfiles = []
|
||||||
}
|
}
|
||||||
this.config.store.recentProfiles = recentProfiles
|
this.config.store.recentProfiles = recentProfiles
|
||||||
this.config.save()
|
this.config.save()
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
title-bar(
|
title-bar(
|
||||||
*ngIf='ready && !hostWindow.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
*ngIf='ready && !hostWindow.isFullscreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
|
||||||
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullScreen'
|
[class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullscreen'
|
||||||
)
|
)
|
||||||
|
|
||||||
.content(
|
.content(
|
||||||
@@ -10,19 +10,17 @@ title-bar(
|
|||||||
)
|
)
|
||||||
.tab-bar
|
.tab-bar
|
||||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
|
.inset.background(*ngIf='hostApp.platform == Platform.macOS \
|
||||||
&& !hostWindow.isFullScreen \
|
&& !hostWindow.isFullscreen \
|
||||||
&& config.store.appearance.frame == "thin" \
|
&& config.store.appearance.frame == "thin" \
|
||||||
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||||
.tabs(
|
.tabs(
|
||||||
dnd-sortable-container,
|
cdkDropList,
|
||||||
[sortableData]='app.tabs',
|
[cdkDropListOrientation]='(config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "bottom") ? "horizontal" : "vertical"',
|
||||||
|
(cdkDropListDropped)='onTabsReordered($event)',
|
||||||
|
cdkAutoDropGroup='app-tabs'
|
||||||
)
|
)
|
||||||
tab-header(
|
tab-header(
|
||||||
*ngFor='let tab of app.tabs; let idx = index',
|
*ngFor='let tab of app.tabs; let idx = index',
|
||||||
dnd-sortable,
|
|
||||||
[sortableIndex]='idx',
|
|
||||||
(onDragStart)='onTabDragStart()',
|
|
||||||
(onDragEnd)='onTabDragEnd()',
|
|
||||||
[index]='idx',
|
[index]='idx',
|
||||||
[tab]='tab',
|
[tab]='tab',
|
||||||
[active]='tab == app.activeTab',
|
[active]='tab == app.activeTab',
|
||||||
@@ -30,7 +28,7 @@ title-bar(
|
|||||||
[@.disabled]='hasVerticalTabs()',
|
[@.disabled]='hasVerticalTabs()',
|
||||||
(click)='app.selectTab(tab)',
|
(click)='app.selectTab(tab)',
|
||||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||||
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
|
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
|
||||||
)
|
)
|
||||||
|
|
||||||
.btn-group.background
|
.btn-group.background
|
||||||
@@ -109,6 +107,7 @@ title-bar(
|
|||||||
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
||||||
|
|
||||||
tab-body.content-tab(
|
tab-body.content-tab(
|
||||||
|
#tabBodies,
|
||||||
*ngFor='let tab of unsortedTabs',
|
*ngFor='let tab of unsortedTabs',
|
||||||
[class.content-tab-active]='tab == app.activeTab',
|
[class.content-tab-active]='tab == app.activeTab',
|
||||||
[active]='tab == app.activeTab',
|
[active]='tab == app.activeTab',
|
||||||
|
@@ -132,6 +132,14 @@ $side-tab-width: 200px;
|
|||||||
window-controls {
|
window-controls {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drop-list-dragging tab-header:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
import { Component, Inject, Input, HostListener, HostBinding, ViewChildren } from '@angular/core'
|
||||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
import { HostAppService, Platform } from '../api/hostApp'
|
import { HostAppService, Platform } from '../api/hostApp'
|
||||||
import { HotkeysService } from '../services/hotkeys.service'
|
import { HotkeysService } from '../services/hotkeys.service'
|
||||||
@@ -12,6 +13,7 @@ import { UpdaterService } from '../services/updater.service'
|
|||||||
|
|
||||||
import { BaseTabComponent } from './baseTab.component'
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||||
|
import { TabBodyComponent } from './tabBody.component'
|
||||||
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -57,7 +59,7 @@ export class AppRootComponent {
|
|||||||
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
||||||
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
||||||
@HostBinding('class.no-tabs') noTabs = true
|
@HostBinding('class.no-tabs') noTabs = true
|
||||||
tabsDragging = false
|
@ViewChildren(TabBodyComponent) tabBodies: TabBodyComponent[]
|
||||||
unsortedTabs: BaseTabComponent[] = []
|
unsortedTabs: BaseTabComponent[] = []
|
||||||
updatesAvailable = false
|
updatesAvailable = false
|
||||||
activeTransfers: FileTransfer[] = []
|
activeTransfers: FileTransfer[] = []
|
||||||
@@ -126,11 +128,18 @@ export class AppRootComponent {
|
|||||||
this.app.tabOpened$.subscribe(tab => {
|
this.app.tabOpened$.subscribe(tab => {
|
||||||
this.unsortedTabs.push(tab)
|
this.unsortedTabs.push(tab)
|
||||||
this.noTabs = false
|
this.noTabs = false
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.tabClosed$.subscribe(tab => {
|
this.app.tabRemoved$.subscribe(tab => {
|
||||||
|
for (const tabBody of this.tabBodies) {
|
||||||
|
if (tabBody.tab === tab) {
|
||||||
|
tabBody.detach()
|
||||||
|
}
|
||||||
|
}
|
||||||
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
||||||
this.noTabs = app.tabs.length === 0
|
this.noTabs = app.tabs.length === 0
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.fileTransferStarted$.subscribe(transfer => {
|
platform.fileTransferStarted$.subscribe(transfer => {
|
||||||
@@ -173,17 +182,6 @@ export class AppRootComponent {
|
|||||||
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
|
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabDragStart () {
|
|
||||||
this.tabsDragging = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onTabDragEnd () {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.tabsDragging = false
|
|
||||||
this.app.emitTabsChanged()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateButtonSubmenu (button: ToolbarButton) {
|
async generateButtonSubmenu (button: ToolbarButton) {
|
||||||
if (button.submenu) {
|
if (button.submenu) {
|
||||||
button.submenuItems = await button.submenu()
|
button.submenuItems = await button.submenu()
|
||||||
@@ -194,6 +192,11 @@ export class AppRootComponent {
|
|||||||
return submenuItems.some(x => !!x.icon)
|
return submenuItems.some(x => !!x.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTabsReordered (event: CdkDragDrop<BaseTabComponent[]>) {
|
||||||
|
moveItemInArray(this.app.tabs, event.previousIndex, event.currentIndex)
|
||||||
|
this.app.emitTabsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
||||||
let buttons: ToolbarButton[] = []
|
let buttons: ToolbarButton[] = []
|
||||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { ViewRef } from '@angular/core'
|
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
||||||
import { RecoveryToken } from '../api/tabRecovery'
|
import { RecoveryToken } from '../api/tabRecovery'
|
||||||
import { BaseComponent } from './base.component'
|
import { BaseComponent } from './base.component'
|
||||||
|
|
||||||
@@ -52,6 +52,10 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
* your tab state to be saved sooner
|
* your tab state to be saved sooner
|
||||||
*/
|
*/
|
||||||
protected recoveryStateChangedHint = new Subject<void>()
|
protected recoveryStateChangedHint = new Subject<void>()
|
||||||
|
protected viewContainer?: ViewContainerRef
|
||||||
|
|
||||||
|
/* @hidden */
|
||||||
|
viewContainerEmbeddedRef?: EmbeddedViewRef<any>
|
||||||
|
|
||||||
private progressClearTimeout: number
|
private progressClearTimeout: number
|
||||||
private titleChange = new Subject<string>()
|
private titleChange = new Subject<string>()
|
||||||
@@ -61,6 +65,8 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
private activity = new Subject<boolean>()
|
private activity = new Subject<boolean>()
|
||||||
private destroyed = new Subject<void>()
|
private destroyed = new Subject<void>()
|
||||||
|
|
||||||
|
private _destroyCalled = false
|
||||||
|
|
||||||
get focused$ (): Observable<void> { return this.focused }
|
get focused$ (): Observable<void> { return this.focused }
|
||||||
get blurred$ (): Observable<void> { return this.blurred }
|
get blurred$ (): Observable<void> { return this.blurred }
|
||||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||||
@@ -152,10 +158,29 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
this.blurred.next()
|
this.blurred.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
|
||||||
|
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
|
||||||
|
this.viewContainer = container
|
||||||
|
return this.viewContainerEmbeddedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromContainer (): void {
|
||||||
|
if (!this.viewContainer || !this.viewContainerEmbeddedRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.viewContainer.detach(this.viewContainer.indexOf(this.viewContainerEmbeddedRef))
|
||||||
|
this.viewContainerEmbeddedRef = undefined
|
||||||
|
this.viewContainer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before the tab is closed
|
* Called before the tab is closed
|
||||||
*/
|
*/
|
||||||
destroy (skipDestroyedEvent = false): void {
|
destroy (skipDestroyedEvent = false): void {
|
||||||
|
if (this._destroyCalled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._destroyCalled = true
|
||||||
this.focused.complete()
|
this.focused.complete()
|
||||||
this.blurred.complete()
|
this.blurred.complete()
|
||||||
this.titleChange.complete()
|
this.titleChange.complete()
|
||||||
@@ -166,6 +191,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||||||
this.destroyed.next()
|
this.destroyed.next()
|
||||||
}
|
}
|
||||||
this.destroyed.complete()
|
this.destroyed.complete()
|
||||||
|
this.hostView.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
.modal-body
|
.modal-body
|
||||||
input.form-control(
|
input.form-control.form-control-lg(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='filter',
|
[(ngModel)]='filter',
|
||||||
autofocus,
|
autofocus,
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
(ngModelChange)='onFilterChange()'
|
(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(
|
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
#item,
|
#item,
|
||||||
(click)='selectOption(option)',
|
(click)='selectOption(option)',
|
||||||
@@ -16,11 +16,13 @@
|
|||||||
)
|
)
|
||||||
i.icon(
|
i.icon(
|
||||||
class='fa-fw {{option.icon}}',
|
class='fa-fw {{option.icon}}',
|
||||||
|
style='color: {{option.color}}',
|
||||||
*ngIf='!iconIsSVG(option.icon)'
|
*ngIf='!iconIsSVG(option.icon)'
|
||||||
)
|
)
|
||||||
.icon(
|
.icon(
|
||||||
[fastHtmlBind]='option.icon',
|
[fastHtmlBind]='option.icon',
|
||||||
|
style='color: {{option.color}}',
|
||||||
*ngIf='iconIsSVG(option.icon)'
|
*ngIf='iconIsSVG(option.icon)'
|
||||||
)
|
)
|
||||||
.mr-2.title {{getOptionText(option)}}
|
.title.mr-2 {{getOptionText(option)}}
|
||||||
.text-muted {{option.description}}
|
.description.no-wrap.text-muted {{option.description}}
|
||||||
|
@@ -16,9 +16,13 @@
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
border-bottom-left-radius: 0;
|
border-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
}
|
||||||
|
@@ -50,8 +50,9 @@ export class SelectorModalComponent<T> {
|
|||||||
if (!f) {
|
if (!f) {
|
||||||
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
|
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
|
||||||
} else {
|
} else {
|
||||||
|
const terms = f.split(' ')
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
this.filteredOptions = this.options.filter(x => x.freeInputPattern ?? (x.name + (x.description ?? '')).toLowerCase().includes(f))
|
this.filteredOptions = this.options.filter(x => x.freeInputPattern ?? terms.every(term => (x.name + (x.description ?? '')).toLowerCase().includes(term)))
|
||||||
}
|
}
|
||||||
this.selectedIndex = Math.max(0, this.selectedIndex)
|
this.selectedIndex = Math.max(0, this.selectedIndex)
|
||||||
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)
|
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)
|
||||||
|
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { HostBinding, ElementRef } from '@angular/core'
|
||||||
|
import { BaseComponent } from './base.component'
|
||||||
|
|
||||||
|
export abstract class SelfPositioningComponent extends BaseComponent {
|
||||||
|
@HostBinding('style.left') cssLeft: string
|
||||||
|
@HostBinding('style.top') cssTop: string
|
||||||
|
@HostBinding('style.width') cssWidth: string | null
|
||||||
|
@HostBinding('style.height') cssHeight: string | null
|
||||||
|
|
||||||
|
constructor (protected element: ElementRef) { super() }
|
||||||
|
|
||||||
|
protected setDimensions (x: number, y: number, w: number, h: number, unit = '%'): void {
|
||||||
|
this.cssLeft = `${x}${unit}`
|
||||||
|
this.cssTop = `${y}${unit}`
|
||||||
|
this.cssWidth = w ? `${w}${unit}` : null
|
||||||
|
this.cssHeight = h ? `${h}${unit}` : null
|
||||||
|
}
|
||||||
|
}
|
@@ -6,8 +6,8 @@ import { TabsService, NewTabParameters } from '../services/tabs.service'
|
|||||||
import { HotkeysService } from '../services/hotkeys.service'
|
import { HotkeysService } from '../services/hotkeys.service'
|
||||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||||
|
|
||||||
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
|
export type SplitOrientation = 'v' | 'h'
|
||||||
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
|
export type SplitDirection = 'r' | 't' | 'b' | 'l'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes a horizontal or vertical split row or column
|
* Describes a horizontal or vertical split row or column
|
||||||
@@ -93,13 +93,13 @@ export class SplitContainer {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
async serialize (): Promise<RecoveryToken> {
|
async serialize (tabsRecovery: TabRecoveryService): Promise<RecoveryToken> {
|
||||||
const children: any[] = []
|
const children: any[] = []
|
||||||
for (const child of this.children) {
|
for (const child of this.children) {
|
||||||
if (child instanceof SplitContainer) {
|
if (child instanceof SplitContainer) {
|
||||||
children.push(await child.serialize())
|
children.push(await child.serialize(tabsRecovery))
|
||||||
} else {
|
} else {
|
||||||
children.push(await child.getRecoveryToken())
|
children.push(await tabsRecovery.getFullRecoveryToken(child))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -123,6 +123,25 @@ export interface SplitSpannerInfo {
|
|||||||
index: number
|
index: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a tab drop zone
|
||||||
|
*/
|
||||||
|
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
|
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||||
* You'll mainly encounter it inside [[AppService]].tabs
|
* You'll mainly encounter it inside [[AppService]].tabs
|
||||||
@@ -137,6 +156,21 @@ export interface SplitSpannerInfo {
|
|||||||
[index]='spanner.index'
|
[index]='spanner.index'
|
||||||
(change)='onSpannerAdjusted(spanner)'
|
(change)='onSpannerAdjusted(spanner)'
|
||||||
></split-tab-spanner>
|
></split-tab-spanner>
|
||||||
|
<split-tab-drop-zone
|
||||||
|
*ngFor='let dropZone of _dropZones'
|
||||||
|
[parent]='this'
|
||||||
|
[dropZone]='dropZone'
|
||||||
|
(tabDropped)='onTabDropped($event, dropZone)'
|
||||||
|
>
|
||||||
|
</split-tab-drop-zone>
|
||||||
|
<split-tab-pane-label
|
||||||
|
*ngFor='let tab of getAllTabs()'
|
||||||
|
cdkDropList
|
||||||
|
cdkAutoDropGroup='app-tabs'
|
||||||
|
[tab]='tab'
|
||||||
|
[parent]='this'
|
||||||
|
>
|
||||||
|
</split-tab-pane-label>
|
||||||
`,
|
`,
|
||||||
styles: [require('./splitTab.component.scss')],
|
styles: [require('./splitTab.component.scss')],
|
||||||
})
|
})
|
||||||
@@ -157,6 +191,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
/** @hidden */
|
/** @hidden */
|
||||||
_spanners: SplitSpannerInfo[] = []
|
_spanners: SplitSpannerInfo[] = []
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
_dropZones: SplitDropZoneInfo[] = []
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
_allFocusMode = false
|
_allFocusMode = false
|
||||||
|
|
||||||
@@ -166,12 +203,19 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||||
|
|
||||||
private tabAdded = new Subject<BaseTabComponent>()
|
private tabAdded = new Subject<BaseTabComponent>()
|
||||||
|
private tabAdopted = new Subject<BaseTabComponent>()
|
||||||
private tabRemoved = new Subject<BaseTabComponent>()
|
private tabRemoved = new Subject<BaseTabComponent>()
|
||||||
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||||
private focusChanged = new Subject<BaseTabComponent>()
|
private focusChanged = new Subject<BaseTabComponent>()
|
||||||
private initialized = new Subject<void>()
|
private initialized = new Subject<void>()
|
||||||
|
|
||||||
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when an existing top-level tab is dragged into this tab
|
||||||
|
*/
|
||||||
|
get tabAdopted$ (): Observable<BaseTabComponent> { return this.tabAdopted }
|
||||||
|
|
||||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,46 +374,77 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||||
|
return this.add(tab, relative, side)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a new `tab` to the `side` of the `relative` tab
|
* Inserts a new `tab` to the `side` of the `relative` tab
|
||||||
*/
|
*/
|
||||||
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|SplitContainer|null, side: SplitDirection): Promise<void> {
|
||||||
tab.parent = this
|
if (thing instanceof SplitTabComponent) {
|
||||||
|
const tab = thing
|
||||||
|
thing = tab.root
|
||||||
|
tab.root = new SplitContainer()
|
||||||
|
for (const child of thing.getAllTabs()) {
|
||||||
|
child.removeFromContainer()
|
||||||
|
}
|
||||||
|
tab.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
|
if (thing instanceof BaseTabComponent) {
|
||||||
let insertIndex = relative ? target.children.indexOf(relative) : -1
|
if (thing.parent instanceof SplitTabComponent) {
|
||||||
|
thing.parent.removeTab(thing)
|
||||||
|
}
|
||||||
|
thing.removeFromContainer()
|
||||||
|
thing.parent = this
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = relative ? this.getParentOf(relative) : null
|
||||||
|
if (!target) {
|
||||||
|
// Rewrap the root container just in case the orientation isn't compatibile
|
||||||
|
target = new SplitContainer()
|
||||||
|
target.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
|
||||||
|
target.children = [this.root]
|
||||||
|
target.ratios = [1]
|
||||||
|
this.root = target
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertIndex = relative
|
||||||
|
? target.children.indexOf(relative) + ('tl'.includes(side) ? 0 : 1)
|
||||||
|
: 'tl'.includes(side) ? 0 : -1
|
||||||
|
|
||||||
if (
|
if (
|
||||||
target.orientation === 'v' && ['l', 'r'].includes(side) ||
|
target.orientation === 'v' && ['l', 'r'].includes(side) ||
|
||||||
target.orientation === 'h' && ['t', 'b'].includes(side)
|
target.orientation === 'h' && ['t', 'b'].includes(side)
|
||||||
) {
|
) {
|
||||||
|
// Inserting into a container but the orientation isn't compatible
|
||||||
const newContainer = new SplitContainer()
|
const newContainer = new SplitContainer()
|
||||||
newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
|
newContainer.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
|
||||||
newContainer.children = relative ? [relative] : []
|
newContainer.children = relative ? [relative] : []
|
||||||
newContainer.ratios = [1]
|
newContainer.ratios = [1]
|
||||||
target.children[insertIndex] = newContainer
|
target.children.splice(relative ? target.children.indexOf(relative) : -1, 1, newContainer)
|
||||||
target = newContainer
|
target = newContainer
|
||||||
insertIndex = 0
|
insertIndex = 'tl'.includes(side) ? 0 : 1
|
||||||
}
|
|
||||||
|
|
||||||
if (insertIndex === -1) {
|
|
||||||
insertIndex = 0
|
|
||||||
} else {
|
|
||||||
insertIndex += side === 'l' || side === 't' ? 0 : 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < target.children.length; i++) {
|
for (let i = 0; i < target.children.length; i++) {
|
||||||
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||||
}
|
}
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
insertIndex = target.ratios.length
|
||||||
|
}
|
||||||
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
||||||
target.children.splice(insertIndex, 0, tab)
|
target.children.splice(insertIndex, 0, thing)
|
||||||
|
|
||||||
this.recoveryStateChangedHint.next()
|
this.recoveryStateChangedHint.next()
|
||||||
|
|
||||||
await this.initialized$.toPromise()
|
await this.initialized$.toPromise()
|
||||||
|
|
||||||
this.attachTabView(tab)
|
for (const tab of thing instanceof SplitContainer ? thing.getAllTabs() : [thing]) {
|
||||||
this.onAfterTabAdded(tab)
|
this.attachTabView(tab)
|
||||||
|
this.onAfterTabAdded(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTab (tab: BaseTabComponent): void {
|
removeTab (tab: BaseTabComponent): void {
|
||||||
@@ -381,8 +456,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
parent.ratios.splice(index, 1)
|
parent.ratios.splice(index, 1)
|
||||||
parent.children.splice(index, 1)
|
parent.children.splice(index, 1)
|
||||||
|
|
||||||
this.detachTabView(tab)
|
tab.removeFromContainer()
|
||||||
tab.parent = null
|
tab.parent = null
|
||||||
|
this.viewRefs.delete(tab)
|
||||||
|
|
||||||
this.layout()
|
this.layout()
|
||||||
|
|
||||||
@@ -401,7 +477,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
}
|
}
|
||||||
const position = parent.children.indexOf(tab)
|
const position = parent.children.indexOf(tab)
|
||||||
parent.children[position] = newTab
|
parent.children[position] = newTab
|
||||||
this.detachTabView(tab)
|
tab.removeFromContainer()
|
||||||
this.attachTabView(newTab)
|
this.attachTabView(newTab)
|
||||||
tab.parent = null
|
tab.parent = null
|
||||||
newTab.parent = this
|
newTab.parent = this
|
||||||
@@ -494,7 +570,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
async getRecoveryToken (): Promise<any> {
|
async getRecoveryToken (): Promise<any> {
|
||||||
return this.root.serialize()
|
return this.root.serialize(this.tabRecovery)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -508,6 +584,20 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
this.splitAdjusted.next(spanner)
|
this.splitAdjusted.next(spanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
onTabDropped (tab: BaseTabComponent, zone: SplitDropZoneInfo) { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
if (tab === this) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.type === 'relative') {
|
||||||
|
this.add(tab, zone.relativeTo ?? null, zone.side)
|
||||||
|
} else {
|
||||||
|
this.add(tab, zone.container.children[zone.position], zone.container.orientation === 'h' ? 'r' : 'b')
|
||||||
|
}
|
||||||
|
this.tabAdopted.next(tab)
|
||||||
|
}
|
||||||
|
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
for (const x of this.getAllTabs()) {
|
for (const x of this.getAllTabs()) {
|
||||||
@@ -518,20 +608,24 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
layout (): void {
|
layout (): void {
|
||||||
this.root.normalize()
|
this.root.normalize()
|
||||||
this._spanners = []
|
this._spanners = []
|
||||||
|
this._dropZones = []
|
||||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachTabView (tab: BaseTabComponent) {
|
private updateTitle (): void {
|
||||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
|
||||||
this.viewRefs.set(tab, ref)
|
}
|
||||||
|
|
||||||
|
private attachTabView (tab: BaseTabComponent) {
|
||||||
|
const ref = tab.insertIntoContainer(this.viewContainer)
|
||||||
|
this.viewRefs.set(tab, ref)
|
||||||
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
||||||
|
|
||||||
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
|
tab.subscribeUntilDestroyed(tab.titleChange$, () => this.updateTitle())
|
||||||
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
|
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
|
||||||
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
|
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
|
||||||
if (tab.title) {
|
if (tab.title) {
|
||||||
this.setTitle(tab.title)
|
this.updateTitle()
|
||||||
}
|
}
|
||||||
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
|
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
|
||||||
this.recoveryStateChangedHint.next()
|
this.recoveryStateChangedHint.next()
|
||||||
@@ -541,14 +635,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private detachTabView (tab: BaseTabComponent) {
|
|
||||||
const ref = this.viewRefs.get(tab)
|
|
||||||
if (ref) {
|
|
||||||
this.viewRefs.delete(tab)
|
|
||||||
this.viewContainer.remove(this.viewContainer.indexOf(ref))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAfterTabAdded (tab: BaseTabComponent) {
|
private onAfterTabAdded (tab: BaseTabComponent) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.layout()
|
this.layout()
|
||||||
@@ -560,6 +646,42 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
|
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
|
||||||
const size = root.orientation === 'v' ? h : w
|
const size = root.orientation === 'v' ? h : w
|
||||||
const sizes = root.ratios.map(ratio => ratio * size)
|
const sizes = root.ratios.map(ratio => ratio * size)
|
||||||
|
const thickness = 10
|
||||||
|
|
||||||
|
if (root === this.root) {
|
||||||
|
this._dropZones.push({
|
||||||
|
x: x - thickness / 2,
|
||||||
|
y: y + thickness,
|
||||||
|
w: thickness,
|
||||||
|
h: h - thickness * 2,
|
||||||
|
type: 'relative',
|
||||||
|
side: 'l',
|
||||||
|
})
|
||||||
|
this._dropZones.push({
|
||||||
|
x,
|
||||||
|
y: y - thickness / 2,
|
||||||
|
w,
|
||||||
|
h: thickness,
|
||||||
|
type: 'relative',
|
||||||
|
side: 't',
|
||||||
|
})
|
||||||
|
this._dropZones.push({
|
||||||
|
x: x + w - thickness / 2,
|
||||||
|
y: y + thickness,
|
||||||
|
w: thickness,
|
||||||
|
h: h - thickness * 2,
|
||||||
|
type: 'relative',
|
||||||
|
side: 'r',
|
||||||
|
})
|
||||||
|
this._dropZones.push({
|
||||||
|
x,
|
||||||
|
y: y + h - thickness / 2,
|
||||||
|
w,
|
||||||
|
h: thickness,
|
||||||
|
type: 'relative',
|
||||||
|
side: 'b',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
root.x = x
|
root.x = x
|
||||||
root.y = y
|
root.y = y
|
||||||
@@ -595,8 +717,63 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += sizes[i]
|
offset += sizes[i]
|
||||||
|
|
||||||
|
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,
|
||||||
|
y: root.orientation === 'v' ? childY + offset - thickness / 2 : childY + thickness,
|
||||||
|
w: root.orientation === 'v' ? childW - thickness * 2 : thickness,
|
||||||
|
h: root.orientation === 'v' ? thickness : childH - thickness * 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sides
|
||||||
|
if (root.orientation === 'v') {
|
||||||
|
this._dropZones.push({
|
||||||
|
x: childX,
|
||||||
|
y: childY + thickness,
|
||||||
|
w: thickness,
|
||||||
|
h: childH - thickness * 2,
|
||||||
|
type: 'relative',
|
||||||
|
relativeTo: child,
|
||||||
|
side: 'l',
|
||||||
|
})
|
||||||
|
this._dropZones.push({
|
||||||
|
x: childX + w - thickness,
|
||||||
|
y: childY + thickness,
|
||||||
|
w: thickness,
|
||||||
|
h: childH - thickness * 2,
|
||||||
|
type: 'relative',
|
||||||
|
relativeTo: child,
|
||||||
|
side: 'r',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this._dropZones.push({
|
||||||
|
x: childX + thickness,
|
||||||
|
y: childY,
|
||||||
|
w: childW - thickness * 2,
|
||||||
|
h: thickness,
|
||||||
|
type: 'relative',
|
||||||
|
relativeTo: child,
|
||||||
|
side: 't',
|
||||||
|
})
|
||||||
|
this._dropZones.push({
|
||||||
|
x: childX + thickness,
|
||||||
|
y: childY + childH - thickness,
|
||||||
|
w: childW - thickness * 2,
|
||||||
|
h: thickness,
|
||||||
|
type: 'relative',
|
||||||
|
relativeTo: child,
|
||||||
|
side: 'b',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (i !== 0) {
|
if (i !== 0) {
|
||||||
this._spanners.push({
|
this._spanners.push({
|
||||||
container: root,
|
container: root,
|
||||||
@@ -612,6 +789,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||||||
root.ratios = state.ratios
|
root.ratios = state.ratios
|
||||||
root.children = children
|
root.children = children
|
||||||
for (const childState of state.children) {
|
for (const childState of state.children) {
|
||||||
|
if (!childState) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (childState.type === 'app:split-tab') {
|
if (childState.type === 'app:split-tab') {
|
||||||
const child = new SplitContainer()
|
const child = new SplitContainer()
|
||||||
await this.recoverContainer(child, childState, duplicate)
|
await this.recoverContainer(child, childState, duplicate)
|
||||||
|
39
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
39
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 15px;
|
||||||
|
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
background: rgba(255, 255, 255, .125);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .25);
|
||||||
|
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
padding: 0px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, .5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep tab-header {
|
||||||
|
// placeholders
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
54
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
54
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { AppService } from '../services/app.service'
|
||||||
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
|
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||||
|
import { SplitDropZoneInfo } from './splitTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab-drop-zone',
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
cdkDropList
|
||||||
|
(cdkDropListDropped)="tabDropped.emit($event.item.data); isHighlighted = false"
|
||||||
|
(cdkDropListEntered)="isHighlighted = true"
|
||||||
|
(cdkDropListExited)="isHighlighted = false"
|
||||||
|
cdkAutoDropGroup='app-tabs'
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [require('./splitTabDropZone.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabDropZoneComponent extends SelfPositioningComponent {
|
||||||
|
@Input() dropZone: SplitDropZoneInfo
|
||||||
|
@Input() parent: BaseTabComponent
|
||||||
|
@Output() tabDropped = new EventEmitter<BaseTabComponent>()
|
||||||
|
@HostBinding('class.active') isActive = false
|
||||||
|
@HostBinding('class.highlighted') isHighlighted = false
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
constructor (
|
||||||
|
element: ElementRef,
|
||||||
|
app: AppService,
|
||||||
|
) {
|
||||||
|
super(element)
|
||||||
|
this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
|
||||||
|
this.isActive = !!tab && tab !== this.parent && (this.dropZone.type === 'relative' || tab !== this.dropZone.container.children[this.dropZone.position])
|
||||||
|
this.layout()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
layout () {
|
||||||
|
this.setDimensions(
|
||||||
|
this.dropZone.x,
|
||||||
|
this.dropZone.y,
|
||||||
|
this.dropZone.w,
|
||||||
|
this.dropZone.h,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, .25);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0;
|
||||||
|
transition: .125s opacity cubic-bezier(0.86, 0, 0.07, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
background: rgba(0, 0, 0, .7);
|
||||||
|
padding: 20px 30px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host.active {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
pointer-events: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: move;
|
||||||
|
}
|
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component, Input, HostBinding, ElementRef } from '@angular/core'
|
||||||
|
import { HotkeysService } from '../services/hotkeys.service'
|
||||||
|
import { AppService } from '../services/app.service'
|
||||||
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
|
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab-pane-label',
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
cdkDrag
|
||||||
|
[cdkDragData]='tab'
|
||||||
|
(cdkDragStarted)='onTabDragStart(tab)'
|
||||||
|
(cdkDragEnded)='onTabDragEnd()'
|
||||||
|
>
|
||||||
|
<i class="fa fa-window-maximize mr-3"></i>
|
||||||
|
<label>{{tab.title}}</label>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [require('./splitTabPaneLabel.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabPaneLabelComponent extends SelfPositioningComponent {
|
||||||
|
@Input() tab: BaseTabComponent
|
||||||
|
@Input() parent: BaseTabComponent
|
||||||
|
@HostBinding('class.active') isActive = false
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
constructor (
|
||||||
|
element: ElementRef,
|
||||||
|
hotkeys: HotkeysService,
|
||||||
|
private app: AppService,
|
||||||
|
) {
|
||||||
|
super(element)
|
||||||
|
this.subscribeUntilDestroyed(hotkeys.hotkey$, hk => {
|
||||||
|
if (hk === 'rearrange-panes' && this.parent.hasFocus) {
|
||||||
|
this.isActive = true
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.subscribeUntilDestroyed(hotkeys.hotkeyOff$, hk => {
|
||||||
|
if (hk === 'rearrange-panes') {
|
||||||
|
this.isActive = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabDragStart (tab: BaseTabComponent): void {
|
||||||
|
this.app.emitTabDragStarted(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabDragEnd (): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
|
this.app.emitTabsChanged()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
layout () {
|
||||||
|
const tabElement: HTMLElement|undefined = this.tab.viewContainerEmbeddedRef?.rootNodes[0]
|
||||||
|
|
||||||
|
if (!tabElement) {
|
||||||
|
// being destroyed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setDimensions(
|
||||||
|
tabElement.offsetLeft,
|
||||||
|
tabElement.offsetTop,
|
||||||
|
tabElement.clientWidth,
|
||||||
|
tabElement.clientHeight,
|
||||||
|
'px'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
transition: 0.125s background;
|
transition: 0.125s background;
|
||||||
|
background: rgba(0, 0, 0, .2);
|
||||||
|
|
||||||
&.v {
|
&.v {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||||
import { SplitContainer } from './splitTab.component'
|
import { SplitContainer } from './splitTab.component'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@@ -8,20 +9,19 @@ import { SplitContainer } from './splitTab.component'
|
|||||||
template: '',
|
template: '',
|
||||||
styles: [require('./splitTabSpanner.component.scss')],
|
styles: [require('./splitTabSpanner.component.scss')],
|
||||||
})
|
})
|
||||||
export class SplitTabSpannerComponent {
|
export class SplitTabSpannerComponent extends SelfPositioningComponent {
|
||||||
@Input() container: SplitContainer
|
@Input() container: SplitContainer
|
||||||
@Input() index: number
|
@Input() index: number
|
||||||
@Output() change = new EventEmitter<void>()
|
@Output() change = new EventEmitter<void>()
|
||||||
@HostBinding('class.active') isActive = false
|
@HostBinding('class.active') isActive = false
|
||||||
@HostBinding('class.h') isHorizontal = false
|
@HostBinding('class.h') isHorizontal = false
|
||||||
@HostBinding('class.v') isVertical = true
|
@HostBinding('class.v') isVertical = true
|
||||||
@HostBinding('style.left') cssLeft: string
|
|
||||||
@HostBinding('style.top') cssTop: string
|
|
||||||
@HostBinding('style.width') cssWidth: string | null
|
|
||||||
@HostBinding('style.height') cssHeight: string | null
|
|
||||||
private marginOffset = -5
|
private marginOffset = -5
|
||||||
|
|
||||||
constructor (private element: ElementRef) { }
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
constructor (element: ElementRef) {
|
||||||
|
super(element)
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
this.element.nativeElement.addEventListener('dblclick', () => {
|
this.element.nativeElement.addEventListener('dblclick', () => {
|
||||||
@@ -92,11 +92,4 @@ export class SplitTabSpannerComponent {
|
|||||||
this.container.ratios[this.index] = ratio
|
this.container.ratios[this.index] = ratio
|
||||||
this.change.emit()
|
this.change.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDimensions (x: number, y: number, w: number, h: number) {
|
|
||||||
this.cssLeft = `${x}%`
|
|
||||||
this.cssTop = `${y}%`
|
|
||||||
this.cssWidth = w ? `${w}%` : null
|
|
||||||
this.cssHeight = h ? `${h}%` : null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,9 @@ div
|
|||||||
h1.tabby-title Tabby
|
h1.tabby-title Tabby
|
||||||
sup α
|
sup α
|
||||||
|
|
||||||
.list-group
|
.list-group.list-group-light
|
||||||
a.list-group-item.list-group-item-action.d-flex(
|
a.list-group-item.list-group-item-action.d-flex(
|
||||||
*ngFor='let button of getButtons()',
|
*ngFor='let button of getButtons(); trackBy: buttonsTrackBy',
|
||||||
(click)='button.click()',
|
(click)='button.click()',
|
||||||
)
|
)
|
||||||
.d-flex.align-self-center([innerHTML]='sanitizeIcon(button.icon)')
|
.d-flex.align-self-center([innerHTML]='sanitizeIcon(button.icon)')
|
||||||
@@ -13,10 +13,10 @@ div
|
|||||||
|
|
||||||
footer.d-flex.align-items-center
|
footer.d-flex.align-items-center
|
||||||
.btn-group.mr-auto
|
.btn-group.mr-auto
|
||||||
button.btn.btn-secondary((click)='homeBase.openGitHub()')
|
button.btn.btn-dark((click)='homeBase.openGitHub()')
|
||||||
i.fab.fa-github
|
i.fab.fa-github
|
||||||
span GitHub
|
span GitHub
|
||||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
button.btn.btn-dark((click)='homeBase.reportBug()')
|
||||||
i.fas.fa-bug
|
i.fas.fa-bug
|
||||||
span Report a problem
|
span Report a problem
|
||||||
|
|
||||||
|
@@ -32,4 +32,8 @@ export class StartPageComponent {
|
|||||||
sanitizeIcon (icon?: string): any {
|
sanitizeIcon (icon?: string): any {
|
||||||
return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
|
return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonsTrackBy (btn: ToolbarButton): any {
|
||||||
|
return btn.title + btn.icon
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,10 @@ export class TabBodyComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detach () {
|
||||||
|
this.placeholder?.detach()
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.placeholder?.detach()
|
this.placeholder?.detach()
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,18 @@
|
|||||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||||
.activity-indicator(*ngIf='tab.activity$|async')
|
.activity-indicator(*ngIf='tab.activity$|async')
|
||||||
|
|
||||||
.index(*ngIf='!config.store.terminal.hideTabIndex', #handle) {{index + 1}}
|
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
|
||||||
|
.index(*ngIf='!config.store.terminal.hideTabIndex && hostApp.platform !== Platform.macOS') {{index + 1}}
|
||||||
|
|
||||||
.name(
|
.name(
|
||||||
[title]='tab.customTitle || tab.title',
|
[title]='tab.customTitle || tab.title',
|
||||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||||
|
cdkDrag,
|
||||||
|
cdkDragRootElement='tab-header',
|
||||||
|
[cdkDragData]='tab',
|
||||||
|
(cdkDragStarted)='onTabDragStart(tab)',
|
||||||
|
(cdkDragEnded)='onTabDragEnd()',
|
||||||
) {{tab.customTitle || tab.title}}
|
) {{tab.customTitle || tab.title}}
|
||||||
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
||||||
|
|
||||||
|
ng-content
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
|
import { Component, Input, Optional, Inject, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||||
import { SortableComponent } from 'ng2-dnd'
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||||
import { BaseTabComponent } from './baseTab.component'
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
@@ -13,11 +12,6 @@ import { BaseComponent } from './base.component'
|
|||||||
import { MenuItemOptions } from '../api/menu'
|
import { MenuItemOptions } from '../api/menu'
|
||||||
import { PlatformService } from '../api/platform'
|
import { PlatformService } from '../api/platform'
|
||||||
|
|
||||||
/** @hidden */
|
|
||||||
export interface SortableComponentProxy {
|
|
||||||
setDragHandle: (_: HTMLElement) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tab-header',
|
selector: 'tab-header',
|
||||||
@@ -29,17 +23,16 @@ export class TabHeaderComponent extends BaseComponent {
|
|||||||
@Input() @HostBinding('class.active') active: boolean
|
@Input() @HostBinding('class.active') active: boolean
|
||||||
@Input() tab: BaseTabComponent
|
@Input() tab: BaseTabComponent
|
||||||
@Input() progress: number|null
|
@Input() progress: number|null
|
||||||
@ViewChild('handle') handle?: ElementRef
|
Platform = Platform
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public app: AppService,
|
public app: AppService,
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
private hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private hotkeys: HotkeysService,
|
private hotkeys: HotkeysService,
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
|
|
||||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@@ -61,18 +54,13 @@ export class TabHeaderComponent extends BaseComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
|
||||||
if (this.handle && this.hostApp.platform === Platform.macOS) {
|
|
||||||
this.parentDraggable.setDragHandle(this.handle.nativeElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showRenameTabModal (): void {
|
showRenameTabModal (): void {
|
||||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||||
modal.result.then(result => {
|
modal.result.then(result => {
|
||||||
this.tab.setTitle(result)
|
this.tab.setTitle(result)
|
||||||
this.tab.customTitle = result
|
this.tab.customTitle = result
|
||||||
|
this.app.emitTabsChanged()
|
||||||
}).catch(() => null)
|
}).catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +73,17 @@ export class TabHeaderComponent extends BaseComponent {
|
|||||||
return items.slice(1)
|
return items.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTabDragStart (tab: BaseTabComponent) {
|
||||||
|
this.app.emitTabDragStarted(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabDragEnd () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
|
this.app.emitTabsChanged()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@HostBinding('class.flex-width') get isFlexWidthEnabled (): boolean {
|
@HostBinding('class.flex-width') get isFlexWidthEnabled (): boolean {
|
||||||
return this.config.store.appearance.flexTabs
|
return this.config.store.appearance.flexTabs
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}
|
||||||
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
.icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')}
|
||||||
.main
|
.main
|
||||||
label {{transfer.getName()}}
|
label.no-wrap([title]='transfer.getName()') {{transfer.getName()}}
|
||||||
.status(*ngIf='transfer.isComplete()')
|
.status(*ngIf='transfer.isComplete()')
|
||||||
ngb-progressbar(type='success', [value]='100')
|
ngb-progressbar(type='success', [value]='100')
|
||||||
.status(*ngIf='transfer.isCancelled()')
|
.status(*ngIf='transfer.isCancelled()')
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 0 5px 25px;
|
padding: 5px 0 5px 25px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
padding: 4px 7px;
|
padding: 4px 7px;
|
||||||
@@ -16,12 +17,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -43,6 +43,7 @@ export class TransfersMenuComponent {
|
|||||||
message: 'There are active file transfers',
|
message: 'There are active file transfers',
|
||||||
buttons: ['Abort all', 'Do not abort'],
|
buttons: ['Abort all', 'Do not abort'],
|
||||||
defaultId: 1,
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
})).response === 1) {
|
})).response === 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,8 @@ hotkeys:
|
|||||||
- 'Ctrl-Shift-PageUp'
|
- 'Ctrl-Shift-PageUp'
|
||||||
move-tab-right:
|
move-tab-right:
|
||||||
- 'Ctrl-Shift-PageDown'
|
- 'Ctrl-Shift-PageDown'
|
||||||
|
rearrange-panes:
|
||||||
|
- 'Ctrl-Shift'
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
|
@@ -16,6 +16,8 @@ hotkeys:
|
|||||||
- '⌘-Shift-Left'
|
- '⌘-Shift-Left'
|
||||||
move-tab-right:
|
move-tab-right:
|
||||||
- '⌘-Shift-Right'
|
- '⌘-Shift-Right'
|
||||||
|
rearrange-panes:
|
||||||
|
- '⌘-Shift'
|
||||||
tab-1:
|
tab-1:
|
||||||
- '⌘-1'
|
- '⌘-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
|
@@ -19,6 +19,8 @@ hotkeys:
|
|||||||
- 'Ctrl-Shift-PageUp'
|
- 'Ctrl-Shift-PageUp'
|
||||||
move-tab-right:
|
move-tab-right:
|
||||||
- 'Ctrl-Shift-PageDown'
|
- 'Ctrl-Shift-PageDown'
|
||||||
|
rearrange-panes:
|
||||||
|
- 'Ctrl-Shift'
|
||||||
tab-1:
|
tab-1:
|
||||||
- 'Alt-1'
|
- 'Alt-1'
|
||||||
tab-2:
|
tab-2:
|
||||||
|
@@ -16,6 +16,7 @@ appearance:
|
|||||||
vibrancyType: 'blur'
|
vibrancyType: 'blur'
|
||||||
terminal:
|
terminal:
|
||||||
showBuiltinProfiles: true
|
showBuiltinProfiles: true
|
||||||
|
showRecentProfiles: 3
|
||||||
hotkeys:
|
hotkeys:
|
||||||
profile:
|
profile:
|
||||||
__nonStructural: true
|
__nonStructural: true
|
||||||
@@ -30,3 +31,4 @@ enableAutomaticUpdates: true
|
|||||||
version: 1
|
version: 1
|
||||||
vault: null
|
vault: null
|
||||||
encrypted: false
|
encrypted: false
|
||||||
|
enableExperimentalFeatures: false
|
||||||
|
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Directive, Input, OnInit } from '@angular/core'
|
||||||
|
import { CdkDropList } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
|
class FakeDropGroup {
|
||||||
|
_items: Set<CdkDropList> = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Directive({
|
||||||
|
selector: '[cdkAutoDropGroup]',
|
||||||
|
})
|
||||||
|
export class CdkAutoDropGroup implements OnInit {
|
||||||
|
static groups: Record<string, FakeDropGroup> = {}
|
||||||
|
|
||||||
|
@Input('cdkAutoDropGroup') groupName: string
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private cdkDropList: CdkDropList,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit (): void {
|
||||||
|
CdkAutoDropGroup.groups[this.groupName] ??= new FakeDropGroup()
|
||||||
|
CdkAutoDropGroup.groups[this.groupName]._items.add(this.cdkDropList)
|
||||||
|
this.cdkDropList['_group'] = CdkAutoDropGroup.groups[this.groupName]
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ProfilesService } from './services/profiles.service'
|
import { ProfilesService } from './services/profiles.service'
|
||||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||||
|
import { PartialProfile, Profile } from './api'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -46,6 +47,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
id: 'move-tab-right',
|
id: 'move-tab-right',
|
||||||
name: 'Move tab to the right',
|
name: 'Move tab to the right',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'rearrange-panes',
|
||||||
|
name: 'Show pane labels (for rearranging)',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tab-1',
|
id: 'tab-1',
|
||||||
name: 'Tab 1',
|
name: 'Tab 1',
|
||||||
@@ -189,9 +194,13 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
return [
|
return [
|
||||||
...this.hotkeys,
|
...this.hotkeys,
|
||||||
...profiles.map(profile => ({
|
...profiles.map(profile => ({
|
||||||
id: `profile.${profile.id}`,
|
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
||||||
name: `New tab: ${profile.name}`,
|
name: `New tab: ${profile.name}`,
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||||
|
return profile.id!.replace(/\./g, '-')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,8 +5,8 @@ import { FormsModule } from '@angular/forms'
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
import { DndModule } from 'ng2-dnd'
|
|
||||||
import { SortablejsModule } from 'ngx-sortablejs'
|
import { SortablejsModule } from 'ngx-sortablejs'
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
import { AppRootComponent } from './components/appRoot.component'
|
import { AppRootComponent } from './components/appRoot.component'
|
||||||
import { CheckboxComponent } from './components/checkbox.component'
|
import { CheckboxComponent } from './components/checkbox.component'
|
||||||
@@ -22,6 +22,8 @@ import { RenameTabModalComponent } from './components/renameTabModal.component'
|
|||||||
import { SelectorModalComponent } from './components/selectorModal.component'
|
import { SelectorModalComponent } from './components/selectorModal.component'
|
||||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||||
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||||
|
import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component'
|
||||||
|
import { SplitTabPaneLabelComponent } from './components/splitTabPaneLabel.component'
|
||||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||||
@@ -30,6 +32,7 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
|||||||
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
||||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||||
|
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
||||||
|
|
||||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api'
|
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api'
|
||||||
|
|
||||||
@@ -47,7 +50,6 @@ import { ButtonProvider } from './buttonProvider'
|
|||||||
import { SplitLayoutProfilesService } from './profiles'
|
import { SplitLayoutProfilesService } from './profiles'
|
||||||
|
|
||||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||||
import 'ng2-dnd/bundles/style.css'
|
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||||
@@ -77,7 +79,7 @@ const PROVIDERS = [
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
NgxFilesizeModule,
|
NgxFilesizeModule,
|
||||||
PerfectScrollbarModule,
|
PerfectScrollbarModule,
|
||||||
DndModule.forRoot(),
|
DragDropModule,
|
||||||
SortablejsModule.forRoot({ animation: 150 }),
|
SortablejsModule.forRoot({ animation: 150 }),
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -98,10 +100,13 @@ const PROVIDERS = [
|
|||||||
SelectorModalComponent,
|
SelectorModalComponent,
|
||||||
SplitTabComponent,
|
SplitTabComponent,
|
||||||
SplitTabSpannerComponent,
|
SplitTabSpannerComponent,
|
||||||
|
SplitTabDropZoneComponent,
|
||||||
|
SplitTabPaneLabelComponent,
|
||||||
UnlockVaultModalComponent,
|
UnlockVaultModalComponent,
|
||||||
WelcomeTabComponent,
|
WelcomeTabComponent,
|
||||||
TransfersMenuComponent,
|
TransfersMenuComponent,
|
||||||
DropZoneDirective,
|
DropZoneDirective,
|
||||||
|
CdkAutoDropGroup,
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
@@ -121,6 +126,7 @@ const PROVIDERS = [
|
|||||||
FastHtmlBindDirective,
|
FastHtmlBindDirective,
|
||||||
AlwaysVisibleTypeaheadDirective,
|
AlwaysVisibleTypeaheadDirective,
|
||||||
SortablejsModule,
|
SortablejsModule,
|
||||||
|
DragDropModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||||
@@ -132,9 +138,11 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||||||
profilesService: ProfilesService,
|
profilesService: ProfilesService,
|
||||||
) {
|
) {
|
||||||
app.ready$.subscribe(() => {
|
app.ready$.subscribe(() => {
|
||||||
if (config.store.enableWelcomeTab) {
|
config.ready$.toPromise().then(() => {
|
||||||
app.openNewTabRaw({ type: WelcomeTabComponent })
|
if (config.store.enableWelcomeTab) {
|
||||||
}
|
app.openNewTabRaw({ type: WelcomeTabComponent })
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.setErrorHandler(err => {
|
platform.setErrorHandler(err => {
|
||||||
@@ -143,8 +151,9 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||||||
|
|
||||||
hotkeys.hotkey$.subscribe(async (hotkey) => {
|
hotkeys.hotkey$.subscribe(async (hotkey) => {
|
||||||
if (hotkey.startsWith('profile.')) {
|
if (hotkey.startsWith('profile.')) {
|
||||||
const id = hotkey.split('.')[1]
|
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||||
const profile = (await profilesService.getProfiles()).find(x => x.id === id)
|
const profiles = await profilesService.getProfiles()
|
||||||
|
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
profilesService.openNewTabForProfile(profile)
|
profilesService.openNewTabForProfile(profile)
|
||||||
}
|
}
|
||||||
|
@@ -54,7 +54,9 @@ export class AppService {
|
|||||||
private activeTabChange = new Subject<BaseTabComponent|null>()
|
private activeTabChange = new Subject<BaseTabComponent|null>()
|
||||||
private tabsChanged = new Subject<void>()
|
private tabsChanged = new Subject<void>()
|
||||||
private tabOpened = new Subject<BaseTabComponent>()
|
private tabOpened = new Subject<BaseTabComponent>()
|
||||||
|
private tabRemoved = new Subject<BaseTabComponent>()
|
||||||
private tabClosed = new Subject<BaseTabComponent>()
|
private tabClosed = new Subject<BaseTabComponent>()
|
||||||
|
private tabDragActive = new Subject<BaseTabComponent|null>()
|
||||||
private ready = new AsyncSubject<void>()
|
private ready = new AsyncSubject<void>()
|
||||||
|
|
||||||
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
||||||
@@ -62,7 +64,9 @@ export class AppService {
|
|||||||
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
||||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||||
|
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||||
|
get tabDragActive$ (): Observable<BaseTabComponent|null> { return this.tabDragActive }
|
||||||
|
|
||||||
/** Fires once when the app is ready */
|
/** Fires once when the app is ready */
|
||||||
get ready$ (): Observable<void> { return this.ready }
|
get ready$ (): Observable<void> { return this.ready }
|
||||||
@@ -131,21 +135,30 @@ export class AppService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
tab.destroyed$.subscribe(() => {
|
tab.destroyed$.subscribe(() => {
|
||||||
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
this.removeTab(tab)
|
||||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
this.tabRemoved.next(tab)
|
||||||
if (tab === this._activeTab) {
|
|
||||||
this.selectTab(this.tabs[newIndex])
|
|
||||||
}
|
|
||||||
this.tabsChanged.next()
|
|
||||||
this.tabClosed.next(tab)
|
this.tabClosed.next(tab)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab instanceof SplitTabComponent) {
|
if (tab instanceof SplitTabComponent) {
|
||||||
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
||||||
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
||||||
|
tab.tabAdopted$.subscribe(t => {
|
||||||
|
this.removeTab(t)
|
||||||
|
this.tabRemoved.next(t)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeTab (tab: BaseTabComponent): void {
|
||||||
|
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||||
|
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||||
|
if (tab === this._activeTab) {
|
||||||
|
this.selectTab(this.tabs[newIndex])
|
||||||
|
}
|
||||||
|
this.tabsChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
||||||
* @param inputs Properties to be assigned on the new tab component instance
|
* @param inputs Properties to be assigned on the new tab component instance
|
||||||
@@ -161,6 +174,9 @@ export class AppService {
|
|||||||
* @param inputs Properties to be assigned on the new tab component instance
|
* @param inputs Properties to be assigned on the new tab component instance
|
||||||
*/
|
*/
|
||||||
openNewTab <T extends BaseTabComponent> (params: NewTabParameters<T>): T {
|
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 splitTab = this.tabsService.create({ type: SplitTabComponent })
|
||||||
const tab = this.tabsService.create(params)
|
const tab = this.tabsService.create(params)
|
||||||
splitTab.addTab(tab, null, 'r')
|
splitTab.addTab(tab, null, 'r')
|
||||||
@@ -344,6 +360,16 @@ export class AppService {
|
|||||||
this.hostApp.emitReady()
|
this.hostApp.emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
emitTabDragStarted (tab: BaseTabComponent): void {
|
||||||
|
this.tabDragActive.next(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
emitTabDragEnded (): void {
|
||||||
|
this.tabDragActive.next(null)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable that fires once
|
* Returns an observable that fires once
|
||||||
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||||
|
@@ -194,7 +194,10 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async save (): Promise<void> {
|
async save (): Promise<void> {
|
||||||
this.store.__cleanup()
|
await this.ready$
|
||||||
|
if (!this._store) {
|
||||||
|
throw new Error('Cannot save an empty store')
|
||||||
|
}
|
||||||
// Scrub undefined values
|
// Scrub undefined values
|
||||||
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
let cleanStore = JSON.parse(JSON.stringify(this._store))
|
||||||
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
cleanStore = await this.maybeEncryptConfig(cleanStore)
|
||||||
@@ -238,7 +241,7 @@ export class ConfigService {
|
|||||||
const module = imp.ngModule || imp
|
const module = imp.ngModule || imp
|
||||||
if (module.ɵinj?.providers) {
|
if (module.ɵinj?.providers) {
|
||||||
this.servicesCache[module.pluginName] = module.ɵinj.providers.map(provider => {
|
this.servicesCache[module.pluginName] = module.ɵinj.providers.map(provider => {
|
||||||
return provider.useClass || provider
|
return provider.useClass ?? provider.useExisting ?? provider
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,6 +375,7 @@ export class ConfigService {
|
|||||||
detail: e.toString(),
|
detail: e.toString(),
|
||||||
buttons: ['Erase config', 'Quit'],
|
buttons: ['Erase config', 'Quit'],
|
||||||
defaultId: 1,
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
})
|
})
|
||||||
if (result.response === 1) {
|
if (result.response === 1) {
|
||||||
this.platform.quit()
|
this.platform.quit()
|
||||||
@@ -382,10 +386,12 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
delete decryptedVault.config.vault
|
delete decryptedVault.config.vault
|
||||||
delete decryptedVault.config.encrypted
|
delete decryptedVault.config.encrypted
|
||||||
|
delete decryptedVault.config.configSync
|
||||||
return {
|
return {
|
||||||
...decryptedVault.config,
|
...decryptedVault.config,
|
||||||
vault: store.vault,
|
vault: store.vault,
|
||||||
encrypted: store.encrypted,
|
encrypted: store.encrypted,
|
||||||
|
configSync: store.configSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,9 +406,11 @@ export class ConfigService {
|
|||||||
vault.config = { ...store }
|
vault.config = { ...store }
|
||||||
delete vault.config.vault
|
delete vault.config.vault
|
||||||
delete vault.config.encrypted
|
delete vault.config.encrypted
|
||||||
|
delete vault.config.configSync
|
||||||
return {
|
return {
|
||||||
vault: await this.vault.encrypt(vault),
|
vault: await this.vault.encrypt(vault),
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
|
configSync: store.configSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { Injectable, Inject } from '@angular/core'
|
|||||||
import * as mixpanel from 'mixpanel'
|
import * as mixpanel from 'mixpanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
|
import { PlatformService, BOOTSTRAP_DATA, BootstrapData, HostAppService } from '../api'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HomeBaseService {
|
export class HomeBaseService {
|
||||||
@@ -13,6 +13,7 @@ export class HomeBaseService {
|
|||||||
private constructor (
|
private constructor (
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
|
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
|
||||||
) {
|
) {
|
||||||
this.appVersion = platform.getAppVersion()
|
this.appVersion = platform.getAppVersion()
|
||||||
@@ -28,20 +29,11 @@ export class HomeBaseService {
|
|||||||
|
|
||||||
reportBug (): void {
|
reportBug (): void {
|
||||||
let body = `Version: ${this.appVersion}\n`
|
let body = `Version: ${this.appVersion}\n`
|
||||||
body += `Platform: ${process.platform} ${this.platform.getOSRelease()}\n`
|
body += `Platform: ${this.hostApp.platform} ${process.arch} ${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]
|
|
||||||
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
||||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
body += `Plugins: ${plugins.join(', ') || 'none'}\n`
|
||||||
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
|
body += `Frontend: ${this.config.store.terminal?.frontend}\n\n`
|
||||||
|
this.platform.openExternal(`https://github.com/Eugeny/tabby/issues/new?body=${encodeURIComponent(body)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableAnalytics (): void {
|
enableAnalytics (): void {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
|
||||||
import { stringifyKeySequence, EventData } from './hotkeys.util'
|
import { KeyEventData, getKeyName, Keystroke, KeyName, getKeystrokeName, metaKeyName, altKeyName } from './hotkeys.util'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { HostAppService, Platform } from '../api/hostApp'
|
import { HostAppService, Platform } from '../api/hostApp'
|
||||||
import { deprecate } from 'util'
|
import { deprecate } from 'util'
|
||||||
@@ -12,14 +12,17 @@ export interface PartialHotkeyMatch {
|
|||||||
matchedLength: number
|
matchedLength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY_TIMEOUT = 2000
|
interface PastKeystroke {
|
||||||
|
keystroke: Keystroke
|
||||||
|
time: number
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HotkeysService {
|
export class HotkeysService {
|
||||||
|
/** @hidden @deprecated */
|
||||||
key = new EventEmitter<KeyboardEvent>()
|
key = new EventEmitter<KeyboardEvent>()
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden @deprecated */
|
||||||
matchedHotkey = new EventEmitter<string>()
|
matchedHotkey = new EventEmitter<string>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,135 +30,212 @@ export class HotkeysService {
|
|||||||
*/
|
*/
|
||||||
get hotkey$ (): Observable<string> { return this._hotkey }
|
get hotkey$ (): Observable<string> { return this._hotkey }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for once hotkey is released
|
||||||
|
*/
|
||||||
|
get hotkeyOff$ (): Observable<string> { return this._hotkeyOff }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for each singular key
|
||||||
|
*/
|
||||||
|
get key$ (): Observable<KeyName> { return this._key }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for each key event
|
||||||
|
*/
|
||||||
|
get keyEvent$ (): Observable<KeyboardEvent> { return this._keyEvent }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for each singular key combination
|
||||||
|
*/
|
||||||
|
get keystroke$ (): Observable<Keystroke> { return this._keystroke }
|
||||||
|
|
||||||
private _hotkey = new Subject<string>()
|
private _hotkey = new Subject<string>()
|
||||||
private currentKeystrokes: EventData[] = []
|
private _hotkeyOff = new Subject<string>()
|
||||||
|
private _keyEvent = new Subject<KeyboardEvent>()
|
||||||
|
private _key = new Subject<KeyName>()
|
||||||
|
private _keystroke = new Subject<Keystroke>()
|
||||||
private disabledLevel = 0
|
private disabledLevel = 0
|
||||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||||
|
|
||||||
|
private pressedKeys = new Set<KeyName>()
|
||||||
|
private pressedKeyTimestamps = new Map<KeyName, number>()
|
||||||
|
private pressedHotkey: string|null = null
|
||||||
|
private pressedKeystroke: Keystroke|null = null
|
||||||
|
private lastKeystrokes: PastKeystroke[] = []
|
||||||
|
private shouldSaveNextKeystroke = true
|
||||||
|
private lastEventTimestamp = 0
|
||||||
|
|
||||||
private constructor (
|
private constructor (
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
|
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
|
||||||
hostApp: HostAppService,
|
hostApp: HostAppService,
|
||||||
) {
|
) {
|
||||||
const events = ['keydown', 'keyup']
|
|
||||||
events.forEach(event => {
|
|
||||||
document.addEventListener(event, (nativeEvent: KeyboardEvent) => {
|
|
||||||
if (document.querySelectorAll('input:focus').length === 0) {
|
|
||||||
this.pushKeystroke(event, nativeEvent)
|
|
||||||
this.processKeystrokes()
|
|
||||||
this.emitKeyEvent(nativeEvent)
|
|
||||||
if (hostApp.platform === Platform.Web) {
|
|
||||||
nativeEvent.preventDefault()
|
|
||||||
nativeEvent.stopPropagation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.config.ready$.toPromise().then(async () => {
|
this.config.ready$.toPromise().then(async () => {
|
||||||
const hotkeys = await this.getHotkeyDescriptions()
|
const hotkeys = await this.getHotkeyDescriptions()
|
||||||
this.hotkeyDescriptions = hotkeys
|
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
|
// deprecated
|
||||||
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
|
||||||
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
|
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
|
||||||
|
this.keyEvent$.subscribe(h => this.key.next(h))
|
||||||
|
this.key.subscribe = deprecate(s => this.keyEvent$.subscribe(s), 'key is deprecated, use keyEvent$')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new key event to the buffer
|
* Adds a new key event to the buffer
|
||||||
*
|
*
|
||||||
* @param name DOM event name
|
* @param eventName DOM event name
|
||||||
* @param nativeEvent event object
|
* @param nativeEvent event object
|
||||||
*/
|
*/
|
||||||
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
|
pushKeyEvent (eventName: string, nativeEvent: KeyboardEvent): void {
|
||||||
nativeEvent['event'] = name
|
if (nativeEvent.timeStamp === this.lastEventTimestamp) {
|
||||||
this.currentKeystrokes.push({
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeEvent['event'] = eventName
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
ctrlKey: nativeEvent.ctrlKey,
|
ctrlKey: nativeEvent.ctrlKey,
|
||||||
metaKey: nativeEvent.metaKey,
|
metaKey: nativeEvent.metaKey,
|
||||||
altKey: nativeEvent.altKey,
|
altKey: nativeEvent.altKey,
|
||||||
shiftKey: nativeEvent.shiftKey,
|
shiftKey: nativeEvent.shiftKey,
|
||||||
code: nativeEvent.code,
|
code: nativeEvent.code,
|
||||||
key: nativeEvent.key,
|
key: nativeEvent.key,
|
||||||
eventName: name,
|
eventName,
|
||||||
time: performance.now(),
|
time: nativeEvent.timeStamp,
|
||||||
})
|
registrationTime: performance.now(),
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the buffer for new complete keystrokes
|
|
||||||
*/
|
|
||||||
processKeystrokes (): void {
|
|
||||||
if (this.isEnabled()) {
|
|
||||||
this.zone.run(() => {
|
|
||||||
const matched = this.getCurrentFullyMatchedHotkey()
|
|
||||||
if (matched) {
|
|
||||||
console.log('Matched hotkey', matched)
|
|
||||||
this._hotkey.next(matched)
|
|
||||||
this.clearCurrentKeystrokes()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [key, time] of this.pressedKeyTimestamps.entries()) {
|
||||||
|
if (time < performance.now() - 2000) {
|
||||||
|
this.removePressedKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyName = getKeyName(eventData)
|
||||||
|
if (eventName === 'keydown') {
|
||||||
|
this.addPressedKey(keyName, eventData)
|
||||||
|
this.shouldSaveNextKeystroke = true
|
||||||
|
this.updateModifiers(eventData)
|
||||||
|
}
|
||||||
|
if (eventName === 'keyup') {
|
||||||
|
const keystroke = getKeystrokeName([...this.pressedKeys])
|
||||||
|
if (this.shouldSaveNextKeystroke) {
|
||||||
|
this._keystroke.next(keystroke)
|
||||||
|
this.lastKeystrokes.push({
|
||||||
|
keystroke,
|
||||||
|
time: performance.now(),
|
||||||
|
})
|
||||||
|
this.shouldSaveNextKeystroke = false
|
||||||
|
}
|
||||||
|
this.removePressedKey(keyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pressedKeys.size) {
|
||||||
|
this.pressedKeystroke = getKeystrokeName([...this.pressedKeys])
|
||||||
|
} else {
|
||||||
|
this.pressedKeystroke = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = this.matchActiveHotkey()
|
||||||
|
this.zone.run(() => {
|
||||||
|
if (matched) {
|
||||||
|
this.emitHotkeyOn(matched)
|
||||||
|
} else if (this.pressedHotkey) {
|
||||||
|
this.emitHotkeyOff(this.pressedHotkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.zone.run(() => {
|
||||||
|
this._key.next(getKeyName(eventData))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' && eventData.metaKey && eventName === 'keydown' && !['Ctrl', 'Shift', altKeyName, metaKeyName].includes(keyName)) {
|
||||||
|
// macOS will swallow non-modified keyups if Cmd is held down
|
||||||
|
this.pushKeyEvent('keyup', nativeEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastEventTimestamp = nativeEvent.timeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
emitKeyEvent (nativeEvent: KeyboardEvent): void {
|
getCurrentKeystrokes (): Keystroke[] {
|
||||||
this.zone.run(() => {
|
if (!this.pressedKeystroke) {
|
||||||
this.key.emit(nativeEvent)
|
return []
|
||||||
})
|
}
|
||||||
|
return [...this.lastKeystrokes.map(x => x.keystroke), this.pressedKeystroke]
|
||||||
|
}
|
||||||
|
|
||||||
|
matchActiveHotkey (partial = false): string|null {
|
||||||
|
if (!this.isEnabled() || !this.pressedKeystroke) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const matches: {
|
||||||
|
id: string,
|
||||||
|
sequence: string[],
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
const currentSequence = this.getCurrentKeystrokes()
|
||||||
|
|
||||||
|
const config = this.getHotkeysConfig()
|
||||||
|
for (const id in config) {
|
||||||
|
for (const sequence of config[id]) {
|
||||||
|
if (currentSequence.length < sequence.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (sequence[sequence.length - 1] !== this.pressedKeystroke) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastIndex = 0
|
||||||
|
let matched = true
|
||||||
|
for (const item of sequence) {
|
||||||
|
const nextOffset = currentSequence.slice(lastIndex).findIndex(
|
||||||
|
x => x.toLowerCase() === item.toLowerCase()
|
||||||
|
)
|
||||||
|
if (nextOffset === -1) {
|
||||||
|
matched = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastIndex += nextOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partial ? lastIndex > 0 : matched) {
|
||||||
|
matches.push({
|
||||||
|
id,
|
||||||
|
sequence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.sort((a, b) => b.sequence.length - a.sequence.length)
|
||||||
|
if (!matches.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return matches[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentKeystrokes (): void {
|
clearCurrentKeystrokes (): void {
|
||||||
this.currentKeystrokes = []
|
this.lastKeystrokes = []
|
||||||
}
|
this.pressedKeys.clear()
|
||||||
|
this.pressedKeyTimestamps.clear()
|
||||||
getCurrentKeystrokes (): string[] {
|
this.pressedKeystroke = null
|
||||||
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
|
this.pressedHotkey = null
|
||||||
return stringifyKeySequence(this.currentKeystrokes)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentFullyMatchedHotkey (): string|null {
|
|
||||||
const currentStrokes = this.getCurrentKeystrokes()
|
|
||||||
const config = this.getHotkeysConfig()
|
|
||||||
for (const id in config) {
|
|
||||||
for (const sequence of config[id]) {
|
|
||||||
if (currentStrokes.length < sequence.length) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (sequence.every(
|
|
||||||
(x: string, index: number) =>
|
|
||||||
x.toLowerCase() ===
|
|
||||||
currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase()
|
|
||||||
)) {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] {
|
|
||||||
const currentStrokes = this.getCurrentKeystrokes()
|
|
||||||
const config = this.getHotkeysConfig()
|
|
||||||
const result: PartialHotkeyMatch[] = []
|
|
||||||
for (const id in config) {
|
|
||||||
for (const sequence of config[id]) {
|
|
||||||
for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) {
|
|
||||||
if (sequence.slice(0, matchLength).every(
|
|
||||||
(x: string, index: number) =>
|
|
||||||
x.toLowerCase() ===
|
|
||||||
currentStrokes[currentStrokes.length - matchLength + index].toLowerCase()
|
|
||||||
)) {
|
|
||||||
result.push({
|
|
||||||
matchedLength: matchLength,
|
|
||||||
id,
|
|
||||||
strokes: sequence,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHotkeyDescription (id: string): HotkeyDescription {
|
getHotkeyDescription (id: string): HotkeyDescription {
|
||||||
@@ -183,6 +263,42 @@ export class HotkeysService {
|
|||||||
).reduce((a, b) => a.concat(b))
|
).reduce((a, b) => a.concat(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateModifiers (event: KeyEventData) {
|
||||||
|
for (const [prop, key] of Object.entries({
|
||||||
|
ctrlKey: 'Ctrl',
|
||||||
|
metaKey: metaKeyName,
|
||||||
|
altKey: altKeyName,
|
||||||
|
shiftKey: 'Shift',
|
||||||
|
})) {
|
||||||
|
if (!event[prop] && this.pressedKeys.has(key)) {
|
||||||
|
this.removePressedKey(key)
|
||||||
|
}
|
||||||
|
if (event[prop] && !this.pressedKeys.has(key)) {
|
||||||
|
this.addPressedKey(key, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitHotkeyOn (hotkey: string) {
|
||||||
|
if (this.pressedHotkey) {
|
||||||
|
if (this.pressedHotkey === hotkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.emitHotkeyOff(this.pressedHotkey)
|
||||||
|
}
|
||||||
|
if (document.querySelectorAll('input:focus').length === 0) {
|
||||||
|
console.debug('Matched hotkey', hotkey)
|
||||||
|
this._hotkey.next(hotkey)
|
||||||
|
this.pressedHotkey = hotkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitHotkeyOff (hotkey: string) {
|
||||||
|
console.debug('Unmatched hotkey', hotkey)
|
||||||
|
this._hotkeyOff.next(hotkey)
|
||||||
|
this.pressedHotkey = null
|
||||||
|
}
|
||||||
|
|
||||||
private getHotkeysConfig () {
|
private getHotkeysConfig () {
|
||||||
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
||||||
}
|
}
|
||||||
@@ -211,4 +327,14 @@ export class HotkeysService {
|
|||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addPressedKey (keyName: KeyName, eventData: KeyEventData) {
|
||||||
|
this.pressedKeys.add(keyName)
|
||||||
|
this.pressedKeyTimestamps.set(keyName, eventData.registrationTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePressedKey (key: KeyName) {
|
||||||
|
this.pressedKeys.delete(key)
|
||||||
|
this.pressedKeyTimestamps.delete(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-type-alias */
|
||||||
export const metaKeyName = {
|
export const metaKeyName = {
|
||||||
darwin: '⌘',
|
darwin: '⌘',
|
||||||
win32: 'Win',
|
win32: 'Win',
|
||||||
@@ -10,72 +11,67 @@ export const altKeyName = {
|
|||||||
linux: 'Alt',
|
linux: 'Alt',
|
||||||
}[process.platform]
|
}[process.platform]
|
||||||
|
|
||||||
export interface EventData {
|
export interface KeyEventData {
|
||||||
ctrlKey: boolean
|
ctrlKey?: boolean
|
||||||
metaKey: boolean
|
metaKey?: boolean
|
||||||
altKey: boolean
|
altKey?: boolean
|
||||||
shiftKey: boolean
|
shiftKey?: boolean
|
||||||
key: string
|
key: string
|
||||||
code: string
|
code: string
|
||||||
eventName: string
|
eventName: string
|
||||||
time: number
|
time: number
|
||||||
|
registrationTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
|
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
|
||||||
|
|
||||||
export function stringifyKeySequence (events: EventData[]): string[] {
|
export type KeyName = string
|
||||||
const items: string[] = []
|
export type Keystroke = string
|
||||||
events = events.slice()
|
|
||||||
|
|
||||||
while (events.length > 0) {
|
export function getKeyName (event: KeyEventData): KeyName {
|
||||||
const event = events.shift()!
|
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||||
if (event.eventName === 'keydown') {
|
let key: string
|
||||||
const itemKeys: string[] = []
|
if (event.key === 'Control') {
|
||||||
if (event.ctrlKey) {
|
key = 'Ctrl'
|
||||||
itemKeys.push('Ctrl')
|
} else if (event.key === 'Meta') {
|
||||||
}
|
key = metaKeyName
|
||||||
if (event.metaKey) {
|
} else if (event.key === 'Alt') {
|
||||||
itemKeys.push(metaKeyName)
|
key = altKeyName
|
||||||
}
|
} else if (event.key === 'Shift') {
|
||||||
if (event.altKey) {
|
key = 'Shift'
|
||||||
itemKeys.push(altKeyName)
|
} else {
|
||||||
}
|
key = event.code
|
||||||
if (event.shiftKey) {
|
if (REGEX_LATIN_KEYNAME.test(event.key)) {
|
||||||
itemKeys.push('Shift')
|
// Handle Dvorak etc via the reported "character" instead of the scancode
|
||||||
}
|
key = event.key.toUpperCase()
|
||||||
|
} else {
|
||||||
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
|
key = key.replace('Key', '')
|
||||||
// TODO make this optional?
|
key = key.replace('Arrow', '')
|
||||||
continue
|
key = key.replace('Digit', '')
|
||||||
}
|
key = {
|
||||||
|
Comma: ',',
|
||||||
let key = event.code
|
Period: '.',
|
||||||
if (REGEX_LATIN_KEYNAME.test(event.key)) {
|
Slash: '/',
|
||||||
// Handle Dvorak etc via the reported "character" instead of the scancode
|
Backslash: '\\',
|
||||||
key = event.key.toUpperCase()
|
IntlBackslash: '`',
|
||||||
} else {
|
Backquote: '~', // Electron says it's the tilde
|
||||||
key = key.replace('Key', '')
|
Minus: '-',
|
||||||
key = key.replace('Arrow', '')
|
Equal: '=',
|
||||||
key = key.replace('Digit', '')
|
Semicolon: ';',
|
||||||
key = {
|
Quote: '\'',
|
||||||
Comma: ',',
|
BracketLeft: '[',
|
||||||
Period: '.',
|
BracketRight: ']',
|
||||||
Slash: '/',
|
}[key] ?? key
|
||||||
Backslash: '\\',
|
|
||||||
IntlBackslash: '`',
|
|
||||||
Backquote: '~', // Electron says it's the tilde
|
|
||||||
Minus: '-',
|
|
||||||
Equal: '=',
|
|
||||||
Semicolon: ';',
|
|
||||||
Quote: '\'',
|
|
||||||
BracketLeft: '[',
|
|
||||||
BracketRight: ']',
|
|
||||||
}[key] ?? key
|
|
||||||
}
|
|
||||||
|
|
||||||
itemKeys.push(key)
|
|
||||||
items.push(itemKeys.join('-'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeystrokeName (keys: KeyName[]): Keystroke {
|
||||||
|
const strictOrdering: KeyName[] = ['Ctrl', metaKeyName, altKeyName, 'Shift']
|
||||||
|
keys = [
|
||||||
|
...strictOrdering.map(x => keys.find(k => k === x)).filter(x => !!x) as KeyName[],
|
||||||
|
...keys.filter(k => !strictOrdering.includes(k)),
|
||||||
|
]
|
||||||
|
return keys.join('-')
|
||||||
}
|
}
|
||||||
|
@@ -42,7 +42,7 @@ export class ProfilesService {
|
|||||||
tab.setTitle(profile.name)
|
tab.setTitle(profile.name)
|
||||||
}
|
}
|
||||||
if (profile.disableDynamicTitle) {
|
if (profile.disableDynamicTitle) {
|
||||||
tab['enableDynamicTitle'] = false
|
tab['disableDynamicTitle'] = true
|
||||||
}
|
}
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
@@ -84,8 +84,9 @@ export class ProfilesService {
|
|||||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||||
return {
|
return {
|
||||||
icon: profile.icon,
|
|
||||||
name: profile.group ? `${fullProfile.group} / ${fullProfile.name}` : fullProfile.name,
|
name: profile.group ? `${fullProfile.group} / ${fullProfile.name}` : fullProfile.name,
|
||||||
|
icon: profile.icon,
|
||||||
|
color: profile.color,
|
||||||
description: this.providerForProfile(fullProfile)?.getDescription(fullProfile),
|
description: this.providerForProfile(fullProfile)?.getDescription(fullProfile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,11 +94,13 @@ export class ProfilesService {
|
|||||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||||
return new Promise<PartialProfile<Profile>|null>(async (resolve, reject) => {
|
return new Promise<PartialProfile<Profile>|null>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const recentProfiles: PartialProfile<Profile>[] = this.config.store.recentProfiles
|
let recentProfiles: PartialProfile<Profile>[] = this.config.store.recentProfiles
|
||||||
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
|
||||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||||
...this.selectorOptionForProfile(p),
|
...this.selectorOptionForProfile(p),
|
||||||
icon: 'fas fa-history',
|
icon: 'fas fa-history',
|
||||||
|
color: p.color,
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
if (p.id) {
|
if (p.id) {
|
||||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||||
|
@@ -20,7 +20,7 @@ export class TabRecoveryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveTabs (tabs: BaseTabComponent[]): Promise<void> {
|
async saveTabs (tabs: BaseTabComponent[]): Promise<void> {
|
||||||
if (!this.enabled) {
|
if (!this.enabled || !this.config.store.recoverTabs) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.localStorage.tabsRecovery = JSON.stringify(
|
window.localStorage.tabsRecovery = JSON.stringify(
|
||||||
@@ -34,9 +34,11 @@ export class TabRecoveryService {
|
|||||||
const token = await tab.getRecoveryToken()
|
const token = await tab.getRecoveryToken()
|
||||||
if (token) {
|
if (token) {
|
||||||
token.tabTitle = tab.title
|
token.tabTitle = tab.title
|
||||||
|
token.tabCustomTitle = tab.customTitle
|
||||||
if (tab.color) {
|
if (tab.color) {
|
||||||
token.tabColor = tab.color
|
token.tabColor = tab.color
|
||||||
}
|
}
|
||||||
|
token.disableDynamicTitle = tab['disableDynamicTitle']
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
@@ -54,6 +56,8 @@ export class TabRecoveryService {
|
|||||||
tab.inputs = tab.inputs ?? {}
|
tab.inputs = tab.inputs ?? {}
|
||||||
tab.inputs.color = token.tabColor ?? null
|
tab.inputs.color = token.tabColor ?? null
|
||||||
tab.inputs.title = token.tabTitle || ''
|
tab.inputs.title = token.tabTitle || ''
|
||||||
|
tab.inputs.customTitle = token.tabCustomTitle || ''
|
||||||
|
tab.inputs.disableDynamicTitle = token.disableDynamicTitle
|
||||||
return tab
|
return tab
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||||
|
@@ -2,7 +2,6 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
|||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { TabRecoveryService } from './tabRecovery.service'
|
import { TabRecoveryService } from './tabRecovery.service'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-type-alias
|
|
||||||
export interface TabComponentType<T extends BaseTabComponent> {
|
export interface TabComponentType<T extends BaseTabComponent> {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||||
new (...args: any[]): T
|
new (...args: any[]): T
|
||||||
|
@@ -26,7 +26,7 @@ interface StoredVault {
|
|||||||
|
|
||||||
export interface VaultSecret {
|
export interface VaultSecret {
|
||||||
type: string
|
type: string
|
||||||
key: Record<string, any>
|
key: VaultSecretKey
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ export interface Vault {
|
|||||||
secrets: VaultSecret[]
|
secrets: VaultSecret[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface VaultSecretKey { }
|
||||||
|
|
||||||
function migrateVaultContent (content: any): Vault {
|
function migrateVaultContent (content: any): Vault {
|
||||||
return {
|
return {
|
||||||
config: content.config,
|
config: content.config,
|
||||||
@@ -184,7 +187,7 @@ export class VaultService {
|
|||||||
return _rememberedPassphrase!
|
return _rememberedPassphrase!
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSecret (type: string, key: Record<string, any>): Promise<VaultSecret|null> {
|
async getSecret (type: string, key: VaultSecretKey): Promise<VaultSecret|null> {
|
||||||
await this.ready$.toPromise()
|
await this.ready$.toPromise()
|
||||||
const vault = await this.load()
|
const vault = await this.load()
|
||||||
if (!vault) {
|
if (!vault) {
|
||||||
@@ -218,7 +221,7 @@ export class VaultService {
|
|||||||
await this.save(vault)
|
await this.save(vault)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeSecret (type: string, key: Record<string, any>): Promise<void> {
|
async removeSecret (type: string, key: VaultSecretKey): Promise<void> {
|
||||||
await this.ready$.toPromise()
|
await this.ready$.toPromise()
|
||||||
const vault = await this.load()
|
const vault = await this.load()
|
||||||
if (!vault) {
|
if (!vault) {
|
||||||
@@ -228,7 +231,7 @@ export class VaultService {
|
|||||||
await this.save(vault)
|
await this.save(vault)
|
||||||
}
|
}
|
||||||
|
|
||||||
private keyMatches (key: Record<string, any>, secret: VaultSecret): boolean {
|
private keyMatches (key: VaultSecretKey, secret: VaultSecret): boolean {
|
||||||
return Object.keys(key).every(k => secret.key[k] === key[k])
|
return Object.keys(key).every(k => secret.key[k] === key[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,9 +270,9 @@ export class VaultFileProvider extends FileProvider {
|
|||||||
if (!vault) {
|
if (!vault) {
|
||||||
throw new Error('Vault is locked')
|
throw new Error('Vault is locked')
|
||||||
}
|
}
|
||||||
const files = vault.secrets.filter(x => x.type === VAULT_SECRET_TYPE_FILE)
|
const files = vault.secrets.filter(x => x.type === VAULT_SECRET_TYPE_FILE) as VaultFileSecret[]
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
const result = await this.selector.show<VaultSecret|null>('Select file', [
|
const result = await this.selector.show<VaultFileSecret|null>('Select file', [
|
||||||
{
|
{
|
||||||
name: 'Add a new file',
|
name: 'Add a new file',
|
||||||
icon: 'fas fa-plus',
|
icon: 'fas fa-plus',
|
||||||
|
@@ -13,6 +13,7 @@ import { TabsService } from './services/tabs.service'
|
|||||||
import { HotkeysService } from './services/hotkeys.service'
|
import { HotkeysService } from './services/hotkeys.service'
|
||||||
import { PromptModalComponent } from './components/promptModal.component'
|
import { PromptModalComponent } from './components/promptModal.component'
|
||||||
import { SplitLayoutProfilesService } from './profiles'
|
import { SplitLayoutProfilesService } from './profiles'
|
||||||
|
import { TAB_COLORS } from './utils'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@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 */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||||
@@ -127,8 +118,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Color',
|
label: 'Color',
|
||||||
sublabel: COLORS.find(x => x.value === tab.color)?.name,
|
sublabel: TAB_COLORS.find(x => x.value === tab.color)?.name,
|
||||||
submenu: COLORS.map(color => ({
|
submenu: TAB_COLORS.map(color => ({
|
||||||
label: color.name,
|
label: color.name,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: tab.color === color.value,
|
checked: tab.color === color.value,
|
||||||
|
@@ -90,10 +90,6 @@ $list-group-border-radius: 0;
|
|||||||
$pre-bg: $dropdown-bg;
|
$pre-bg: $dropdown-bg;
|
||||||
$pre-color: $dropdown-link-color;
|
$pre-color: $dropdown-link-color;
|
||||||
|
|
||||||
$alert-danger-bg: $body-bg;
|
|
||||||
$alert-danger-text: $red;
|
|
||||||
$alert-danger-border: $red;
|
|
||||||
|
|
||||||
$headings-font-weight: lighter;
|
$headings-font-weight: lighter;
|
||||||
$headings-color: $base0;
|
$headings-color: $base0;
|
||||||
|
|
||||||
|
@@ -230,24 +230,24 @@ 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;
|
background: transparent;
|
||||||
border-color: rgba(0, 0, 0, 0.2);
|
border-color: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.list-group-item-action {
|
||||||
|
&:hover, &.active {
|
||||||
|
background: $list-group-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-light {
|
.list-group-light {
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid rgba(255, 255, 255, .05);
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.list-group-item-action {
|
&.list-group-item-action {
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
|
@@ -26,8 +26,8 @@ $purple: #613d7c !default;
|
|||||||
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
||||||
$content-bg-solid: #1D272D;
|
$content-bg-solid: #1D272D;
|
||||||
|
|
||||||
$table-bg: rgba(255,255,255,.05);
|
$table-bg: rgba(255,255,255,.025);
|
||||||
$table-bg-hover: rgba(255,255,255,.1);
|
$table-bg-hover: rgba(255,255,255,.05);
|
||||||
$table-border-color: rgba(255,255,255,.1);
|
$table-border-color: rgba(255,255,255,.1);
|
||||||
|
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
@@ -88,7 +88,7 @@ $list-group-item-padding-y: 0.8rem;
|
|||||||
$list-group-item-padding-x: 1rem;
|
$list-group-item-padding-x: 1rem;
|
||||||
|
|
||||||
$list-group-hover-bg: $table-bg-hover;
|
$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-color: $component-active-color;
|
||||||
$list-group-active-border-color: translate;
|
$list-group-active-border-color: translate;
|
||||||
|
|
||||||
@@ -193,3 +193,7 @@ $modal-content-border-width: 0;
|
|||||||
|
|
||||||
$progress-bg: $table-bg;
|
$progress-bg: $table-bg;
|
||||||
$progress-height: 3px;
|
$progress-height: 3px;
|
||||||
|
|
||||||
|
$alert-bg-level: 9;
|
||||||
|
$alert-border-level: 5;
|
||||||
|
$alert-color-level: -5;
|
||||||
|
@@ -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"
|
function-bind "^1.1.1"
|
||||||
get-intrinsic "^1.0.2"
|
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:
|
debug@4:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||||
@@ -374,11 +369,6 @@ ms@2.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
ng2-dnd@^5.0.2:
|
|
||||||
version "5.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/ng2-dnd/-/ng2-dnd-5.0.2.tgz#862278ac7dedfa14f5783bbf34014d5d73dfefb4"
|
|
||||||
integrity sha512-5mWWBePwvEPsNd/HkdbD543Q9mPyJofL6zkNydl8/Ah3qrrvZT2DaEPbknY08OgkXpI2qUGksc01OzzVlRQ9dQ==
|
|
||||||
|
|
||||||
ngx-filesize@^2.0.16:
|
ngx-filesize@^2.0.16:
|
||||||
version "2.0.16"
|
version "2.0.16"
|
||||||
resolved "https://registry.yarnpkg.com/ngx-filesize/-/ngx-filesize-2.0.16.tgz#fdaba04170edb6cfcdf7be932783cf913b03f016"
|
resolved "https://registry.yarnpkg.com/ngx-filesize/-/ngx-filesize-2.0.16.tgz#fdaba04170edb6cfcdf7be932783cf913b03f016"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-electron",
|
"name": "tabby-electron",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Electron-specific bindings",
|
"description": "Electron-specific bindings",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"@angular/core": "^9.1.9"
|
"@angular/core": "^9.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"winston": "^3.3.3",
|
"electron-promise-ipc": "^2.2.4",
|
||||||
"electron-promise-ipc": "^2.2.4"
|
"tmp-promise": "^3.0.2",
|
||||||
|
"winston": "^3.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
|
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core'
|
||||||
import { TerminalColorSchemeProvider } from 'tabby-terminal'
|
import { TerminalColorSchemeProvider } from 'tabby-terminal'
|
||||||
|
import { SFTPContextMenuItemProvider } from 'tabby-ssh'
|
||||||
|
|
||||||
import { HyperColorSchemes } from './colorSchemes'
|
import { HyperColorSchemes } from './colorSchemes'
|
||||||
import { ElectronPlatformService } from './services/platform.service'
|
import { ElectronPlatformService } from './services/platform.service'
|
||||||
@@ -14,11 +15,12 @@ import { ElectronHostAppService } from './services/hostApp.service'
|
|||||||
import { ElectronService } from './services/electron.service'
|
import { ElectronService } from './services/electron.service'
|
||||||
import { ElectronHotkeyProvider } from './hotkeys'
|
import { ElectronHotkeyProvider } from './hotkeys'
|
||||||
import { ElectronConfigProvider } from './config'
|
import { ElectronConfigProvider } from './config'
|
||||||
|
import { EditSFTPContextMenu } from './sftpContextMenu'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||||
{ provide: PlatformService, useClass: ElectronPlatformService },
|
{ provide: PlatformService, useExisting: ElectronPlatformService },
|
||||||
{ provide: HostWindowService, useExisting: ElectronHostWindow },
|
{ provide: HostWindowService, useExisting: ElectronHostWindow },
|
||||||
{ provide: HostAppService, useExisting: ElectronHostAppService },
|
{ provide: HostAppService, useExisting: ElectronHostAppService },
|
||||||
{ provide: LogService, useClass: ElectronLogService },
|
{ provide: LogService, useClass: ElectronLogService },
|
||||||
@@ -27,6 +29,7 @@ import { ElectronConfigProvider } from './config'
|
|||||||
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
|
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
|
||||||
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
|
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
|
||||||
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
|
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
|
||||||
|
{ provide: SFTPContextMenuItemProvider, useClass: EditSFTPContextMenu, multi: true },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class ElectronModule {
|
export default class ElectronModule {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage } 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'
|
import * as remote from '@electron/remote'
|
||||||
|
|
||||||
export interface MessageBoxResponse {
|
export interface MessageBoxResponse {
|
||||||
@@ -15,11 +15,11 @@ export class ElectronService {
|
|||||||
dialog: Dialog
|
dialog: Dialog
|
||||||
clipboard: Clipboard
|
clipboard: Clipboard
|
||||||
globalShortcut: GlobalShortcut
|
globalShortcut: GlobalShortcut
|
||||||
nativeImage: typeof NativeImage
|
|
||||||
screen: Screen
|
screen: Screen
|
||||||
remote: Remote
|
remote: Remote
|
||||||
process: any
|
process: any
|
||||||
autoUpdater: AutoUpdater
|
autoUpdater: AutoUpdater
|
||||||
|
powerSaveBlocker: PowerSaveBlocker
|
||||||
TouchBar: typeof TouchBar
|
TouchBar: typeof TouchBar
|
||||||
BrowserWindow: typeof BrowserWindow
|
BrowserWindow: typeof BrowserWindow
|
||||||
Menu: typeof Menu
|
Menu: typeof Menu
|
||||||
@@ -37,8 +37,8 @@ export class ElectronService {
|
|||||||
this.screen = remote.screen
|
this.screen = remote.screen
|
||||||
this.dialog = remote.dialog
|
this.dialog = remote.dialog
|
||||||
this.globalShortcut = remote.globalShortcut
|
this.globalShortcut = remote.globalShortcut
|
||||||
this.nativeImage = remote.nativeImage
|
|
||||||
this.autoUpdater = remote.autoUpdater
|
this.autoUpdater = remote.autoUpdater
|
||||||
|
this.powerSaveBlocker = remote.powerSaveBlocker
|
||||||
this.TouchBar = remote.TouchBar
|
this.TouchBar = remote.TouchBar
|
||||||
this.BrowserWindow = remote.BrowserWindow
|
this.BrowserWindow = remote.BrowserWindow
|
||||||
this.Menu = remote.Menu
|
this.Menu = remote.Menu
|
||||||
|
@@ -12,9 +12,9 @@ export interface Bounds {
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ElectronHostWindow extends HostWindowService {
|
export class ElectronHostWindow extends HostWindowService {
|
||||||
get isFullscreen (): boolean { return this._isFullScreen}
|
get isFullscreen (): boolean { return this._isFullscreen }
|
||||||
|
|
||||||
private _isFullScreen = false
|
private _isFullscreen = false
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
zone: NgZone,
|
zone: NgZone,
|
||||||
@@ -23,28 +23,26 @@ export class ElectronHostWindow extends HostWindowService {
|
|||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => {
|
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(() => {
|
electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => {
|
||||||
this._isFullScreen = false
|
this._isFullscreen = false
|
||||||
}))
|
}))
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:window-shown', () => {
|
electron.ipcRenderer.on('host:window-shown', () => zone.run(() => this.windowShown.next()))
|
||||||
zone.run(() => this.windowShown.next())
|
|
||||||
})
|
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:window-close-request', () => {
|
electron.ipcRenderer.on('host:window-close-request', () => zone.run(() => {
|
||||||
zone.run(() => this.windowCloseRequest.next())
|
this.windowCloseRequest.next()
|
||||||
})
|
}))
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:window-moved', () => {
|
electron.ipcRenderer.on('host:window-moved', () => zone.run(() => {
|
||||||
zone.run(() => this.windowMoved.next())
|
this.windowMoved.next()
|
||||||
})
|
}))
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:window-focused', () => {
|
electron.ipcRenderer.on('host:window-focused', () => zone.run(() => {
|
||||||
zone.run(() => this.windowFocused.next())
|
this.windowFocused.next()
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindow (): BrowserWindow {
|
getWindow (): BrowserWindow {
|
||||||
@@ -64,7 +62,7 @@ export class ElectronHostWindow extends HostWindowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleFullscreen (): void {
|
toggleFullscreen (): void {
|
||||||
this.getWindow().setFullScreen(!this._isFullScreen)
|
this.getWindow().setFullScreen(!this._isFullscreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
minimize (): void {
|
minimize (): void {
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs/promises'
|
import * as fs from 'fs/promises'
|
||||||
|
import * as gracefulFS from 'graceful-fs'
|
||||||
import * as fsSync from 'fs'
|
import * as fsSync from 'fs'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
import { promisify } from 'util'
|
||||||
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
||||||
import { execFile } from 'mz/child_process'
|
import { execFile } from 'mz/child_process'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
@@ -20,10 +22,11 @@ try {
|
|||||||
var wnr = require('windows-native-registry')
|
var wnr = require('windows-native-registry')
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ElectronPlatformService extends PlatformService {
|
export class ElectronPlatformService extends PlatformService {
|
||||||
supportsWindowControls = true
|
supportsWindowControls = true
|
||||||
private configPath: string
|
private configPath: string
|
||||||
|
private _configSaveInProgress = Promise.resolve()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
@@ -107,7 +110,17 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveConfig (content: string): Promise<void> {
|
async saveConfig (content: string): Promise<void> {
|
||||||
await fs.writeFile(this.configPath, content, 'utf8')
|
try {
|
||||||
|
await this._configSaveInProgress
|
||||||
|
} catch { }
|
||||||
|
this._configSaveInProgress = this._saveConfigInternal(content)
|
||||||
|
await this._configSaveInProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
async _saveConfigInternal (content: string): Promise<void> {
|
||||||
|
const tempPath = this.configPath + '.new'
|
||||||
|
await fs.writeFile(tempPath, content, 'utf8')
|
||||||
|
await promisify(gracefulFS.rename)(tempPath, this.configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfigPath (): string|null {
|
getConfigPath (): string|null {
|
||||||
@@ -178,7 +191,7 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
this.electron.app.exit(0)
|
this.electron.app.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
|
async startUpload (options?: FileUploadOptions, paths?: string[]): Promise<FileUpload[]> {
|
||||||
options ??= { multiple: false }
|
options ??= { multiple: false }
|
||||||
|
|
||||||
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
|
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
|
||||||
@@ -186,36 +199,42 @@ export class ElectronPlatformService extends PlatformService {
|
|||||||
properties.push('multiSelections')
|
properties.push('multiSelections')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.electron.dialog.showOpenDialog(
|
if (!paths) {
|
||||||
this.hostWindow.getWindow(),
|
const result = await this.electron.dialog.showOpenDialog(
|
||||||
{
|
this.hostWindow.getWindow(),
|
||||||
buttonLabel: 'Select',
|
{
|
||||||
properties,
|
buttonLabel: 'Select',
|
||||||
},
|
properties,
|
||||||
)
|
},
|
||||||
if (result.canceled) {
|
)
|
||||||
return []
|
if (result.canceled) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
paths = result.filePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(result.filePaths.map(async p => {
|
return Promise.all(paths.map(async p => {
|
||||||
const transfer = new ElectronFileUpload(p)
|
const transfer = new ElectronFileUpload(p, this.electron)
|
||||||
await wrapPromise(this.zone, transfer.open())
|
await wrapPromise(this.zone, transfer.open())
|
||||||
this.fileTransferStarted.next(transfer)
|
this.fileTransferStarted.next(transfer)
|
||||||
return transfer
|
return transfer
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async startDownload (name: string, mode: number, size: number): Promise<FileDownload|null> {
|
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {
|
||||||
const result = await this.electron.dialog.showSaveDialog(
|
if (!filePath) {
|
||||||
this.hostWindow.getWindow(),
|
const result = await this.electron.dialog.showSaveDialog(
|
||||||
{
|
this.hostWindow.getWindow(),
|
||||||
defaultPath: name,
|
{
|
||||||
},
|
defaultPath: name,
|
||||||
)
|
},
|
||||||
if (!result.filePath) {
|
)
|
||||||
return null
|
if (!result.filePath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
filePath = result.filePath
|
||||||
}
|
}
|
||||||
const transfer = new ElectronFileDownload(result.filePath, mode, size)
|
const transfer = new ElectronFileDownload(filePath, mode, size, this.electron)
|
||||||
await wrapPromise(this.zone, transfer.open())
|
await wrapPromise(this.zone, transfer.open())
|
||||||
this.fileTransferStarted.next(transfer)
|
this.fileTransferStarted.next(transfer)
|
||||||
return transfer
|
return transfer
|
||||||
@@ -233,10 +252,12 @@ class ElectronFileUpload extends FileUpload {
|
|||||||
private mode: number
|
private mode: number
|
||||||
private file: fs.FileHandle
|
private file: fs.FileHandle
|
||||||
private buffer: Buffer
|
private buffer: Buffer
|
||||||
|
private powerSaveBlocker = 0
|
||||||
|
|
||||||
constructor (private filePath: string) {
|
constructor (private filePath: string, private electron: ElectronService) {
|
||||||
super()
|
super()
|
||||||
this.buffer = Buffer.alloc(256 * 1024)
|
this.buffer = Buffer.alloc(256 * 1024)
|
||||||
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||||
}
|
}
|
||||||
|
|
||||||
async open (): Promise<void> {
|
async open (): Promise<void> {
|
||||||
@@ -265,19 +286,23 @@ class ElectronFileUpload extends FileUpload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close (): void {
|
close (): void {
|
||||||
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||||
this.file.close()
|
this.file.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ElectronFileDownload extends FileDownload {
|
class ElectronFileDownload extends FileDownload {
|
||||||
private file: fs.FileHandle
|
private file: fs.FileHandle
|
||||||
|
private powerSaveBlocker = 0
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private filePath: string,
|
private filePath: string,
|
||||||
private mode: number,
|
private mode: number,
|
||||||
private size: number,
|
private size: number,
|
||||||
|
private electron: ElectronService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
||||||
}
|
}
|
||||||
|
|
||||||
async open (): Promise<void> {
|
async open (): Promise<void> {
|
||||||
@@ -306,6 +331,7 @@ class ElectronFileDownload extends FileDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close (): void {
|
close (): void {
|
||||||
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||||
this.file.close()
|
this.file.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,56 +1,29 @@
|
|||||||
import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
|
import { ipcRenderer } from 'electron'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { AppService, HostAppService, Platform } from 'tabby-core'
|
import { AppService, HostAppService, Platform } from 'tabby-core'
|
||||||
import { ElectronService } from '../services/electron.service'
|
|
||||||
import { ElectronHostWindow } from './hostWindow.service'
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TouchbarService {
|
export class TouchbarService {
|
||||||
private tabsSegmentedControl: TouchBarSegmentedControl
|
|
||||||
private tabSegments: SegmentedControlSegment[] = []
|
|
||||||
|
|
||||||
private constructor (
|
private constructor (
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private hostWindow: ElectronHostWindow,
|
|
||||||
private electron: ElectronService,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
) {
|
) {
|
||||||
if (this.hostApp.platform !== Platform.macOS) {
|
if (this.hostApp.platform !== Platform.macOS) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.tabsChanged$.subscribe(() => this.updateTabs())
|
app.tabsChanged$.subscribe(() => this.update())
|
||||||
app.activeTabChange$.subscribe(() => this.updateTabs())
|
app.activeTabChange$.subscribe(() => this.update())
|
||||||
|
|
||||||
const activityIconPath = `${electron.app.getAppPath()}/assets/activity.png`
|
|
||||||
const activityIcon = this.electron.nativeImage.createFromPath(activityIconPath)
|
|
||||||
app.tabOpened$.subscribe(tab => {
|
app.tabOpened$.subscribe(tab => {
|
||||||
tab.titleChange$.subscribe(title => {
|
tab.titleChange$.subscribe(() => this.update())
|
||||||
const segment = this.tabSegments[app.tabs.indexOf(tab)]
|
tab.activity$.subscribe(() => this.update())
|
||||||
// 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
updateTabs (): void {
|
ipcRenderer.on('touchbar-selection', (_event, index) => this.zone.run(() => {
|
||||||
this.tabSegments = this.app.tabs.map(tab => ({
|
this.app.selectTab(this.app.tabs[index])
|
||||||
label: this.shortenTitle(tab.title),
|
|
||||||
}))
|
}))
|
||||||
this.tabsSegmentedControl.segments = this.tabSegments
|
|
||||||
this.tabsSegmentedControl.selectedIndex = this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update (): void {
|
update (): void {
|
||||||
@@ -58,20 +31,12 @@ export class TouchbarService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
|
const tabSegments = this.app.tabs.map(tab => ({
|
||||||
segments: this.tabSegments,
|
label: this.shortenTitle(tab.title),
|
||||||
selectedIndex: this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined,
|
hasActivity: this.app.activeTab !== tab && tab.hasActivity,
|
||||||
change: (selectedIndex) => this.zone.run(() => {
|
}))
|
||||||
this.app.selectTab(this.app.tabs[selectedIndex])
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const touchBar = new this.electron.TouchBar({
|
ipcRenderer.send('window-set-touch-bar', tabSegments, this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined)
|
||||||
items: [
|
|
||||||
this.tabsSegmentedControl,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
this.hostWindow.setTouchBar(touchBar)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private shortenTitle (title: string): string {
|
private shortenTitle (title: string): string {
|
||||||
|
@@ -126,10 +126,11 @@ export class ElectronUpdaterService extends UpdaterService {
|
|||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Installing the update will close all tabs and restart Tabby.',
|
message: 'Installing the update will close all tabs and restart Tabby.',
|
||||||
buttons: ['Cancel', 'Update'],
|
buttons: ['Update', 'Cancel'],
|
||||||
defaultId: 1,
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
}
|
}
|
||||||
)).response === 1) {
|
)).response === 0) {
|
||||||
await this.downloaded
|
await this.downloaded
|
||||||
this.electron.autoUpdater.quitAndInstall()
|
this.electron.autoUpdater.quitAndInstall()
|
||||||
}
|
}
|
||||||
|
59
tabby-electron/src/sftpContextMenu.ts
Normal file
59
tabby-electron/src/sftpContextMenu.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as tmp from 'tmp-promise'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import { Subject, debounceTime, debounce } from 'rxjs'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { MenuItemOptions } from 'tabby-core'
|
||||||
|
import { SFTPFile, SFTPPanelComponent, SFTPContextMenuItemProvider, SFTPSession } from 'tabby-ssh'
|
||||||
|
import { ElectronPlatformService } from './services/platform.service'
|
||||||
|
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Injectable()
|
||||||
|
export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||||
|
weight = 0
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private platform: ElectronPlatformService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||||
|
if (item.isDirectory) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
click: () => this.edit(item, panel.sftp),
|
||||||
|
label: 'Edit locally',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private async edit (item: SFTPFile, sftp: SFTPSession) {
|
||||||
|
const tempDir = (await tmp.dir({ unsafeCleanup: true })).path
|
||||||
|
const tempPath = path.join(tempDir, item.name)
|
||||||
|
const transfer = await this.platform.startDownload(item.name, item.mode, item.size, tempPath)
|
||||||
|
if (!transfer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sftp.download(item.fullPath, transfer)
|
||||||
|
this.platform.openPath(tempPath)
|
||||||
|
|
||||||
|
const events = new Subject<string>()
|
||||||
|
const watcher = fs.watch(tempPath, event => events.next(event))
|
||||||
|
events.pipe(debounceTime(1000), debounce(async event => {
|
||||||
|
if (event === 'rename') {
|
||||||
|
watcher.close()
|
||||||
|
}
|
||||||
|
const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
|
||||||
|
if (!upload.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sftp.upload(item.fullPath, upload[0])
|
||||||
|
})).subscribe()
|
||||||
|
watcher.on('close', () => events.complete())
|
||||||
|
sftp.closed$.subscribe(() => watcher.close())
|
||||||
|
}
|
||||||
|
}
|
@@ -16,6 +16,19 @@ async@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||||
|
|
||||||
|
balanced-match@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
brace-expansion@^1.1.7:
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
|
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^1.0.0"
|
||||||
|
concat-map "0.0.1"
|
||||||
|
|
||||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
||||||
@@ -70,6 +83,11 @@ colorspace@1.1.x:
|
|||||||
color "3.0.x"
|
color "3.0.x"
|
||||||
text-hex "1.0.x"
|
text-hex "1.0.x"
|
||||||
|
|
||||||
|
concat-map@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
core-util-is@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
@@ -143,6 +161,11 @@ fn.name@1.x.x:
|
|||||||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||||
|
|
||||||
|
fs.realpath@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
|
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||||
|
|
||||||
function-bind@^1.1.1:
|
function-bind@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||||
@@ -157,6 +180,18 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
has-symbols "^1.0.1"
|
has-symbols "^1.0.1"
|
||||||
|
|
||||||
|
glob@^7.1.3:
|
||||||
|
version "7.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
|
||||||
|
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
|
||||||
|
dependencies:
|
||||||
|
fs.realpath "^1.0.0"
|
||||||
|
inflight "^1.0.4"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
once "^1.3.0"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
has-bigints@^1.0.1:
|
has-bigints@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
||||||
@@ -174,7 +209,15 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
inherits@^2.0.3, inherits@~2.0.3:
|
inflight@^1.0.4:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||||
|
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||||
|
dependencies:
|
||||||
|
once "^1.3.0"
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
|
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
@@ -267,6 +310,13 @@ logform@^2.2.0:
|
|||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
triple-beam "^1.3.0"
|
triple-beam "^1.3.0"
|
||||||
|
|
||||||
|
minimatch@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
|
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||||
|
dependencies:
|
||||||
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
ms@^2.1.1:
|
ms@^2.1.1:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
@@ -302,6 +352,13 @@ object.entries@^1.1.3:
|
|||||||
es-abstract "^1.18.0-next.1"
|
es-abstract "^1.18.0-next.1"
|
||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
|
|
||||||
|
once@^1.3.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
|
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||||
|
dependencies:
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
one-time@^1.0.0:
|
one-time@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
|
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
|
||||||
@@ -309,6 +366,11 @@ one-time@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fn.name "1.x.x"
|
fn.name "1.x.x"
|
||||||
|
|
||||||
|
path-is-absolute@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
|
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||||
|
|
||||||
process-nextick-args@~2.0.0:
|
process-nextick-args@~2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||||
@@ -336,6 +398,13 @@ readable-stream@^3.4.0:
|
|||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
|
rimraf@^3.0.0:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
@@ -400,6 +469,20 @@ text-hex@1.0.x:
|
|||||||
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
||||||
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
||||||
|
|
||||||
|
tmp-promise@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a"
|
||||||
|
integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==
|
||||||
|
dependencies:
|
||||||
|
tmp "^0.2.0"
|
||||||
|
|
||||||
|
tmp@^0.2.0:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||||
|
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||||
|
dependencies:
|
||||||
|
rimraf "^3.0.0"
|
||||||
|
|
||||||
triple-beam@^1.2.0, triple-beam@^1.3.0:
|
triple-beam@^1.2.0, triple-beam@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
||||||
@@ -463,3 +546,8 @@ winston@^3.3.3:
|
|||||||
stack-trace "0.0.x"
|
stack-trace "0.0.x"
|
||||||
triple-beam "^1.3.0"
|
triple-beam "^1.3.0"
|
||||||
winston-transport "^4.4.0"
|
winston-transport "^4.4.0"
|
||||||
|
|
||||||
|
wrappy@1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-local",
|
"name": "tabby-local",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Tabby's local shell plugin",
|
"description": "Tabby's local shell plugin",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
"author": "Eugene Pankov",
|
"author": "Eugene Pankov",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hterm-umdjs": "1.4.1",
|
|
||||||
"opentype.js": "^1.3.3"
|
"opentype.js": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -106,10 +106,11 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
|
|||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `"${children[0].command}" is still running. Close?`,
|
message: `"${children[0].command}" is still running. Close?`,
|
||||||
buttons: ['Cancel', 'Kill'],
|
buttons: ['Kill', 'Cancel'],
|
||||||
defaultId: 1,
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
}
|
}
|
||||||
)).response === 1
|
)).response === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy (): void {
|
ngOnDestroy (): void {
|
||||||
|
@@ -3,9 +3,6 @@ import { ConfigProvider, Platform } from 'tabby-core'
|
|||||||
/** @hidden */
|
/** @hidden */
|
||||||
export class TerminalConfigProvider extends ConfigProvider {
|
export class TerminalConfigProvider extends ConfigProvider {
|
||||||
defaults = {
|
defaults = {
|
||||||
hotkeys: {
|
|
||||||
'copy-current-path': [],
|
|
||||||
},
|
|
||||||
terminal: {
|
terminal: {
|
||||||
autoOpen: false,
|
autoOpen: false,
|
||||||
useConPTY: true,
|
useConPTY: true,
|
||||||
|
@@ -84,6 +84,10 @@ export class LocalProfilesService extends ProfileProvider<LocalProfile> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSuggestedName (profile: LocalProfile): string {
|
||||||
|
return this.getDescription(profile)
|
||||||
|
}
|
||||||
|
|
||||||
getDescription (profile: PartialProfile<LocalProfile>): string {
|
getDescription (profile: PartialProfile<LocalProfile>): string {
|
||||||
return profile.options?.command ?? ''
|
return profile.options?.command ?? ''
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ export class TerminalService {
|
|||||||
if (!profile) {
|
if (!profile) {
|
||||||
profile = profiles.filter(x => x.type === 'local' && x.isBuiltin)[0]
|
profile = profiles.filter(x => x.type === 'local' && x.isBuiltin)[0]
|
||||||
}
|
}
|
||||||
return profile as PartialProfile<LocalProfile>
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as psNode from 'ps-node'
|
import * as psNode from 'ps-node'
|
||||||
import * as fs from 'mz/fs'
|
import * as fs from 'mz/fs'
|
||||||
import * as os from 'os'
|
|
||||||
import { Injector } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core'
|
import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core'
|
||||||
import { BaseSession } from 'tabby-terminal'
|
import { BaseSession } from 'tabby-terminal'
|
||||||
@@ -19,8 +18,6 @@ try {
|
|||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi
|
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
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
export class PTYProxy {
|
export class PTYProxy {
|
||||||
@@ -90,7 +87,6 @@ export class Session extends BaseSession {
|
|||||||
private ptyClosed = false
|
private ptyClosed = false
|
||||||
private pauseAfterExit = false
|
private pauseAfterExit = false
|
||||||
private guessedCWD: string|null = null
|
private guessedCWD: string|null = null
|
||||||
private reportedCWD: string
|
|
||||||
private initialCWD: string|null = null
|
private initialCWD: string|null = null
|
||||||
private config: ConfigService
|
private config: ConfigService
|
||||||
private hostApp: HostAppService
|
private hostApp: HostAppService
|
||||||
@@ -184,9 +180,7 @@ export class Session extends BaseSession {
|
|||||||
|
|
||||||
this.pty.subscribe('data', (array: Uint8Array) => {
|
this.pty.subscribe('data', (array: Uint8Array) => {
|
||||||
this.pty!.ackData(array.length)
|
this.pty!.ackData(array.length)
|
||||||
|
const data = Buffer.from(array)
|
||||||
let data = Buffer.from(array)
|
|
||||||
data = this.processOSC1337(data)
|
|
||||||
this.emitOutput(data)
|
this.emitOutput(data)
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
this.guessWindowsCWD(data.toString())
|
this.guessWindowsCWD(data.toString())
|
||||||
@@ -293,7 +287,7 @@ export class Session extends BaseSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
supportsWorkingDirectory (): boolean {
|
supportsWorkingDirectory (): boolean {
|
||||||
return !!(this.truePID || this.reportedCWD || this.guessedCWD)
|
return !!(this.truePID ?? this.reportedCWD ?? this.guessedCWD)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkingDirectory (): Promise<string|null> {
|
async getWorkingDirectory (): Promise<string|null> {
|
||||||
@@ -336,22 +330,4 @@ export class Session extends BaseSession {
|
|||||||
this.guessedCWD = match[0]
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -138,13 +138,6 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab instanceof TerminalTabComponent && tab.session?.supportsWorkingDirectory()) {
|
|
||||||
items.push({
|
|
||||||
label: 'Copy current path',
|
|
||||||
click: () => tab.copyCurrentPath(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -174,11 +174,6 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
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:
|
is-arguments@^1.0.4, is-arguments@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
|
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-plugin-manager",
|
"name": "tabby-plugin-manager",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Tabby's plugin manager",
|
"description": "Tabby's plugin manager",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
|
@@ -8,7 +8,7 @@ import { PluginManagerService } from '../services/pluginManager.service'
|
|||||||
|
|
||||||
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
|
||||||
|
|
||||||
const FORCE_ENABLE = ['tabby-core', 'tabby-settings']
|
const FORCE_ENABLE = ['tabby-core', 'tabby-settings', 'tabby-electron', 'tabby-web']
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-serial",
|
"name": "tabby-serial",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Serial connections for Tabby",
|
"description": "Serial connections for Tabby",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
|
@@ -25,10 +25,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
|||||||
xon: false,
|
xon: false,
|
||||||
xoff: false,
|
xoff: false,
|
||||||
xany: false,
|
xany: false,
|
||||||
inputMode: 'local-echo',
|
inputMode: null,
|
||||||
outputMode: null,
|
outputMode: null,
|
||||||
inputNewlines: null,
|
inputNewlines: null,
|
||||||
outputNewlines: 'crlf',
|
outputNewlines: null,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSuggestedName (profile: SerialProfile): string {
|
||||||
|
return this.getDescription(profile)
|
||||||
|
}
|
||||||
|
|
||||||
getDescription (profile: SerialProfile): string {
|
getDescription (profile: SerialProfile): string {
|
||||||
return profile.options.port
|
return profile.options.port
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tabby-settings",
|
"name": "tabby-settings",
|
||||||
"version": "1.0.148-nightly.2",
|
"version": "1.0.150",
|
||||||
"description": "Tabby terminal settings page",
|
"description": "Tabby terminal settings page",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tabby-builtin-plugin"
|
"tabby-builtin-plugin"
|
||||||
|
@@ -0,0 +1,122 @@
|
|||||||
|
h3.mb-3 Config sync
|
||||||
|
|
||||||
|
ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
|
li(ngbNavItem)
|
||||||
|
a(ngbNavLink) Sync
|
||||||
|
ng-template(ngbNavContent)
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync host
|
||||||
|
|
||||||
|
.input-group.w-50
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='config.store.configSync.host',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
.input-group-append(*ngIf='config.store.configSync.host')
|
||||||
|
button.btn.btn-secondary((click)='platform.openExternal("http://" + config.store.configSync.host)')
|
||||||
|
i.fas.fa-external-link-alt
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Secret sync token
|
||||||
|
.description Get it from the Tabby Web settings window
|
||||||
|
|
||||||
|
.input-group
|
||||||
|
input.form-control(
|
||||||
|
type='password',
|
||||||
|
[(ngModel)]='config.store.configSync.token',
|
||||||
|
(ngModelChange)='config.save(); testConnection()'
|
||||||
|
)
|
||||||
|
.input-group-append(*ngIf='config.store.configSync.token')
|
||||||
|
.input-group-text
|
||||||
|
i.fas.fa-fw.fa-circle-notch.fa-spin.text-warning(*ngIf='connectionSuccessful === null')
|
||||||
|
i.fas.fa-fw.fa-check.text-success(*ngIf='connectionSuccessful')
|
||||||
|
i.fas.fa-fw.fa-exclamation-triangle.text-danger(*ngIf='connectionSuccessful === false')
|
||||||
|
|
||||||
|
ng-container(*ngIf='config.store.configSync.token')
|
||||||
|
.alert.alert-danger(*ngIf='connectionSuccessful === false')
|
||||||
|
i.fas.fa-exclamation-triangle
|
||||||
|
span.ml-2 Connection failed: {{connectionError}}
|
||||||
|
|
||||||
|
ng-container(*ngIf='connectionSuccessful')
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Configs
|
||||||
|
|
||||||
|
div(*ngIf='configs === null')
|
||||||
|
i.fas.fa-fw.fa-circle-notch.fa-spin
|
||||||
|
span.ml-2 Loading configs...
|
||||||
|
|
||||||
|
ng-container(*ngIf='configs !== null')
|
||||||
|
.list-group-light
|
||||||
|
.list-group-item.d-flex.align-items-center(
|
||||||
|
*ngFor='let cfg of configs',
|
||||||
|
[class.active]='isActiveConfig(cfg)',
|
||||||
|
)
|
||||||
|
i.fas.fa-fw.fa-file
|
||||||
|
.ml-2.d-flex.flex-column.align-items-start
|
||||||
|
div {{cfg.name}}
|
||||||
|
small.text-muted Modified on {{cfg.modified_at|date:'medium'}}
|
||||||
|
.badge.badge-info(*ngIf='isActiveConfig(cfg)') ACTIVE
|
||||||
|
.mr-auto
|
||||||
|
button.btn.btn-link.ml-1(
|
||||||
|
(click)='uploadAndSync(cfg)',
|
||||||
|
[class.hover-reveal]='!isActiveConfig(cfg)'
|
||||||
|
)
|
||||||
|
i.fas.fa-arrow-up
|
||||||
|
span.ml-2(*ngIf='isActiveConfig(cfg)') Upload
|
||||||
|
span.ml-2(*ngIf='!isActiveConfig(cfg)') Replace
|
||||||
|
button.btn.btn-link.ml-1(
|
||||||
|
(click)='downloadAndSync(cfg)',
|
||||||
|
[class.hover-reveal]='!isActiveConfig(cfg)'
|
||||||
|
)
|
||||||
|
i.fas.fa-arrow-down
|
||||||
|
span.ml-2 Download
|
||||||
|
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
|
href='#',
|
||||||
|
(click)='uploadAsNew()'
|
||||||
|
)
|
||||||
|
i.fas.fa-fw.fa-cloud-upload-alt
|
||||||
|
.ml-2 Upload as a new config
|
||||||
|
|
||||||
|
ng-container(*ngIf='hasMatchingRemoteConfig()')
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync automatically
|
||||||
|
.description Automatically upload changes and check for updates every minute
|
||||||
|
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.auto',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
li(ngbNavItem)
|
||||||
|
a(ngbNavLink) Advanced
|
||||||
|
ng-template(ngbNavContent)
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync hotkeys
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.hotkeys',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync window settings
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.appearance',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-line
|
||||||
|
.header
|
||||||
|
.title Sync Vault
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='config.store.configSync.parts.vault',
|
||||||
|
(ngModelChange)='config.save()',
|
||||||
|
)
|
||||||
|
|
||||||
|
div([ngbNavOutlet]='nav')
|
109
tabby-settings/src/components/configSyncSettingsTab.component.ts
Normal file
109
tabby-settings/src/components/configSyncSettingsTab.component.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { BaseComponent, ConfigService, PromptModalComponent, HostAppService, PlatformService, NotificationsService } from 'tabby-core'
|
||||||
|
import { Config, ConfigSyncService } from '../services/configSync.service'
|
||||||
|
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'config-sync-settings-tab',
|
||||||
|
template: require('./configSyncSettingsTab.component.pug'),
|
||||||
|
})
|
||||||
|
export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
||||||
|
connectionSuccessful: boolean|null = null
|
||||||
|
connectionError: Error|null = null
|
||||||
|
configs: Config[]|null = null
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public config: ConfigService,
|
||||||
|
public platform: PlatformService,
|
||||||
|
private configSync: ConfigSyncService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
private notifications: NotificationsService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit () {
|
||||||
|
await this.testConnection()
|
||||||
|
this.loadConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection () {
|
||||||
|
if (!this.config.store.configSync.host || !this.config.store.configSync.token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.connectionSuccessful = null
|
||||||
|
try {
|
||||||
|
await this.configSync.getUser()
|
||||||
|
this.connectionSuccessful = true
|
||||||
|
this.loadConfigs()
|
||||||
|
} catch (e) {
|
||||||
|
this.connectionSuccessful = false
|
||||||
|
this.connectionError = e
|
||||||
|
this.configs = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfigs () {
|
||||||
|
this.configs = await this.configSync.getConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAsNew () {
|
||||||
|
let name = `New config on ${this.hostApp.platform}`
|
||||||
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = 'Name for the new config'
|
||||||
|
modal.componentInstance.value = name
|
||||||
|
name = (await modal.result)?.value
|
||||||
|
if (!name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cfg = await this.configSync.createNewConfig(name)
|
||||||
|
this.loadConfigs()
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
this.uploadAndSync(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAndSync (cfg: Config) {
|
||||||
|
if (this.config.store.configSync.configID !== cfg.id) {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Overwrite the config on the remote side and start syncing?',
|
||||||
|
buttons: ['Overwrite remote and sync', 'Cancel'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
|
})).response === 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
await this.configSync.upload()
|
||||||
|
this.loadConfigs()
|
||||||
|
this.notifications.info('Config uploaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadAndSync (cfg: Config) {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Overwrite the local config and start syncing?',
|
||||||
|
buttons: ['Overwrite local and sync', 'Cancel'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
|
})).response === 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.configSync.setConfig(cfg)
|
||||||
|
await this.configSync.download()
|
||||||
|
this.notifications.info('Config downloaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMatchingRemoteConfig () {
|
||||||
|
return !!this.configs?.find(c => this.isActiveConfig(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
isActiveConfig (c: Config) {
|
||||||
|
return c.id === this.config.store.configSync.configID
|
||||||
|
}
|
||||||
|
}
|
@@ -46,7 +46,10 @@
|
|||||||
input.form-control.w-50(
|
input.form-control.w-50(
|
||||||
type='text',
|
type='text',
|
||||||
[(ngModel)]='profile.color',
|
[(ngModel)]='profile.color',
|
||||||
placeholder='#000000'
|
placeholder='#000000',
|
||||||
|
alwaysVisibleTypeahead,
|
||||||
|
[ngbTypeahead]='colorsAutocomplete',
|
||||||
|
[resultFormatter]='colorsFormatter'
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user