mirror of
https://github.com/Eugeny/tabby.git
synced 2025-08-19 07:41:53 +00:00
Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
78f25a7679 | ||
![]() |
0c4d8b0784 | ||
![]() |
e2c8093b97 | ||
![]() |
c497a71361 | ||
![]() |
ec2982b1c4 | ||
![]() |
be0aeefdb3 | ||
![]() |
eadd8d563e | ||
![]() |
08f1ad4c75 | ||
![]() |
426606ba06 | ||
![]() |
7b59ba4b73 | ||
![]() |
533837f5b7 | ||
![]() |
3f0db97a68 | ||
![]() |
231594d709 | ||
![]() |
e4ae114c71 | ||
![]() |
20000d16f8 | ||
![]() |
5e0a9b2e52 | ||
![]() |
fa70447223 | ||
![]() |
28b84e38ca | ||
![]() |
3c4a078fa5 | ||
![]() |
52f4e88420 | ||
![]() |
16d9045a80 | ||
![]() |
07d7d8daba | ||
![]() |
b2b9476298 | ||
![]() |
cf7f3dffe3 | ||
![]() |
621005eb27 | ||
![]() |
d46e1de8aa | ||
![]() |
c44f3c5f25 | ||
![]() |
b3f9d48609 | ||
![]() |
edd7e9c7b7 | ||
![]() |
ab8061ab39 | ||
![]() |
c1a1f53707 | ||
![]() |
04097a0ef5 | ||
![]() |
85be974e64 | ||
![]() |
0df5fb4a34 | ||
![]() |
920b2b85b3 | ||
![]() |
4e4788bf57 | ||
![]() |
9aa60a9d0d | ||
![]() |
451ac51520 | ||
![]() |
04084aef33 | ||
![]() |
4198ca3fae | ||
![]() |
3b09dfa145 | ||
![]() |
923b559857 | ||
![]() |
58682b6bf1 | ||
![]() |
88c4198145 | ||
![]() |
a6c535414f | ||
![]() |
6ebb7723ff | ||
![]() |
07dd6600dc | ||
![]() |
cc6cfec907 | ||
![]() |
4ecfcfda36 | ||
![]() |
c5681b1376 | ||
![]() |
1fc57018e3 | ||
![]() |
8b8bacdf69 | ||
![]() |
3aaa419f8b | ||
![]() |
94819019ec | ||
![]() |
7b37035f75 | ||
![]() |
a5ef3507c3 | ||
![]() |
b9c6d30678 | ||
![]() |
009556f984 | ||
![]() |
87007d5ae3 | ||
![]() |
61ea2c77c8 | ||
![]() |
8bfc1dc302 |
@@ -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,
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
21
package.json
21
package.json
@@ -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"
|
||||
|
@@ -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) {
|
@@ -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 = [
|
||||
|
@@ -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"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-core",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.150",
|
||||
"description": "Tabby core",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -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'
|
||||
|
@@ -13,6 +13,7 @@ export interface MessageBoxOptions {
|
||||
detail?: string
|
||||
buttons: string[]
|
||||
defaultId?: number
|
||||
cancelId?: number
|
||||
}
|
||||
|
||||
export interface MessageBoxResult {
|
||||
|
@@ -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 {
|
||||
|
@@ -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',
|
||||
|
@@ -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()
|
||||
|
@@ -1,5 +1,5 @@
|
||||
.modal-body
|
||||
input.form-control(
|
||||
input.form-control.form-control-lg(
|
||||
type='text',
|
||||
[(ngModel)]='filter',
|
||||
autofocus,
|
||||
|
@@ -19,6 +19,5 @@
|
||||
}
|
||||
|
||||
input {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
35
tabby-core/src/components/splitTabPaneLabel.component.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
:host {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, .25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: .125s opacity cubic-bezier(0.86, 0, 0.07, 1);
|
||||
}
|
||||
|
||||
div {
|
||||
background: rgba(0, 0, 0, .7);
|
||||
padding: 20px 30px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
:host.active {
|
||||
opacity: 1;
|
||||
|
||||
> div {
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
cursor: move;
|
||||
}
|
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
80
tabby-core/src/components/splitTabPaneLabel.component.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, HostBinding, ElementRef } from '@angular/core'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'split-tab-pane-label',
|
||||
template: `
|
||||
<div
|
||||
cdkDrag
|
||||
[cdkDragData]='tab'
|
||||
(cdkDragStarted)='onTabDragStart(tab)'
|
||||
(cdkDragEnded)='onTabDragEnd()'
|
||||
>
|
||||
<i class="fa fa-window-maximize mr-3"></i>
|
||||
<label>{{tab.title}}</label>
|
||||
</div>
|
||||
`,
|
||||
styles: [require('./splitTabPaneLabel.component.scss')],
|
||||
})
|
||||
export class SplitTabPaneLabelComponent extends SelfPositioningComponent {
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() parent: BaseTabComponent
|
||||
@HostBinding('class.active') isActive = false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
element: ElementRef,
|
||||
hotkeys: HotkeysService,
|
||||
private app: AppService,
|
||||
) {
|
||||
super(element)
|
||||
this.subscribeUntilDestroyed(hotkeys.hotkey$, hk => {
|
||||
if (hk === 'rearrange-panes' && this.parent.hasFocus) {
|
||||
this.isActive = true
|
||||
this.layout()
|
||||
}
|
||||
})
|
||||
this.subscribeUntilDestroyed(hotkeys.hotkeyOff$, hk => {
|
||||
if (hk === 'rearrange-panes') {
|
||||
this.isActive = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.layout()
|
||||
}
|
||||
|
||||
onTabDragStart (tab: BaseTabComponent): void {
|
||||
this.app.emitTabDragStarted(tab)
|
||||
}
|
||||
|
||||
onTabDragEnd (): void {
|
||||
setTimeout(() => {
|
||||
this.app.emitTabDragEnded()
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
layout () {
|
||||
const tabElement: HTMLElement|undefined = this.tab.viewContainerEmbeddedRef?.rootNodes[0]
|
||||
|
||||
if (!tabElement) {
|
||||
// being destroyed
|
||||
return
|
||||
}
|
||||
|
||||
this.setDimensions(
|
||||
tabElement.offsetLeft,
|
||||
tabElement.offsetTop,
|
||||
tabElement.clientWidth,
|
||||
tabElement.clientHeight,
|
||||
'px'
|
||||
)
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
transition: 0.125s background;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
|
||||
&.v {
|
||||
cursor: ns-resize;
|
||||
|
@@ -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)')
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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)') ×
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@
|
||||
min-width: 0;
|
||||
margin-right: auto;
|
||||
margin-bottom: 3px;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ hotkeys:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
rearrange-panes:
|
||||
- 'Ctrl-Shift'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
|
@@ -16,6 +16,8 @@ hotkeys:
|
||||
- '⌘-Shift-Left'
|
||||
move-tab-right:
|
||||
- '⌘-Shift-Right'
|
||||
rearrange-panes:
|
||||
- '⌘-Shift'
|
||||
tab-1:
|
||||
- '⌘-1'
|
||||
tab-2:
|
||||
|
@@ -19,6 +19,8 @@ hotkeys:
|
||||
- 'Ctrl-Shift-PageUp'
|
||||
move-tab-right:
|
||||
- 'Ctrl-Shift-PageDown'
|
||||
rearrange-panes:
|
||||
- 'Ctrl-Shift'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
tab-2:
|
||||
|
@@ -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',
|
||||
|
@@ -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,
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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('-')
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ export class ProfilesService {
|
||||
tab.setTitle(profile.name)
|
||||
}
|
||||
if (profile.disableDynamicTitle) {
|
||||
tab['enableDynamicTitle'] = false
|
||||
tab['disableDynamicTitle'] = true
|
||||
}
|
||||
return tab
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
}
|
||||
|
59
tabby-electron/src/sftpContextMenu.ts
Normal file
59
tabby-electron/src/sftpContextMenu.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as tmp from 'tmp-promise'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { Subject, debounceTime, debounce } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { MenuItemOptions } from 'tabby-core'
|
||||
import { SFTPFile, SFTPPanelComponent, SFTPContextMenuItemProvider, SFTPSession } from 'tabby-ssh'
|
||||
import { ElectronPlatformService } from './services/platform.service'
|
||||
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
constructor (
|
||||
private platform: ElectronPlatformService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (item: SFTPFile, panel: SFTPPanelComponent): Promise<MenuItemOptions[]> {
|
||||
if (item.isDirectory) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
click: () => this.edit(item, panel.sftp),
|
||||
label: 'Edit locally',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private async edit (item: SFTPFile, sftp: SFTPSession) {
|
||||
const tempDir = (await tmp.dir({ unsafeCleanup: true })).path
|
||||
const tempPath = path.join(tempDir, item.name)
|
||||
const transfer = await this.platform.startDownload(item.name, item.mode, item.size, tempPath)
|
||||
if (!transfer) {
|
||||
return
|
||||
}
|
||||
await sftp.download(item.fullPath, transfer)
|
||||
this.platform.openPath(tempPath)
|
||||
|
||||
const events = new Subject<string>()
|
||||
const watcher = fs.watch(tempPath, event => events.next(event))
|
||||
events.pipe(debounceTime(1000), debounce(async event => {
|
||||
if (event === 'rename') {
|
||||
watcher.close()
|
||||
}
|
||||
const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
|
||||
if (!upload.length) {
|
||||
return
|
||||
}
|
||||
sftp.upload(item.fullPath, upload[0])
|
||||
})).subscribe()
|
||||
watcher.on('close', () => events.complete())
|
||||
sftp.closed$.subscribe(() => watcher.close())
|
||||
}
|
||||
}
|
@@ -16,6 +16,19 @@ async@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
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=
|
||||
|
@@ -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"
|
||||
|
@@ -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 {
|
||||
|
@@ -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 ?? ''
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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[] {
|
||||
|
@@ -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)) {
|
||||
|
@@ -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()',
|
||||
)
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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> = {}
|
||||
|
||||
|
12
tabby-ssh/src/api/contextMenu.ts
Normal file
12
tabby-ssh/src/api/contextMenu.ts
Normal 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[]>
|
||||
}
|
2
tabby-ssh/src/api/index.ts
Normal file
2
tabby-ssh/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './contextMenu'
|
||||
export * from './interfaces'
|
53
tabby-ssh/src/api/interfaces.ts
Normal file
53
tabby-ssh/src/api/interfaces.ts
Normal 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',
|
||||
]
|
@@ -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 {
|
||||
|
@@ -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({
|
||||
|
@@ -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)
|
||||
|
@@ -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> {
|
||||
|
@@ -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 }
|
||||
|
@@ -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 ?? ''
|
||||
}
|
||||
|
@@ -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'
|
||||
|
@@ -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 {
|
||||
|
64
tabby-ssh/src/session/forwards.ts
Normal file
64
tabby-ssh/src/session/forwards.ts
Normal 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}`
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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',
|
||||
]
|
48
tabby-ssh/src/sftpContextMenu.ts
Normal file
48
tabby-ssh/src/sftpContextMenu.ts
Normal 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
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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}` : ''
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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)
|
||||
}
|
||||
}))
|
||||
|
@@ -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 {
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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`)
|
||||
|
@@ -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"
|
||||
|
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,5 +16,5 @@
|
||||
"resolutions": {
|
||||
"**/util": "^0.12.0"
|
||||
},
|
||||
"version": "1.0.149-nightly.4"
|
||||
"version": "1.0.150"
|
||||
}
|
||||
|
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -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
147
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user