Compare commits

..

61 Commits

Author SHA1 Message Date
Eugeny
78f25a7679 Merge pull request #4364 from Eugeny/all-contributors/add-al-wi 2021-08-06 10:09:35 +02:00
allcontributors[bot]
0c4d8b0784 docs: update .all-contributorsrc [skip ci] 2021-08-06 08:09:22 +00:00
Eugeny
e2c8093b97 Merge pull request #4363 from al-wi/profile-filter-terms
Profile filter per search term instead of the entire search input
2021-08-06 10:09:21 +02:00
allcontributors[bot]
c497a71361 docs: update README.md [skip ci] 2021-08-06 08:09:21 +00:00
Alexander Wiedemann
ec2982b1c4 Filter per search term instead of the entire search input 2021-08-06 09:55:15 +02:00
Eugene Pankov
be0aeefdb3 Update baseTerminalTab.component.ts 2021-08-06 09:52:37 +02:00
Eugene Pankov
eadd8d563e more hotkey fixes 2021-08-06 09:52:34 +02:00
Eugene Pankov
08f1ad4c75 hotkey handling fixes 2021-08-06 09:43:54 +02:00
Eugene Pankov
426606ba06 fixed split tab index - fixes #4249 2021-08-06 09:07:38 +02:00
Eugene Pankov
7b59ba4b73 reworked hotkey handling - fixes #4355, fixes #4326, fixes #4340 2021-08-06 09:03:55 +02:00
Eugene Pankov
533837f5b7 another possible fix for profile editing - #4330 2021-08-05 10:44:00 +02:00
Eugene Pankov
3f0db97a68 finally fixed docs 2021-08-04 20:53:42 +02:00
Eugene Pankov
231594d709 fixed docs build 2021-08-04 20:28:22 +02:00
Eugene Pankov
e4ae114c71 removed old ssh2 patch 2021-08-04 20:22:39 +02:00
Eugene Pankov
20000d16f8 fixed build order 2021-08-04 20:21:14 +02:00
Eugene Pankov
5e0a9b2e52 Merge branch 'master' of github.com:Eugeny/tabby 2021-08-04 19:51:11 +02:00
Eugene Pankov
fa70447223 keep "disable dynamic title" while duplicating or restoring a tab - fixes #4334 2021-08-04 19:51:08 +02:00
Eugene Pankov
28b84e38ca fixed profiles not getting saved on Windows - fixes #4330 2021-08-04 19:44:02 +02:00
Eugeny
3c4a078fa5 Merge pull request #4322 from Eugeny/dependabot/npm_and_yarn/typedoc-0.21.5
Bump typedoc from 0.21.4 to 0.21.5
2021-08-04 19:41:21 +02:00
Eugeny
52f4e88420 Merge pull request #4333 from Eugeny/dependabot/npm_and_yarn/webpack-5.48.0 2021-08-04 19:40:47 +02:00
Eugeny
16d9045a80 Merge pull request #4336 from Eugeny/dependabot/npm_and_yarn/electron-13.1.8
Bump electron from 13.1.7 to 13.1.8
2021-08-04 19:39:37 +02:00
Eugeny
07d7d8daba Merge pull request #4321 from Eugeny/dependabot/npm_and_yarn/electron-notarize-1.0.1 2021-08-04 19:39:16 +02:00
Eugeny
b2b9476298 Merge pull request #4320 from Eugeny/dependabot/npm_and_yarn/eslint-7.32.0
Bump eslint from 7.31.0 to 7.32.0
2021-08-04 19:38:48 +02:00
Eugeny
cf7f3dffe3 Merge pull request #4317 from Eugeny/dependabot/npm_and_yarn/ssh2-1.2.0
Bump ssh2 from 1.1.0 to 1.2.0
2021-08-04 19:38:30 +02:00
Eugene Pankov
621005eb27 build order fix 2021-08-04 19:28:41 +02:00
dependabot[bot]
d46e1de8aa Bump electron from 13.1.7 to 13.1.8
Bumps [electron](https://github.com/electron/electron) from 13.1.7 to 13.1.8.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v13.1.7...v13.1.8)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-04 04:03:36 +00:00
Eugene Pankov
c44f3c5f25 lint 2021-08-03 15:13:17 +02:00
dependabot[bot]
b3f9d48609 Bump webpack from 5.47.1 to 5.48.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.47.1 to 5.48.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.47.1...v5.48.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-03 04:06:40 +00:00
Eugene Pankov
edd7e9c7b7 set default cancel button in message boxes 2021-08-02 21:01:35 +02:00
Eugene Pankov
ab8061ab39 better new profile name handling - fixes #4325 2021-08-02 20:52:39 +02:00
Eugene Pankov
c1a1f53707 added a default name for duplicated profiles - fixes #4325 2021-08-02 20:03:02 +02:00
Eugene Pankov
04097a0ef5 serial: revert default CRLF processing to none - #4325 2021-08-02 20:00:38 +02:00
Eugene Pankov
85be974e64 reworked drop zones to allow more pane drop positions 2021-08-02 17:40:54 +02:00
Eugene Pankov
0df5fb4a34 fixed start page button flickering - fixes #4298 2021-08-02 09:25:05 +02:00
Eugene Pankov
920b2b85b3 Update appRoot.component.pug 2021-08-02 09:22:42 +02:00
Eugene Pankov
4e4788bf57 fixed glitchy window drag in macOS - fixes #4324 2021-08-02 09:17:08 +02:00
dependabot[bot]
9aa60a9d0d Bump typedoc from 0.21.4 to 0.21.5
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.21.4 to 0.21.5.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.21.4...v0.21.5)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:08:14 +00:00
dependabot[bot]
451ac51520 Bump electron-notarize from 1.0.0 to 1.0.1
Bumps [electron-notarize](https://github.com/electron/electron-notarize) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/electron/electron-notarize/releases)
- [Changelog](https://github.com/electron/electron-notarize/blob/master/.releaserc.json)
- [Commits](https://github.com/electron/electron-notarize/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: electron-notarize
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:07:48 +00:00
dependabot[bot]
04084aef33 Bump eslint from 7.31.0 to 7.32.0
Bumps [eslint](https://github.com/eslint/eslint) from 7.31.0 to 7.32.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.31.0...v7.32.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:07:29 +00:00
dependabot[bot]
4198ca3fae Bump ssh2 from 1.1.0 to 1.2.0
Bumps [ssh2](https://github.com/mscdex/ssh2) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/mscdex/ssh2/releases)
- [Commits](https://github.com/mscdex/ssh2/compare/v1.1.0...v1.2.0)

---
updated-dependencies:
- dependency-name: ssh2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-02 04:05:32 +00:00
Eugene Pankov
3b09dfa145 sftp: allow editing remote files locally - fixes #4311 2021-08-01 16:13:11 +02:00
Eugene Pankov
923b559857 bumped plugins 2021-08-01 10:17:53 +02:00
Eugene Pankov
58682b6bf1 serial: disable local echo by default 2021-08-01 10:17:52 +02:00
Eugene Pankov
88c4198145 Merge branch 'master' of github.com:Eugeny/tabby 2021-07-31 19:28:05 +02:00
Eugene Pankov
a6c535414f switched to webpack asset modules 2021-07-31 19:28:01 +02:00
Eugeny
6ebb7723ff Merge pull request #4212 from Eugeny/dependabot/npm_and_yarn/slugify-1.6.0
Bump slugify from 1.5.3 to 1.6.0
2021-07-31 19:17:14 +02:00
Eugeny
07dd6600dc Merge pull request #4300 from Eugeny/dependabot/npm_and_yarn/webpack-5.47.1
Bump webpack from 5.46.0 to 5.47.1
2021-07-31 19:12:05 +02:00
Eugene Pankov
cc6cfec907 added a default color to split tab spanners 2021-07-31 18:59:52 +02:00
Eugene Pankov
4ecfcfda36 ui tweaks 2021-07-31 18:02:39 +02:00
Eugene Pankov
c5681b1376 allow rearranging panes within a tab 2021-07-31 17:57:43 +02:00
Eugene Pankov
1fc57018e3 allow modifier-only hotkeys 2021-07-31 17:18:03 +02:00
Eugene Pankov
8b8bacdf69 fixed config file getting spontaneously erased - fixes #4293 2021-07-31 15:28:10 +02:00
dependabot[bot]
3aaa419f8b Bump webpack from 5.46.0 to 5.47.1
Bumps [webpack](https://github.com/webpack/webpack) from 5.46.0 to 5.47.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.46.0...v5.47.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-30 04:05:41 +00:00
Eugene Pankov
94819019ec bumped plugins 2021-07-29 21:20:41 +02:00
Eugene Pankov
7b37035f75 toolbar dropdown ui fix 2021-07-29 21:20:34 +02:00
Eugene Pankov
a5ef3507c3 toolbar ui fix 2021-07-29 21:20:29 +02:00
Eugene Pankov
b9c6d30678 include titles of all panes in a split tab's title 2021-07-29 21:20:25 +02:00
Eugene Pankov
009556f984 possible fix for #4293 2021-07-29 21:07:47 +02:00
Eugene Pankov
87007d5ae3 demo progress report fix 2021-07-29 21:07:31 +02:00
Eugene Pankov
61ea2c77c8 config sync ui updates 2021-07-29 21:07:22 +02:00
dependabot[bot]
8bfc1dc302 Bump slugify from 1.5.3 to 1.6.0
Bumps [slugify](https://github.com/simov/slugify) from 1.5.3 to 1.6.0.
- [Release notes](https://github.com/simov/slugify/releases)
- [Commits](https://github.com/simov/slugify/compare/v1.5.3...v1.6.0)

---
updated-dependencies:
- dependency-name: slugify
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-18 14:53:38 +00:00
92 changed files with 1315 additions and 685 deletions

View File

@@ -433,6 +433,15 @@
"contributions": [
"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,

View File

@@ -194,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://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://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>
</table>

View File

@@ -40,22 +40,8 @@ module.exports = {
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
test: /\.(png|svg)$/,
use: {
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]',
},
},
test: /\.(png|svg|ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
type: 'asset',
},
],
},

View File

@@ -27,14 +27,14 @@
"compare-versions": "^3.6.0",
"core-js": "^3.15.2",
"cross-env": "7.0.3",
"css-loader": "5.2.6",
"electron": "13.1.7",
"css-loader": "^6.2.0",
"electron": "13.1.8",
"electron-builder": "22.10.5",
"electron-download": "^4.1.1",
"electron-installer-snap": "^5.1.0",
"electron-notarize": "^1.0.0",
"electron-notarize": "^1.0.1",
"electron-rebuild": "^2.3.5",
"eslint": "^7.31.0",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.23.4",
"file-loader": "^6.2.0",
"graceful-fs": "^4.2.6",
@@ -58,21 +58,20 @@
"sass-loader": "^12.1.0",
"shell-quote": "^1.7.2",
"shelljs": "0.8.4",
"slugify": "^1.5.3",
"slugify": "^1.6.0",
"sortablejs": "^1.14.0",
"source-code-pro": "^2.38.0",
"source-map-loader": "^3.0.0",
"source-sans-pro": "3.6.0",
"ssh2": "^1.1.0",
"style-loader": "^3.1.0",
"ssh2": "^1.2.0",
"style-loader": "^3.2.1",
"svg-inline-loader": "^0.8.2",
"ts-loader": "^9.2.3",
"tslib": "^2.3.0",
"typedoc": "^0.21.4",
"typedoc": "^0.21.5",
"typescript": "^4.3.5",
"url-loader": "^4.1.1",
"val-loader": "4.0.0",
"webpack": "^5.46.0",
"webpack": "^5.48.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.0",
"yaml-loader": "0.6.0",
@@ -90,7 +89,7 @@
"start": "cross-env TABBY_DEV=1 electron app --debug --inspect",
"start:prod": "electron app --debug",
"prod": "cross-env TABBY_DEV=1 electron app",
"docs": "typedoc --out docs/api --tsconfig tabby-core/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",
"postinstall": "node ./scripts/install-deps.js",
"patch": "patch-package; cd web; patch-package"

View File

@@ -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) {

View File

@@ -17,14 +17,14 @@ exports.builtinPlugins = [
'tabby-core',
'tabby-settings',
'tabby-terminal',
'tabby-electron',
'tabby-local',
'tabby-web',
'tabby-community-color-schemes',
'tabby-plugin-manager',
'tabby-ssh',
'tabby-serial',
'tabby-telnet',
'tabby-electron',
'tabby-local',
'tabby-plugin-manager',
]
exports.allPackages = [

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-community-color-schemes",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Community color schemes for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-core",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Tabby core",
"keywords": [
"tabby-builtin-plugin"

View File

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

View File

@@ -13,6 +13,7 @@ export interface MessageBoxOptions {
detail?: string
buttons: string[]
defaultId?: number
cancelId?: number
}
export interface MessageBoxResult {

View File

@@ -46,6 +46,10 @@ export abstract class ProfileProvider<P extends Profile> {
abstract getNewTabParameters (profile: PartialProfile<P>): Promise<NewTabParameters<BaseTabComponent>>
getSuggestedName (profile: PartialProfile<P>): string|null {
return null
}
abstract getDescription (profile: PartialProfile<P>): string
quickConnect (query: string): PartialProfile<P>|null {

View File

@@ -21,10 +21,6 @@ title-bar(
)
tab-header(
*ngFor='let tab of app.tabs; let idx = index',
cdkDrag,
[cdkDragData]='tab',
(cdkDragStarted)='onTabDragStart(tab)',
(cdkDragEnded)='onTabDragEnd()',
[index]='idx',
[tab]='tab',
[active]='tab == app.activeTab',

View File

@@ -182,17 +182,6 @@ export class AppRootComponent {
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
}
onTabDragStart (tab: BaseTabComponent) {
this.app.emitTabDragStarted(tab)
}
onTabDragEnd () {
setTimeout(() => {
this.app.emitTabDragEnded()
this.app.emitTabsChanged()
})
}
async generateButtonSubmenu (button: ToolbarButton) {
if (button.submenu) {
button.submenuItems = await button.submenu()

View File

@@ -1,5 +1,5 @@
.modal-body
input.form-control(
input.form-control.form-control-lg(
type='text',
[(ngModel)]='filter',
autofocus,

View File

@@ -19,6 +19,5 @@
}
input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-radius: 0;
}

View File

@@ -50,8 +50,9 @@ export class SelectorModalComponent<T> {
if (!f) {
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
} else {
const terms = f.split(' ')
// 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.min(this.filteredOptions.length - 1, this.selectedIndex)

View File

@@ -93,13 +93,13 @@ export class SplitContainer {
return s
}
async serialize (): Promise<RecoveryToken> {
async serialize (tabsRecovery: TabRecoveryService): Promise<RecoveryToken> {
const children: any[] = []
for (const child of this.children) {
if (child instanceof SplitContainer) {
children.push(await child.serialize())
children.push(await child.serialize(tabsRecovery))
} else {
children.push(await child.getRecoveryToken())
children.push(await tabsRecovery.getFullRecoveryToken(child))
}
}
return {
@@ -127,8 +127,14 @@ export interface SplitSpannerInfo {
* Represents a tab drop zone
*/
export interface SplitDropZoneInfo {
relativeToTab: BaseTabComponent
side: SplitDirection
container?: SplitContainer
position?: number
relativeTo?: BaseTabComponent|SplitContainer
side?: SplitDirection
x: number
y: number
w: number
h: number
}
/**
@@ -152,6 +158,14 @@ export interface SplitDropZoneInfo {
(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')],
})
@@ -362,7 +376,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/**
* Inserts a new `tab` to the `side` of the `relative` tab
*/
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|SplitContainer|null, side: SplitDirection): Promise<void> {
if (thing instanceof SplitTabComponent) {
const tab = thing
thing = tab.root
@@ -374,34 +388,47 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
}
if (thing instanceof BaseTabComponent) {
if (thing.parent instanceof SplitTabComponent) {
thing.parent.removeTab(thing)
}
thing.removeFromContainer()
thing.parent = this
}
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
let insertIndex = relative ? target.children.indexOf(relative) : -1
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 (
target.orientation === 'v' && ['l', 'r'].includes(side) ||
target.orientation === 'h' && ['t', 'b'].includes(side)
) {
// Inserting into a container but the orientation isn't compatible
const newContainer = new SplitContainer()
newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
newContainer.orientation = ['l', 'r'].includes(side) ? 'h' : 'v'
newContainer.children = relative ? [relative] : []
newContainer.ratios = [1]
target.children[insertIndex] = newContainer
target.children.splice(relative ? target.children.indexOf(relative) : -1, 1, newContainer)
target = newContainer
insertIndex = 0
}
if (insertIndex === -1) {
insertIndex = 0
} else {
insertIndex += side === 'l' || side === 't' ? 0 : 1
insertIndex = 'tl'.includes(side) ? 0 : 1
}
for (let i = 0; i < target.children.length; i++) {
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.children.splice(insertIndex, 0, thing)
@@ -538,7 +565,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
async getRecoveryToken (): Promise<any> {
return this.root.serialize()
return this.root.serialize(this.tabRecovery)
}
/** @hidden */
@@ -558,7 +585,11 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
return
}
this.add(tab, zone.relativeToTab, zone.side)
if (zone.container) {
this.add(tab, zone.container.children[zone.position!], zone.container.orientation === 'h' ? 'r' : 'b')
} else {
this.add(tab, null, zone.side!)
}
this.tabAdopted.next(tab)
}
@@ -576,16 +607,20 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
this.layoutInternal(this.root, 0, 0, 100, 100)
}
private updateTitle (): void {
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
}
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.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.progress$, p => this.setProgress(p))
if (tab.title) {
this.setTitle(tab.title)
this.updateTitle()
}
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
this.recoveryStateChangedHint.next()
@@ -606,6 +641,38 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
const size = root.orientation === 'v' ? h : w
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,
side: 'l',
})
this._dropZones.push({
x,
y: y - thickness / 2,
w,
h: thickness,
side: 't',
})
this._dropZones.push({
x: x + w - thickness / 2,
y: y + thickness,
w: thickness,
h: h - thickness * 2,
side: 'r',
})
this._dropZones.push({
x,
y: y + h - thickness / 2,
w,
h: thickness,
side: 'b',
})
}
root.x = x
root.y = y
@@ -639,17 +706,60 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
element.style.width = '90%'
element.style.height = '90%'
}
for (const side of ['t', 'r', 'b', 'l']) {
this._dropZones.push({
relativeToTab: child,
side: side as SplitDirection,
})
}
}
}
offset += sizes[i]
if (i !== root.ratios.length - 1) {
// Spanner area
this._dropZones.push({
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,
relativeTo: child,
side: 'l',
})
this._dropZones.push({
x: childX + w - thickness,
y: childY + thickness,
w: thickness,
h: childH - thickness * 2,
relativeTo: child,
side: 'r',
})
} else {
this._dropZones.push({
x: childX + thickness,
y: childY,
w: childW - thickness * 2,
h: thickness,
relativeTo: child,
side: 't',
})
this._dropZones.push({
x: childX + thickness,
y: childY + childH - thickness,
w: childW - thickness * 2,
h: thickness,
relativeTo: child,
side: 'b',
})
}
if (i !== 0) {
this._spanners.push({
container: root,

View File

@@ -9,6 +9,7 @@
flex: 1 1 0;
width: 100%;
height: 100%;
opacity: 0;
background: rgba(255, 255, 255, .125);
border-radius: 5px;
@@ -21,6 +22,7 @@
border-radius: 3px;
> div {
opacity: 1;
background: rgba(255, 255, 255, .5);
}
}

View File

@@ -34,7 +34,7 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
) {
super(element)
this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
this.isActive = !!tab && tab !== this.parent
this.isActive = !!tab && tab !== this.parent && tab !== this.dropZone.container?.children[this.dropZone.position!]
this.layout()
})
}
@@ -44,26 +44,11 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
}
layout () {
const tabElement: HTMLElement|undefined = this.dropZone.relativeToTab.viewContainerEmbeddedRef?.rootNodes[0]
if (!tabElement) {
// being destroyed
return
}
const args = {
t: [0, 0, tabElement.clientWidth, tabElement.clientHeight / 5],
l: [0, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
r: [tabElement.clientWidth * 2 / 3, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
b: [0, tabElement.clientHeight * 4 / 5, tabElement.clientWidth, tabElement.clientHeight / 5],
}[this.dropZone.side]
this.setDimensions(
args[0] + tabElement.offsetLeft,
args[1] + tabElement.offsetTop,
args[2],
args[3],
'px'
this.dropZone.x,
this.dropZone.y,
this.dropZone.w,
this.dropZone.h,
)
}
}

View 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;
}

View 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'
)
}
}

View File

@@ -3,6 +3,7 @@
position: absolute;
z-index: 5;
transition: 0.125s background;
background: rgba(0, 0, 0, .2);
&.v {
cursor: ns-resize;

View File

@@ -5,7 +5,7 @@ div
.list-group
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()',
)
.d-flex.align-self-center([innerHTML]='sanitizeIcon(button.icon)')

View File

@@ -32,4 +32,8 @@ export class StartPageComponent {
sanitizeIcon (icon?: string): any {
return this.domSanitizer.bypassSecurityTrustHtml(icon ?? '')
}
buttonsTrackBy (btn: ToolbarButton): any {
return btn.title + btn.icon
}
}

View File

@@ -2,13 +2,17 @@
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
.activity-indicator(*ngIf='tab.activity$|async')
ng-container(*ngIf='!config.store.terminal.hideTabIndex')
.index(*ngIf='hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
.index(*ngIf='hostApp.platform !== Platform.macOS') {{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(
[title]='tab.customTitle || tab.title',
[class.no-hover]='config.store.terminal.hideCloseButton'
cdkDrag,
cdkDragRootElement='tab-header',
[cdkDragData]='tab',
(cdkDragStarted)='onTabDragStart(tab)',
(cdkDragEnded)='onTabDragEnd()',
) {{tab.customTitle || tab.title}}
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') &times;

View File

@@ -72,6 +72,17 @@ export class TabHeaderComponent extends BaseComponent {
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 {
return this.config.store.appearance.flexTabs
}

View File

@@ -22,6 +22,7 @@
min-width: 0;
margin-right: auto;
margin-bottom: 3px;
width: 100%;
label {
margin: 0;

View File

@@ -43,6 +43,7 @@ export class TransfersMenuComponent {
message: 'There are active file transfers',
buttons: ['Abort all', 'Do not abort'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}

View File

@@ -18,6 +18,8 @@ hotkeys:
- 'Ctrl-Shift-PageUp'
move-tab-right:
- 'Ctrl-Shift-PageDown'
rearrange-panes:
- 'Ctrl-Shift'
tab-1:
- 'Alt-1'
tab-2:

View File

@@ -16,6 +16,8 @@ hotkeys:
- '⌘-Shift-Left'
move-tab-right:
- '⌘-Shift-Right'
rearrange-panes:
- '⌘-Shift'
tab-1:
- '⌘-1'
tab-2:

View File

@@ -19,6 +19,8 @@ hotkeys:
- 'Ctrl-Shift-PageUp'
move-tab-right:
- 'Ctrl-Shift-PageDown'
rearrange-panes:
- 'Ctrl-Shift'
tab-1:
- 'Alt-1'
tab-2:

View File

@@ -46,6 +46,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'move-tab-right',
name: 'Move tab to the right',
},
{
id: 'rearrange-panes',
name: 'Show pane labels (for rearranging)',
},
{
id: 'tab-1',
name: 'Tab 1',

View File

@@ -23,6 +23,7 @@ import { SelectorModalComponent } from './components/selectorModal.component'
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.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 { WelcomeTabComponent } from './components/welcomeTab.component'
import { TransfersMenuComponent } from './components/transfersMenu.component'
@@ -100,6 +101,7 @@ const PROVIDERS = [
SplitTabComponent,
SplitTabSpannerComponent,
SplitTabDropZoneComponent,
SplitTabPaneLabelComponent,
UnlockVaultModalComponent,
WelcomeTabComponent,
TransfersMenuComponent,

View File

@@ -194,6 +194,10 @@ export class ConfigService {
}
async save (): Promise<void> {
await this.ready$
if (!this._store) {
throw new Error('Cannot save an empty store')
}
// Scrub undefined values
let cleanStore = JSON.parse(JSON.stringify(this._store))
cleanStore = await this.maybeEncryptConfig(cleanStore)
@@ -371,6 +375,7 @@ export class ConfigService {
detail: e.toString(),
buttons: ['Erase config', 'Quit'],
defaultId: 1,
cancelId: 1,
})
if (result.response === 1) {
this.platform.quit()

View File

@@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs'
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 { HostAppService, Platform } from '../api/hostApp'
import { deprecate } from 'util'
@@ -12,14 +12,17 @@ export interface PartialHotkeyMatch {
matchedLength: number
}
const KEY_TIMEOUT = 2000
interface PastKeystroke {
keystroke: Keystroke
time: number
}
@Injectable({ providedIn: 'root' })
export class HotkeysService {
/** @hidden @deprecated */
key = new EventEmitter<KeyboardEvent>()
/** @hidden */
/** @hidden @deprecated */
matchedHotkey = new EventEmitter<string>()
/**
@@ -27,11 +30,42 @@ export class HotkeysService {
*/
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 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 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 zone: NgZone,
private config: ConfigService,
@@ -39,16 +73,13 @@ export class HotkeysService {
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()
}
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()
}
})
})
@@ -60,102 +91,150 @@ export class HotkeysService {
// deprecated
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
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
*
* @param name DOM event name
* @param eventName DOM event name
* @param nativeEvent event object
*/
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
nativeEvent['event'] = name
this.currentKeystrokes.push({
pushKeyEvent (eventName: string, nativeEvent: KeyboardEvent): void {
if (nativeEvent.timeStamp === this.lastEventTimestamp) {
return
}
nativeEvent['event'] = eventName
const eventData = {
ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey,
altKey: nativeEvent.altKey,
shiftKey: nativeEvent.shiftKey,
code: nativeEvent.code,
key: nativeEvent.key,
eventName: name,
time: 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()
}
})
eventName,
time: nativeEvent.timeStamp,
registrationTime: performance.now(),
}
for (const [key, time] of this.pressedKeyTimestamps.entries()) {
if (time < performance.now() - 5000) {
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 {
this.zone.run(() => {
this.key.emit(nativeEvent)
})
getCurrentKeystrokes (): Keystroke[] {
if (!this.pressedKeystroke) {
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 {
this.currentKeystrokes = []
}
getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
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
this.lastKeystrokes = []
this.pressedKeys.clear()
this.pressedKeyTimestamps.clear()
this.pressedKeystroke = null
this.pressedHotkey = null
}
getHotkeyDescription (id: string): HotkeyDescription {
@@ -183,6 +262,42 @@ export class HotkeysService {
).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 () {
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
}
@@ -211,4 +326,14 @@ export class HotkeysService {
}
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)
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-type-alias */
export const metaKeyName = {
darwin: '⌘',
win32: 'Win',
@@ -10,72 +11,67 @@ export const altKeyName = {
linux: 'Alt',
}[process.platform]
export interface EventData {
ctrlKey: boolean
metaKey: boolean
altKey: boolean
shiftKey: boolean
export interface KeyEventData {
ctrlKey?: boolean
metaKey?: boolean
altKey?: boolean
shiftKey?: boolean
key: string
code: string
eventName: string
time: number
registrationTime: number
}
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export function stringifyKeySequence (events: EventData[]): string[] {
const items: string[] = []
events = events.slice()
export type KeyName = string
export type Keystroke = string
while (events.length > 0) {
const event = events.shift()!
if (event.eventName === 'keydown') {
const itemKeys: string[] = []
if (event.ctrlKey) {
itemKeys.push('Ctrl')
}
if (event.metaKey) {
itemKeys.push(metaKeyName)
}
if (event.altKey) {
itemKeys.push(altKeyName)
}
if (event.shiftKey) {
itemKeys.push('Shift')
}
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
// TODO make this optional?
continue
}
let key = event.code
if (REGEX_LATIN_KEYNAME.test(event.key)) {
// Handle Dvorak etc via the reported "character" instead of the scancode
key = event.key.toUpperCase()
} else {
key = key.replace('Key', '')
key = key.replace('Arrow', '')
key = key.replace('Digit', '')
key = {
Comma: ',',
Period: '.',
Slash: '/',
Backslash: '\\',
IntlBackslash: '`',
Backquote: '~', // Electron says it's the tilde
Minus: '-',
Equal: '=',
Semicolon: ';',
Quote: '\'',
BracketLeft: '[',
BracketRight: ']',
}[key] ?? key
}
itemKeys.push(key)
items.push(itemKeys.join('-'))
export function getKeyName (event: KeyEventData): KeyName {
// eslint-disable-next-line @typescript-eslint/init-declarations
let key: string
if (event.key === 'Control') {
key = 'Ctrl'
} else if (event.key === 'Meta') {
key = metaKeyName
} else if (event.key === 'Alt') {
key = altKeyName
} else if (event.key === 'Shift') {
key = 'Shift'
} else {
key = event.code
if (REGEX_LATIN_KEYNAME.test(event.key)) {
// Handle Dvorak etc via the reported "character" instead of the scancode
key = event.key.toUpperCase()
} else {
key = key.replace('Key', '')
key = key.replace('Arrow', '')
key = key.replace('Digit', '')
key = {
Comma: ',',
Period: '.',
Slash: '/',
Backslash: '\\',
IntlBackslash: '`',
Backquote: '~', // Electron says it's the tilde
Minus: '-',
Equal: '=',
Semicolon: ';',
Quote: '\'',
BracketLeft: '[',
BracketRight: ']',
}[key] ?? key
}
}
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('-')
}

View File

@@ -42,7 +42,7 @@ export class ProfilesService {
tab.setTitle(profile.name)
}
if (profile.disableDynamicTitle) {
tab['enableDynamicTitle'] = false
tab['disableDynamicTitle'] = true
}
return tab
}

View File

@@ -37,6 +37,7 @@ export class TabRecoveryService {
if (tab.color) {
token.tabColor = tab.color
}
token.disableDynamicTitle = tab['disableDynamicTitle']
}
return token
}
@@ -54,6 +55,7 @@ export class TabRecoveryService {
tab.inputs = tab.inputs ?? {}
tab.inputs.color = token.tabColor ?? null
tab.inputs.title = token.tabTitle || ''
tab.inputs.disableDynamicTitle = token.disableDynamicTitle
return tab
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-electron",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Electron-specific bindings",
"keywords": [
"tabby-builtin-plugin"
@@ -20,7 +20,8 @@
"@angular/core": "^9.1.9"
},
"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"
}
}

View File

@@ -1,6 +1,7 @@
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 { TerminalColorSchemeProvider } from 'tabby-terminal'
import { SFTPContextMenuItemProvider } from 'tabby-ssh'
import { HyperColorSchemes } from './colorSchemes'
import { ElectronPlatformService } from './services/platform.service'
@@ -14,11 +15,12 @@ import { ElectronHostAppService } from './services/hostApp.service'
import { ElectronService } from './services/electron.service'
import { ElectronHotkeyProvider } from './hotkeys'
import { ElectronConfigProvider } from './config'
import { EditSFTPContextMenu } from './sftpContextMenu'
@NgModule({
providers: [
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: PlatformService, useClass: ElectronPlatformService },
{ provide: PlatformService, useExisting: ElectronPlatformService },
{ provide: HostWindowService, useExisting: ElectronHostWindow },
{ provide: HostAppService, useExisting: ElectronHostAppService },
{ provide: LogService, useClass: ElectronLogService },
@@ -27,6 +29,7 @@ import { ElectronConfigProvider } from './config'
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
{ provide: FileProvider, useClass: ElectronFileProvider, multi: true },
{ provide: SFTPContextMenuItemProvider, useClass: EditSFTPContextMenu, multi: true },
],
})
export default class ElectronModule {

View File

@@ -1,7 +1,9 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import * as gracefulFS from 'graceful-fs'
import * as fsSync from 'fs'
import * as os from 'os'
import { promisify } from 'util'
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core'
@@ -20,10 +22,11 @@ try {
var wnr = require('windows-native-registry')
} catch { }
@Injectable()
@Injectable({ providedIn: 'root' })
export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true
private configPath: string
private _configSaveInProgress = Promise.resolve()
constructor (
private hostApp: HostAppService,
@@ -107,7 +110,17 @@ export class ElectronPlatformService extends PlatformService {
}
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 {
@@ -178,7 +191,7 @@ export class ElectronPlatformService extends PlatformService {
this.electron.app.exit(0)
}
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
async startUpload (options?: FileUploadOptions, paths?: string[]): Promise<FileUpload[]> {
options ??= { multiple: false }
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
@@ -186,18 +199,21 @@ export class ElectronPlatformService extends PlatformService {
properties.push('multiSelections')
}
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
},
)
if (result.canceled) {
return []
if (!paths) {
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
},
)
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, this.electron)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
@@ -205,17 +221,20 @@ export class ElectronPlatformService extends PlatformService {
}))
}
async startDownload (name: string, mode: number, size: number): Promise<FileDownload|null> {
const result = await this.electron.dialog.showSaveDialog(
this.hostWindow.getWindow(),
{
defaultPath: name,
},
)
if (!result.filePath) {
return null
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {
if (!filePath) {
const result = await this.electron.dialog.showSaveDialog(
this.hostWindow.getWindow(),
{
defaultPath: name,
},
)
if (!result.filePath) {
return null
}
filePath = result.filePath
}
const transfer = new ElectronFileDownload(result.filePath, mode, size, this.electron)
const transfer = new ElectronFileDownload(filePath, mode, size, this.electron)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
return transfer

View File

@@ -126,10 +126,11 @@ export class ElectronUpdaterService extends UpdaterService {
{
type: 'warning',
message: 'Installing the update will close all tabs and restart Tabby.',
buttons: ['Cancel', 'Update'],
defaultId: 1,
buttons: ['Update', 'Cancel'],
defaultId: 0,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
await this.downloaded
this.electron.autoUpdater.quitAndInstall()
}

View 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())
}
}

View File

@@ -16,6 +16,19 @@ async@^3.1.0:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
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:
version "1.0.2"
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"
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:
version "1.0.2"
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"
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:
version "1.1.1"
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-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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@@ -174,7 +209,15 @@ has@^1.0.3:
dependencies:
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"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -267,6 +310,13 @@ logform@^2.2.0:
ms "^2.1.1"
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:
version "2.1.3"
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"
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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
@@ -309,6 +366,11 @@ one-time@^1.0.0:
dependencies:
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:
version "2.0.1"
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"
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:
version "5.1.2"
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"
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:
version "1.3.0"
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"
triple-beam "^1.3.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=

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-local",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Tabby's local shell plugin",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -106,10 +106,11 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `"${children[0].command}" is still running. Close?`,
buttons: ['Cancel', 'Kill'],
defaultId: 1,
buttons: ['Kill', 'Cancel'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
ngOnDestroy (): void {

View File

@@ -84,6 +84,10 @@ export class LocalProfilesService extends ProfileProvider<LocalProfile> {
}
}
getSuggestedName (profile: LocalProfile): string {
return this.getDescription(profile)
}
getDescription (profile: PartialProfile<LocalProfile>): string {
return profile.options?.command ?? ''
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-plugin-manager",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Tabby's plugin manager",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-serial",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Serial connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -25,10 +25,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
xon: false,
xoff: false,
xany: false,
inputMode: 'local-echo',
inputMode: null,
outputMode: null,
inputNewlines: null,
outputNewlines: 'crlf',
outputNewlines: null,
scripts: [],
},
}
@@ -92,6 +92,10 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
}
}
getSuggestedName (profile: SerialProfile): string {
return this.getDescription(profile)
}
getDescription (profile: SerialProfile): string {
return profile.options.port
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-settings",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Tabby terminal settings page",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -8,11 +8,15 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.header
.title Sync host
input.form-control(
type='text',
[(ngModel)]='config.store.configSync.host',
(ngModelChange)='config.save()',
)
.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
@@ -49,23 +53,24 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
.list-group-light
.list-group-item.d-flex.align-items-center(
*ngFor='let cfg of configs',
[class.active]='cfg.id === config.store.configSync.configID',
[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='cfg.id === config.store.configSync.configID') ACTIVE
.badge.badge-info(*ngIf='isActiveConfig(cfg)') ACTIVE
.mr-auto
button.btn.btn-link.ml-1(
(click)='uploadAndSync(cfg)',
[class.hover-reveal]='cfg.id !== config.store.configSync.configID'
[class.hover-reveal]='!isActiveConfig(cfg)'
)
i.fas.fa-arrow-up
span.ml-2 Upload
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]='cfg.id !== config.store.configSync.configID'
[class.hover-reveal]='!isActiveConfig(cfg)'
)
i.fas.fa-arrow-down
span.ml-2 Download
@@ -76,7 +81,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
i.fas.fa-fw.fa-cloud-upload-alt
.ml-2 Upload as a new config
ng-container(*ngIf='config.store.configSync.configID')
ng-container(*ngIf='hasMatchingRemoteConfig()')
.form-line
.header
.title Sync automatically

View File

@@ -17,10 +17,10 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
constructor (
public config: ConfigService,
public platform: PlatformService,
private configSync: ConfigSyncService,
private hostApp: HostAppService,
private ngbModal: NgbModal,
private platform: PlatformService,
private notifications: NotificationsService,
) {
super()
@@ -73,6 +73,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
message: 'Overwrite the config on the remote side and start syncing?',
buttons: ['Overwrite remote and sync', 'Cancel'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}
@@ -89,6 +90,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
message: 'Overwrite the local config and start syncing?',
buttons: ['Overwrite local and sync', 'Cancel'],
defaultId: 1,
cancelId: 1,
})).response === 1) {
return
}
@@ -96,4 +98,12 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
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
}
}

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { trigger, transition, style, animate } from '@angular/animations'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { HotkeysService, BaseComponent } from 'tabby-core'
import { HotkeysService, BaseComponent, Keystroke } from 'tabby-core'
const INPUT_TIMEOUT = 1000
@@ -36,7 +36,7 @@ const INPUT_TIMEOUT = 1000
],
})
export class HotkeyInputModalComponent extends BaseComponent {
@Input() value: string[] = []
@Input() value: Keystroke[] = []
@Input() timeoutProgress = 0
private lastKeyEvent: number|null = null
@@ -48,12 +48,14 @@ export class HotkeyInputModalComponent extends BaseComponent {
) {
super()
this.hotkeys.clearCurrentKeystrokes()
this.subscribeUntilDestroyed(hotkeys.key, (event) => {
this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes()
this.subscribeUntilDestroyed(hotkeys.keyEvent$, event => {
event.preventDefault()
event.stopPropagation()
})
this.subscribeUntilDestroyed(hotkeys.keystroke$, keystroke => {
this.lastKeyEvent = performance.now()
this.value.push(keystroke)
})
}
splitKeys (keys: string): string[] {

View File

@@ -63,18 +63,31 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
})),
)
}
const profile = deepClone(base)
profile.id = null
profile.name = ''
const profile: PartialProfile<Profile> = deepClone(base)
delete profile.id
if (base.isTemplate) {
profile.name = ''
} else if (!base.isBuiltin) {
profile.name = `${base.name} copy`
}
profile.isBuiltin = false
profile.isTemplate = false
await this.editProfile(profile)
await this.showProfileEditModal(profile)
if (!profile.name) {
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? `${base.name} copy`
}
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
this.config.store.profiles = [profile, ...this.config.store.profiles]
await this.config.save()
}
async editProfile (profile: PartialProfile<Profile>): Promise<void> {
await this.showProfileEditModal(profile)
await this.config.save()
}
async showProfileEditModal (profile: PartialProfile<Profile>): Promise<void> {
const modal = this.ngbModal.open(
EditProfileModalComponent,
{ size: 'lg' },
@@ -89,8 +102,6 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
delete profile[k]
}
Object.assign(profile, result)
await this.config.save()
}
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
@@ -98,10 +109,11 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: `Delete "${profile.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
this.profilesService.providerForProfile(profile)?.deleteProfile(
this.profilesService.getConfigProxyForProfile(profile))
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
@@ -156,16 +168,18 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: `Delete "${group.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete the group's profiles?`,
buttons: ['Move to "Ungrouped"', 'Delete'],
defaultId: 0,
cancelId: 0,
}
)).response === 0) {
for (const profile of this.profiles.filter(x => x.group === group.name)) {

View File

@@ -29,7 +29,7 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
i.fas.fa-book
span What's new
button.btn.btn-secondary(
button.btn.btn-secondary.mr-3.mb-2(
*ngIf='!updateAvailable && hostApp.platform !== Platform.Web',
(click)='checkForUpdates()',
[disabled]='checkingForUpdate'
@@ -39,7 +39,7 @@ button.btn.btn-warning.btn-block(*ngIf='config.restartRequested', '(click)'='res
)
span Check for updates
button.btn.btn-info(
button.btn.btn-info.mr-3.mb-2(
*ngIf='updateAvailable',
(click)='updater.update()',
)

View File

@@ -42,10 +42,11 @@ export class VaultSettingsTabComponent extends BaseComponent {
{
type: 'warning',
message: 'Delete vault contents?',
buttons: ['Keep', 'Delete'],
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
await this.vault.setEnabled(false)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-ssh",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "SSH connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -1,5 +1,5 @@
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
export const supportedAlgorithms: Record<string, string> = {}

View File

@@ -0,0 +1,12 @@
import { MenuItemOptions } from 'tabby-core'
import { SFTPFile } from '../session/sftp'
import { SFTPPanelComponent } from '../components/sftpPanel.component'
/**
* Extend to add items to the SFTPPanel context menu
*/
export abstract class SFTPContextMenuItemProvider {
weight = 0
abstract getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]>
}

View File

@@ -0,0 +1,2 @@
export * from './contextMenu'
export * from './interfaces'

View File

@@ -0,0 +1,53 @@
import { Profile } from 'tabby-core'
import { LoginScriptsOptions } from 'tabby-terminal'
export enum SSHAlgorithmType {
HMAC = 'hmac',
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey',
}
export interface SSHProfile extends Profile {
options: SSHProfileOptions
}
export interface SSHProfileOptions extends LoginScriptsOptions {
host: string
port?: number
user: string
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
password?: string
privateKeys?: string[]
keepaliveInterval?: number
keepaliveCountMax?: number
readyTimeout?: number
x11?: boolean
skipBanner?: boolean
jumpHost?: string
agentForward?: boolean
warnOnClose?: boolean
algorithms?: Record<string, string[]>
proxyCommand?: string
forwardedPorts?: ForwardedPortConfig[]
}
export enum PortForwardType {
Local = 'Local',
Remote = 'Remote',
Dynamic = 'Dynamic',
}
export interface ForwardedPortConfig {
type: PortForwardType
host: string
port: number
targetAddress: string
targetPort: number
}
export const ALGORITHM_BLACKLIST = [
// cause native crashes in node crypto, use EC instead
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group-exchange-sha1',
]

View File

@@ -1,18 +1,16 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { SSHSession } from '../api'
import { SFTPSession, SFTPFile } from '../session/sftp'
import { posix as path } from 'path'
import * as C from 'constants'
import { FileUpload, PlatformService } from 'tabby-core'
import { SFTPDeleteModalComponent } from './sftpDeleteModal.component'
import { posix as path } from 'path'
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
import { FileUpload, MenuItemOptions, PlatformService } from 'tabby-core'
import { SFTPSession, SFTPFile } from '../session/sftp'
import { SSHSession } from '../session/ssh'
import { SFTPContextMenuItemProvider } from '../api'
interface PathSegment {
name: string
path: string
}
/** @hidden */
@Component({
selector: 'sftp-panel',
template: require('./sftpPanel.component.pug'),
@@ -29,8 +27,10 @@ export class SFTPPanelComponent {
constructor (
private platform: PlatformService,
private ngbModal: NgbModal,
) { }
@Optional() @Inject(SFTPContextMenuItemProvider) protected contextMenuProviders: SFTPContextMenuItemProvider[],
) {
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
}
async ngOnInit (): Promise<void> {
this.sftp = await this.session.openSFTP()
@@ -96,28 +96,10 @@ export class SFTPPanelComponent {
}
async uploadOne (transfer: FileUpload): Promise<void> {
const itemPath = path.join(this.path, transfer.getName())
const tempPath = itemPath + '.tabby-upload'
this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
const savedPath = this.path
try {
const handle = await this.sftp.open(tempPath, 'w')
while (true) {
const chunk = await transfer.read()
if (!chunk.length) {
break
}
await handle.write(chunk)
}
handle.close()
await this.sftp.rename(tempPath, itemPath)
transfer.close()
if (this.path === savedPath) {
await this.navigate(this.path)
}
} catch (e) {
transfer.cancel()
this.sftp.unlink(tempPath)
throw e
if (this.path === savedPath) {
await this.navigate(this.path)
}
}
@@ -126,21 +108,7 @@ export class SFTPPanelComponent {
if (!transfer) {
return
}
try {
const handle = await this.sftp.open(itemPath, 'r')
while (true) {
const chunk = await handle.read()
if (!chunk.length) {
break
}
await transfer.write(chunk)
}
transfer.close()
handle.close()
} catch (e) {
transfer.cancel()
throw e
}
this.sftp.download(itemPath, transfer)
}
getModeString (item: SFTPFile): string {
@@ -159,31 +127,18 @@ export class SFTPPanelComponent {
return result
}
showContextMenu (item: SFTPFile, event: MouseEvent): void {
event.preventDefault()
this.platform.popupContextMenu([
{
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: `Delete ${item.fullPath}?`,
defaultId: 0,
buttons: ['Delete', 'Cancel'],
})).response === 0) {
await this.deleteItem(item)
this.navigate(this.path)
}
},
label: 'Delete',
},
], event)
async buildContextMenu (item: SFTPFile): Promise<MenuItemOptions[]> {
let items: MenuItemOptions[] = []
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(item, this)))) {
items.push({ type: 'separator' })
items = items.concat(section)
}
return items.slice(1)
}
async deleteItem (item: SFTPFile): Promise<void> {
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
modal.componentInstance.item = item
modal.componentInstance.sftp = this.sftp
await modal.result
async showContextMenu (item: SFTPFile, event: MouseEvent): Promise<void> {
event.preventDefault()
this.platform.popupContextMenu(await this.buildContextMenu(item), event)
}
close (): void {

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input } from '@angular/core'
import { ForwardedPort, ForwardedPortConfig, SSHSession } from '../api'
import { ForwardedPort } from '../session/forwards'
import { SSHSession } from '../session/ssh'
import { ForwardedPortConfig } from '../api'
/** @hidden */
@Component({

View File

@@ -6,7 +6,12 @@
i.fas.fa-xs.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open')
strong.mr-auto {{profile.options.user}}@{{profile.options.host}}:{{profile.options.port}}
.mr-2(ngbDropdown, *ngIf='session && !session.supportsWorkingDirectory()', placement='bottom-right')
.mr-2(
ngbDropdown,
container='body',
*ngIf='session && !session.supportsWorkingDirectory()',
placement='bottom-right'
)
button.btn.btn-sm.btn-link(ngbDropdownToggle)
i.far.fa-lightbulb
.bg-dark(ngbDropdownMenu)

View File

@@ -5,8 +5,9 @@ import { first } from 'rxjs'
import { Platform, RecoveryToken } from 'tabby-core'
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHProfile, SSHSession } from '../api'
import { SSHSession } from '../session/ssh'
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
import { SSHProfile } from '../api'
/** @hidden */
@@ -208,10 +209,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `Disconnect from ${this.profile?.options.host}?`,
buttons: ['Cancel', 'Disconnect'],
defaultId: 1,
buttons: ['Disconnect', 'Do not close'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
async openSFTP (): Promise<void> {

View File

@@ -22,6 +22,8 @@ import { RecoveryProvider } from './recoveryProvider'
import { SSHHotkeyProvider } from './hotkeys'
import { SFTPContextMenu } from './tabContextMenu'
import { SSHProfilesService } from './profiles'
import { SFTPContextMenuItemProvider } from './api/contextMenu'
import { CommonSFTPContextMenu } from './sftpContextMenu'
/** @hidden */
@NgModule({
@@ -41,6 +43,7 @@ import { SSHProfilesService } from './profiles'
{ provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: SFTPContextMenu, multi: true },
{ provide: ProfileProvider, useExisting: SSHProfilesService, multi: true },
{ provide: SFTPContextMenuItemProvider, useClass: CommonSFTPContextMenu, multi: true },
],
entryComponents: [
SSHProfileSettingsComponent,
@@ -105,3 +108,7 @@ export default class SSHModule {
await this.selector.show('Select an SSH profile', options)
}
}
export * from './api'
export { SFTPFile, SFTPSession } from './session/sftp'
export { SFTPPanelComponent, SFTPContextMenuItemProvider }

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core'
import { ProfileProvider, NewTabParameters, PartialProfile } from 'tabby-core'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
import { SSHTabComponent } from './components/sshTab.component'
import { PasswordStorageService } from './services/passwordStorage.service'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
@Injectable({ providedIn: 'root' })
@@ -81,6 +81,10 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
}
}
getSuggestedName (profile: SSHProfile): string {
return `${profile.options.user}@${profile.options.host}:${profile.options.port}`
}
getDescription (profile: PartialProfile<SSHProfile>): string {
return profile.options?.host ?? ''
}

View File

@@ -1,7 +1,7 @@
import * as keytar from 'keytar'
import { Injectable } from '@angular/core'
import { SSHProfile } from '../api'
import { VaultService } from 'tabby-core'
import { SSHProfile } from '../api'
export const VAULT_SECRET_TYPE_PASSWORD = 'ssh:password'
export const VAULT_SECRET_TYPE_PASSPHRASE = 'ssh:key-passphrase'

View File

@@ -4,11 +4,13 @@ import { Injectable, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Client } from 'ssh2'
import { exec } from 'child_process'
import { ChildProcess } from 'node:child_process'
import { Subject, Observable } from 'rxjs'
import { Logger, LogService, ConfigService, NotificationsService, HostAppService, Platform, PlatformService, PromptModalComponent } from 'tabby-core'
import { ALGORITHM_BLACKLIST, ForwardedPort, SSHAlgorithmType, SSHProfile, SSHSession } from '../api'
import { SSHSession } from '../session/ssh'
import { ForwardedPort } from '../session/forwards'
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from '../api'
import { PasswordStorageService } from './passwordStorage.service'
import { ChildProcess } from 'node:child_process'
@Injectable({ providedIn: 'root' })
export class SSHService {

View File

@@ -0,0 +1,64 @@
import socksv5 from 'socksv5'
import { Server, Socket, createServer } from 'net'
import { ForwardedPortConfig, PortForwardType } from '../api'
export class ForwardedPort implements ForwardedPortConfig {
type: PortForwardType
host = '127.0.0.1'
port: number
targetAddress: string
targetPort: number
private listener: Server|null = null
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
if (this.type === PortForwardType.Local) {
const listener = this.listener = createServer(s => callback(
() => s,
() => s.destroy(),
s.remoteAddress ?? null,
s.remotePort ?? null,
this.targetAddress,
this.targetPort,
))
return new Promise((resolve, reject) => {
listener.listen(this.port, this.host)
listener.on('error', reject)
listener.on('listening', resolve)
})
} else if (this.type === PortForwardType.Dynamic) {
return new Promise((resolve, reject) => {
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
callback(
() => acceptConnection(true),
() => rejectConnection(),
null,
null,
info.dstAddr,
info.dstPort,
)
}) as Server
this.listener.on('error', reject)
this.listener.listen(this.port, this.host, resolve)
this.listener['useAuth'](socksv5.auth.None())
})
} else {
throw new Error('Invalid forward type for a local listener')
}
}
stopLocalListener (): void {
this.listener?.close()
}
toString (): string {
if (this.type === PortForwardType.Local) {
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
} if (this.type === PortForwardType.Remote) {
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
} else {
return `(dynamic) ${this.host}:${this.port}`
}
}
}

View File

@@ -1,8 +1,9 @@
import * as C from 'constants'
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
import { Subject, Observable } from 'rxjs'
import { posix as posixPath } from 'path'
import { NgZone } from '@angular/core'
import { wrapPromise } from 'tabby-core'
import { Injector, NgZone } from '@angular/core'
import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core'
import { SFTPWrapper } from 'ssh2'
import { promisify } from 'util'
@@ -70,9 +71,22 @@ export class SFTPFileHandle {
}
export class SFTPSession {
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
get closed$ (): Observable<void> { return this.closed }
private closed = new Subject<void>()
private zone: NgZone
private logger: Logger
constructor (private sftp: SFTPWrapper, injector: Injector) {
this.zone = injector.get(NgZone)
this.logger = injector.get(LogService).create('sftp')
sftp.on('close', () => {
this.closed.next()
this.closed.complete()
})
}
async readdir (p: string): Promise<SFTPFile[]> {
this.logger.debug('readdir', p)
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
return entries.map(entry => this._makeFile(
posixPath.join(p, entry.filename), entry,
@@ -80,10 +94,12 @@ export class SFTPSession {
}
readlink (p: string): Promise<string> {
this.logger.debug('readlink', p)
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
}
async stat (p: string): Promise<SFTPFile> {
this.logger.debug('stat', p)
const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
return {
name: posixPath.basename(p),
@@ -97,22 +113,68 @@ export class SFTPSession {
}
async open (p: string, mode: string): Promise<SFTPFileHandle> {
this.logger.debug('open', p)
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
return new SFTPFileHandle(this.sftp, handle, this.zone)
}
async rmdir (p: string): Promise<void> {
this.logger.debug('rmdir', p)
await promisify((f: any) => this.sftp.rmdir(p, f))()
}
async rename (oldPath: string, newPath: string): Promise<void> {
this.logger.debug('rename', oldPath, newPath)
await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
}
async unlink (p: string): Promise<void> {
this.logger.debug('unlink', p)
await promisify((f: any) => this.sftp.unlink(p, f))()
}
async upload (path: string, transfer: FileUpload): Promise<void> {
this.logger.info('Uploading into', path)
const tempPath = path + '.tabby-upload'
try {
const handle = await this.open(tempPath, 'w')
while (true) {
const chunk = await transfer.read()
if (!chunk.length) {
break
}
await handle.write(chunk)
}
handle.close()
await this.unlink(path)
await this.rename(tempPath, path)
transfer.close()
} catch (e) {
transfer.cancel()
this.unlink(tempPath)
throw e
}
}
async download (path: string, transfer: FileDownload): Promise<void> {
this.logger.info('Downloading', path)
try {
const handle = await this.open(path, 'r')
while (true) {
const chunk = await handle.read()
if (!chunk.length) {
break
}
await transfer.write(chunk)
}
transfer.close()
handle.close()
} catch (e) {
transfer.cancel()
throw e
}
}
private _makeFile (p: string, entry: FileEntry): SFTPFile {
return {
fullPath: p,

View File

@@ -5,126 +5,22 @@ import * as path from 'path'
import * as sshpk from 'sshpk'
import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi'
import socksv5 from 'socksv5'
import { Injector, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, Profile, LogService } from 'tabby-core'
import { BaseSession, LoginScriptsOptions } from 'tabby-terminal'
import { Server, Socket, createServer, createConnection } from 'net'
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService } from 'tabby-core'
import { BaseSession } from 'tabby-terminal'
import { Socket, createConnection } from 'net'
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
import { Subject, Observable } from 'rxjs'
import { ProxyCommandStream } from './services/ssh.service'
import { PasswordStorageService } from './services/passwordStorage.service'
import { ProxyCommandStream } from '../services/ssh.service'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { promisify } from 'util'
import { SFTPSession } from './session/sftp'
import { SFTPSession } from './sftp'
import { PortForwardType, SSHProfile } from '../api/interfaces'
import { ForwardedPort } from './forwards'
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
export enum SSHAlgorithmType {
HMAC = 'hmac',
KEX = 'kex',
CIPHER = 'cipher',
HOSTKEY = 'serverHostKey',
}
export interface SSHProfile extends Profile {
options: SSHProfileOptions
}
export interface SSHProfileOptions extends LoginScriptsOptions {
host: string
port?: number
user: string
auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive'
password?: string
privateKeys?: string[]
keepaliveInterval?: number
keepaliveCountMax?: number
readyTimeout?: number
x11?: boolean
skipBanner?: boolean
jumpHost?: string
agentForward?: boolean
warnOnClose?: boolean
algorithms?: Record<string, string[]>
proxyCommand?: string
forwardedPorts?: ForwardedPortConfig[]
}
export enum PortForwardType {
Local = 'Local',
Remote = 'Remote',
Dynamic = 'Dynamic',
}
export interface ForwardedPortConfig {
type: PortForwardType
host: string
port: number
targetAddress: string
targetPort: number
}
export class ForwardedPort implements ForwardedPortConfig {
type: PortForwardType
host = '127.0.0.1'
port: number
targetAddress: string
targetPort: number
private listener: Server|null = null
async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise<void> {
if (this.type === PortForwardType.Local) {
const listener = this.listener = createServer(s => callback(
() => s,
() => s.destroy(),
s.remoteAddress ?? null,
s.remotePort ?? null,
this.targetAddress,
this.targetPort,
))
return new Promise((resolve, reject) => {
listener.listen(this.port, this.host)
listener.on('error', reject)
listener.on('listening', resolve)
})
} else if (this.type === PortForwardType.Dynamic) {
return new Promise((resolve, reject) => {
this.listener = socksv5.createServer((info, acceptConnection, rejectConnection) => {
callback(
() => acceptConnection(true),
() => rejectConnection(),
null,
null,
info.dstAddr,
info.dstPort,
)
}) as Server
this.listener.on('error', reject)
this.listener.listen(this.port, this.host, resolve)
this.listener['useAuth'](socksv5.auth.None())
})
} else {
throw new Error('Invalid forward type for a local listener')
}
}
stopLocalListener (): void {
this.listener?.close()
}
toString (): string {
if (this.type === PortForwardType.Local) {
return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}`
} if (this.type === PortForwardType.Remote) {
return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}`
} else {
return `(dynamic) ${this.host}:${this.port}`
}
}
}
interface AuthMethod {
type: 'none'|'publickey'|'agent'|'password'|'keyboard-interactive'|'hostbased'
name?: string
@@ -158,7 +54,7 @@ export class SSHSession extends BaseSession {
private config: ConfigService
constructor (
injector: Injector,
private injector: Injector,
public profile: SSHProfile,
) {
super(injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`))
@@ -241,7 +137,7 @@ export class SSHSession extends BaseSession {
if (!this.sftp) {
this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
}
return new SFTPSession(this.sftp, this.zone)
return new SFTPSession(this.sftp, this.injector)
}
async start (): Promise<void> {
@@ -613,9 +509,3 @@ export class SSHSession extends BaseSession {
}
}
}
export const ALGORITHM_BLACKLIST = [
// cause native crashes in node crypto, use EC instead
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group-exchange-sha1',
]

View File

@@ -0,0 +1,48 @@
import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { MenuItemOptions, PlatformService } from 'tabby-core'
import { SFTPSession, SFTPFile } from './session/sftp'
import { SFTPContextMenuItemProvider } from './api'
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
import { SFTPPanelComponent } from './components/sftpPanel.component'
/** @hidden */
@Injectable()
export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
weight = 10
constructor (
private platform: PlatformService,
private ngbModal: NgbModal,
) {
super()
}
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
return [
{
click: async () => {
if ((await this.platform.showMessageBox({
type: 'warning',
message: `Delete ${item.fullPath}?`,
defaultId: 0,
cancelId: 1,
buttons: ['Delete', 'Cancel'],
})).response === 0) {
await this.deleteItem(item, panel.sftp)
panel.navigate(panel.path)
}
},
label: 'Delete',
},
]
}
async deleteItem (item: SFTPFile, session: SFTPSession): Promise<void> {
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
modal.componentInstance.item = item
modal.componentInstance.sftp = session
await modal.result
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-telnet",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Telnet/socket connections for Tabby",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -119,9 +119,10 @@ export class TelnetTabComponent extends BaseTerminalTabComponent {
{
type: 'warning',
message: `Disconnect from ${this.profile?.options.host}?`,
buttons: ['Cancel', 'Disconnect'],
defaultId: 1,
buttons: ['Disconnect', 'Do not close'],
defaultId: 0,
cancelId: 1,
}
)).response === 1
)).response === 0
}
}

View File

@@ -62,6 +62,10 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
}
}
getSuggestedName (profile: TelnetProfile): string|null {
return this.getDescription(profile) || null
}
getDescription (profile: TelnetProfile): string {
return profile.options.host ? `${profile.options.host}:${profile.options.port}` : ''
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-terminal",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Tabby's terminal emulation core",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -98,9 +98,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
enablePassthrough = true
/**
* Enables receiving dynamic window/tab title provided by the shell
* Disables display of dynamic window/tab title provided by the shell
*/
enableDynamicTitle = true
disableDynamicTitle = false
alternateScreenActive = false
@@ -315,12 +315,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
setImmediate(async () => {
if (this.hasFocus) {
await this.frontend!.attach(this.content.nativeElement)
this.frontend!.configure()
await this.frontend?.attach(this.content.nativeElement)
this.frontend?.configure()
} else {
this.focused$.pipe(first()).subscribe(async () => {
await this.frontend!.attach(this.content.nativeElement)
this.frontend!.configure()
await this.frontend?.attach(this.content.nativeElement)
this.frontend?.configure()
})
}
})
@@ -445,6 +445,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
message: `Paste multiple lines?`,
buttons,
defaultId: 0,
cancelId: 1,
}
)).response
if (result === 1) {
@@ -585,7 +586,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => {
if (this.enableDynamicTitle) {
if (!this.disableDynamicTitle) {
this.setTitle(title)
}
}))

View File

@@ -45,6 +45,12 @@
will-change: transform;
transform: translate(0, -100px);
transition: 0.25s transform ease-out;
> .btn {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
&.toolbar-revealed, &.toolbar-pinned {

View File

@@ -79,10 +79,11 @@ export class ColorSchemeSettingsTabComponent {
{
type: 'warning',
message: `Delete "${scheme.name}"?`,
buttons: ['Keep', 'Delete'],
buttons: ['Delete', 'Keep'],
defaultId: 1,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
this.customColorSchemes = this.customColorSchemes.filter(x => x.name !== scheme.name)
this.config.store.terminal.customColorSchemes = this.customColorSchemes
this.config.save()

View File

@@ -27,10 +27,11 @@ export class LoginScriptsSettingsComponent {
type: 'warning',
message: 'Delete this script?',
detail: script.expect,
buttons: ['Keep', 'Delete'],
defaultId: 1,
buttons: ['Delete', 'Keep'],
defaultId: 0,
cancelId: 1,
}
)).response === 1) {
)).response === 0) {
this.scripts = this.scripts.filter(x => x !== script)
}
}

View File

@@ -85,16 +85,13 @@ export class XTermFrontend extends Frontend {
this.xterm.unicode.activeVersion = '11'
const keyboardEventHandler = (name: string, event: KeyboardEvent) => {
this.hotkeysService.pushKeystroke(name, event)
this.hotkeysService.pushKeyEvent(name, event)
let ret = true
if (this.hotkeysService.getCurrentPartiallyMatchedHotkeys().length !== 0) {
if (this.hotkeysService.matchActiveHotkey(true) !== null) {
event.stopPropagation()
event.preventDefault()
ret = false
}
this.hotkeysService.processKeystrokes()
this.hotkeysService.emitKeyEvent(event)
return ret
}

View File

@@ -97,18 +97,16 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
htermHandler: 'onKeyUp_',
},
]
events.forEach((event) => {
events.forEach(event => {
const oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
hotkeys.pushKeystroke(event.name, nativeEvent)
if (hotkeys.getCurrentPartiallyMatchedHotkeys().length === 0) {
hotkeys.pushKeyEvent(event.name, nativeEvent)
if (hotkeys.matchActiveHotkey(true) !== null) {
oldHandler.bind(this)(nativeEvent)
} else {
nativeEvent.stopPropagation()
nativeEvent.preventDefault()
}
hotkeys.processKeystrokes()
hotkeys.emitKeyEvent(nativeEvent)
}
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-web-demo",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {

View File

@@ -64,7 +64,7 @@ export class Session extends BaseSession {
}, 2000)
})
this.vm.add_listener('download-progress', (e) => {
this.emitMessage(`\rDownloading ${path.basename(e.file_name)}: ${e.loaded / 1024}/${e.total / 1024} kB `)
this.emitMessage(`\rDownloading ${path.basename(e.file_name)}: ${Math.floor(e.loaded / 1024)}/${Math.floor(e.total / 1024)} kB `)
})
this.vm.add_listener('download-error', (e) => {
this.emitMessage(`\r\nDownload error: ${e}\r\n`)

View File

@@ -1,6 +1,6 @@
{
"name": "tabby-web",
"version": "1.0.149-nightly.4",
"version": "1.0.150",
"description": "Web-specific bindings",
"keywords": [
"tabby-builtin-plugin"

View File

@@ -100,7 +100,7 @@ export class WebPlatformService extends PlatformService {
const response = await modal.result
return { response }
} catch {
return { response: 0 }
return { response: options.cancelId ?? 1 }
}
}

View File

@@ -16,5 +16,5 @@
"resolutions": {
"**/util": "^0.12.0"
},
"version": "1.0.149-nightly.4"
"version": "1.0.150"
}

View File

@@ -64,22 +64,8 @@ module.exports = {
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
test: /\.(png|svg)$/,
use: {
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]',
},
},
test: /\.(png|svg|ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
type: 'asset',
},
],
},

View File

@@ -87,12 +87,7 @@ module.exports = options => {
{ test: /\.svg/, use: ['svg-inline-loader'] },
{
test: /\.(ttf|eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: 'url-loader',
options: {
limit: 999999999999,
},
},
type: 'asset',
},
],
},

147
yarn.lock
View File

@@ -788,6 +788,11 @@ acorn-globals@^3.0.0:
dependencies:
acorn "^4.0.4"
acorn-import-assertions@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78"
integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==
acorn-jsx@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
@@ -1225,7 +1230,7 @@ big.js@^3.1.3:
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
binaryextensions@^4.15.0:
@@ -2077,20 +2082,18 @@ crypto-random-string@^2.0.0:
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-loader@5.2.6:
version "5.2.6"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1"
integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w==
css-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071"
integrity sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g==
dependencies:
icss-utils "^5.1.0"
loader-utils "^2.0.0"
postcss "^8.2.15"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.1.0"
schema-utils "^3.0.0"
semver "^7.3.5"
css-selector-parser@^1.1.0:
@@ -2100,7 +2103,7 @@ css-selector-parser@^1.1.0:
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
currently-unhandled@^0.4.1:
@@ -2502,10 +2505,10 @@ electron-localshortcut@^3.1.0:
keyboardevent-from-electron-accelerator "^2.0.0"
keyboardevents-areequal "^0.2.1"
electron-notarize@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.0.0.tgz"
integrity sha512-dsib1IAquMn0onCrNMJ6gtEIZn/azG8hZMCYOuZIMVMUeRMgBYHK1s5TK9P8xAcrAjh/2aN5WYHzgVSWX314og==
electron-notarize@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.0.1.tgz#97e6ab57fdc32abc6dfa88d3edbb5037189a9fb1"
integrity sha512-5B0ToIuuqb+Uzq3Kvs7BReUh52WRELmy8dHWusQwXgksYm2RgzsFFGNhv9eAmzuzXNW4xPgUbdCmYrcVGSlXIg==
dependencies:
debug "^4.1.1"
fs-extra "^9.0.1"
@@ -2547,10 +2550,10 @@ electron-to-chromium@^1.3.723:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz#f632d900a1f788dab22fec9c62ec5c9c8f0c4052"
integrity sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==
electron@13.1.7:
version "13.1.7"
resolved "https://registry.yarnpkg.com/electron/-/electron-13.1.7.tgz#7e17f5c93a8d182a2a486884fed3dc34ab101be9"
integrity sha512-sVfpP/0s6a82FK32LMuEe9L+aWZw15u3uYn9xUJArPjy4OZHteE6yM5871YCNXNiDnoCLQ5eqQWipiVgHsf8nQ==
electron@13.1.8:
version "13.1.8"
resolved "https://registry.yarnpkg.com/electron/-/electron-13.1.8.tgz#a6def6eca7cafc7b068a8f71a069e521ba803182"
integrity sha512-ei2ZyyG81zUOlvm5Zxri668TdH5GNLY0wF+XrC2FRCqa8AABAPjJIWTRkhFEr/H6PDVPNZjMPvSs3XhHyVVk2g==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^14.6.2"
@@ -2573,7 +2576,7 @@ emojis-list@^2.0.0:
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
encodeurl@^1.0.2:
@@ -2816,10 +2819,10 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint@^7.31.0:
version "7.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==
eslint@^7.32.0:
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.3"
@@ -3869,11 +3872,6 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
inflight@^1.0.4, inflight@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -4402,9 +4400,9 @@ json5@^1.0.1:
minimist "^1.2.0"
json5@^2.1.2:
version "2.1.3"
resolved "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz"
integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
version "2.2.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
dependencies:
minimist "^1.2.5"
@@ -4610,7 +4608,7 @@ loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.4.0:
loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
dependencies:
big.js "^5.2.2"
@@ -6014,24 +6012,22 @@ postcss-modules-values@^4.0.0:
icss-utils "^5.0.0"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
version "6.0.4"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz"
integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
version "6.0.6"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
dependencies:
cssesc "^3.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
util-deprecate "^1.0.2"
postcss-value-parser@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@^8.2.15:
version "8.3.0"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
version "8.3.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.23"
@@ -7153,10 +7149,10 @@ slide@^1.1.3, slide@^1.1.5, slide@~1.1.3, slide@~1.1.6:
resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz"
integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
slugify@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.5.3.tgz#36e009864f5476bfd5db681222643d92339c890d"
integrity sha512-/HkjRdwPY3yHJReXu38NiusZw2+LLE2SrhkWJtmlPDB1fqFSvioYj62NkPcrKiNCgRLeGcGK7QBvr1iQwybeXw==
slugify@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.0.tgz#6bdf8ed01dabfdc46425b67e3320b698832ff893"
integrity sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang==
smart-buffer@^1.0.13:
version "1.1.15"
@@ -7214,11 +7210,6 @@ source-code-pro@^2.38.0:
resolved "https://registry.yarnpkg.com/source-code-pro/-/source-code-pro-2.38.0.tgz#85c57689f7386bb9d0515fb00ba4845bfb7b485b"
integrity sha512-JMXu7l3XrLREG17eEwY66ANG9716WTu6OeNvZfRKQKANEvbSERDZjk5AYTHeV6owQNPQTeiiW3ri2Ou93XFGvg==
source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
source-map-js@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
@@ -7309,10 +7300,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
ssh2@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.1.0.tgz#43dd24930e15e317687f519d6b40270d9cd00d00"
integrity sha512-CidQLG2ZacoT0Z7O6dOyisj4JdrOrLVJ4KbHjVNz9yI1vO08FAYQPcnkXY9BP8zeYo+J/nBgY6Gg4R7w4WFWtg==
ssh2@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.2.0.tgz#177a46bb12b7ef2b7bce28bdcbd7eae3cbc50045"
integrity sha512-vklfVRyylayGV/zMwVEkTC9kBhA3t264hoUHV/yGuunBJh6uBGP1VlzhOp8EsqxpKnG0xkLE1qHZlU0+t8Vh6Q==
dependencies:
asn1 "^0.2.4"
bcrypt-pbkdf "^1.0.2"
@@ -7549,10 +7540,10 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-loader@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.1.0.tgz#e52a22ca2fd1f54c03f2c3e69b10182e68120908"
integrity sha512-HYVvBMX3RX7zx71pquZV6EcnPN7Deba+zQteSxCLqt3bxYRphmeMr+2mZMrIZjZ7IMa6aOUhNGn8cXGvWMjClw==
style-loader@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.2.1.tgz#63cb920ec145c8669e9a50e92961452a1ef5dcde"
integrity sha512-1k9ZosJCRFaRbY6hH49JFlRB0fVSbmnyq1iTPjNxUmGVjBNEmwrrHPenhlp+Lgo51BojHSf6pl2FcqYaN3PfVg==
sumchecker@^2.0.2:
version "2.0.2"
@@ -7983,10 +7974,10 @@ typedoc-default-themes@^0.12.10:
resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843"
integrity sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==
typedoc@^0.21.4:
version "0.21.4"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.21.4.tgz#fced3cffdc30180db60a5dbfec9dbbb273cb5b31"
integrity sha512-slZQhvD9U0d9KacktYAyuNMMOXJRFNHy+Gd8xY2Qrqq3eTTTv3frv3N4au/cFnab9t3T5WA0Orb6QUjMc+1bDA==
typedoc@^0.21.5:
version "0.21.5"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.21.5.tgz#45643618ede5c3d75e2040b964d05fcffed7ca58"
integrity sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw==
dependencies:
glob "^7.1.7"
handlebars "^4.7.7"
@@ -8045,11 +8036,6 @@ unbox-primitive@^1.0.0:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz"
integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
unique-filename@^1.1.0, unique-filename@~1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz"
@@ -8155,15 +8141,6 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
url-loader@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==
dependencies:
loader-utils "^2.0.0"
mime-types "^2.1.27"
schema-utils "^3.0.0"
url-parse-lax@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz"
@@ -8321,18 +8298,15 @@ webpack-merge@^5.7.3:
clone-deep "^4.0.1"
wildcard "^2.0.0"
webpack-sources@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd"
integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==
dependencies:
source-list-map "^2.0.1"
source-map "^0.6.1"
webpack-sources@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
webpack@^5.46.0:
version "5.46.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.46.0.tgz#105d20d96f79db59b316b0ae54316f0f630314b5"
integrity sha512-qxD0t/KTedJbpcXUmvMxY5PUvXDbF8LsThCzqomeGaDlCA6k998D8yYVwZMvO8sSM3BTEOaD4uzFniwpHaTIJw==
webpack@^5.48.0:
version "5.48.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.48.0.tgz#06180fef9767a6fd066889559a4c4d49bee19b83"
integrity sha512-CGe+nfbHrYzbk7SKoYITCgN3LRAG0yVddjNUecz9uugo1QtYdiyrVD8nP1PhkNqPfdxC2hknmmKpP355Epyn6A==
dependencies:
"@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.50"
@@ -8340,6 +8314,7 @@ webpack@^5.46.0:
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
acorn "^8.4.1"
acorn-import-assertions "^1.7.6"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.8.0"
@@ -8356,7 +8331,7 @@ webpack@^5.46.0:
tapable "^2.1.1"
terser-webpack-plugin "^5.1.3"
watchpack "^2.2.0"
webpack-sources "^2.3.1"
webpack-sources "^3.2.0"
which-boxed-primitive@^1.0.2:
version "1.0.2"