mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-10 02:21:50 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cc6cfec907 | ||
![]() |
4ecfcfda36 | ||
![]() |
c5681b1376 | ||
![]() |
1fc57018e3 | ||
![]() |
8b8bacdf69 | ||
![]() |
94819019ec | ||
![]() |
7b37035f75 | ||
![]() |
a5ef3507c3 | ||
![]() |
b9c6d30678 | ||
![]() |
009556f984 | ||
![]() |
87007d5ae3 | ||
![]() |
61ea2c77c8 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-community-color-schemes",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"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.149",
|
||||
"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, KeySequenceItem } from '../services/hotkeys.util'
|
||||
export { NotificationsService } from '../services/notifications.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { ProfilesService } from '../services/profiles.service'
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -152,6 +152,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')],
|
||||
})
|
||||
@@ -374,6 +382,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -576,16 +588,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()
|
||||
|
@@ -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.relativeToTab
|
||||
this.layout()
|
||||
})
|
||||
}
|
||||
|
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;
|
||||
|
@@ -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)
|
||||
|
@@ -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 { stringifyKeySequence, KeyEventData, KeySequenceItem } from './hotkeys.util'
|
||||
import { ConfigService } from './config.service'
|
||||
import { HostAppService, Platform } from '../api/hostApp'
|
||||
import { deprecate } from 'util'
|
||||
@@ -27,10 +27,25 @@ 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 recognized hotkey
|
||||
*/
|
||||
get key$ (): Observable<KeyboardEvent> { return this._key }
|
||||
|
||||
private _hotkey = new Subject<string>()
|
||||
private currentKeystrokes: EventData[] = []
|
||||
private _hotkeyOff = new Subject<string>()
|
||||
private _key = new Subject<KeyboardEvent>()
|
||||
private currentEvents: KeyEventData[] = []
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: HotkeyDescription[] = []
|
||||
private pressedHotkey: string|null = null
|
||||
private lastMatchedHotkeyStartTime = performance.now()
|
||||
private lastMatchedHotkeyEndTime = performance.now()
|
||||
|
||||
private constructor (
|
||||
private zone: NgZone,
|
||||
@@ -39,12 +54,10 @@ 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)
|
||||
events.forEach(eventType => {
|
||||
document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
|
||||
if (eventType === 'keyup' || document.querySelectorAll('input:focus').length === 0) {
|
||||
this.pushKeystroke(eventType, nativeEvent)
|
||||
if (hostApp.platform === Platform.Web) {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
@@ -60,6 +73,8 @@ 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.key$.subscribe(e => this.key.emit(e))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +85,10 @@ export class HotkeysService {
|
||||
*/
|
||||
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
|
||||
nativeEvent['event'] = name
|
||||
this.currentKeystrokes.push({
|
||||
if (nativeEvent.timeStamp && this.currentEvents.find(x => x.time === nativeEvent.timeStamp)) {
|
||||
return
|
||||
}
|
||||
this.currentEvents.push({
|
||||
ctrlKey: nativeEvent.ctrlKey,
|
||||
metaKey: nativeEvent.metaKey,
|
||||
altKey: nativeEvent.altKey,
|
||||
@@ -78,8 +96,11 @@ export class HotkeysService {
|
||||
code: nativeEvent.code,
|
||||
key: nativeEvent.key,
|
||||
eventName: name,
|
||||
time: performance.now(),
|
||||
time: nativeEvent.timeStamp,
|
||||
registrationTime: performance.now(),
|
||||
})
|
||||
this.processKeystrokes()
|
||||
this.emitKeyEvent(nativeEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,53 +109,87 @@ export class HotkeysService {
|
||||
processKeystrokes (): void {
|
||||
if (this.isEnabled()) {
|
||||
this.zone.run(() => {
|
||||
const matched = this.getCurrentFullyMatchedHotkey()
|
||||
let fullMatches: {
|
||||
id: string,
|
||||
sequence: string[],
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
}[] = []
|
||||
|
||||
const currentSequence = this.getCurrentKeySequence()
|
||||
const config = this.getHotkeysConfig()
|
||||
for (const id in config) {
|
||||
for (const sequence of config[id]) {
|
||||
if (currentSequence.length < sequence.length) {
|
||||
continue
|
||||
}
|
||||
if (sequence.every(
|
||||
(x: string, index: number) =>
|
||||
x.toLowerCase() ===
|
||||
currentSequence[currentSequence.length - sequence.length + index].value.toLowerCase()
|
||||
)) {
|
||||
fullMatches.push({
|
||||
id: id,
|
||||
sequence,
|
||||
startTime: currentSequence[currentSequence.length - sequence.length].firstEvent.registrationTime,
|
||||
endTime: currentSequence[currentSequence.length - 1].lastEvent.registrationTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fullMatches.sort((a, b) => b.startTime - a.startTime + (b.sequence.length - a.sequence.length))
|
||||
fullMatches = fullMatches.filter(x => x.startTime >= this.lastMatchedHotkeyStartTime)
|
||||
fullMatches = fullMatches.filter(x => x.endTime > this.lastMatchedHotkeyEndTime)
|
||||
|
||||
const matched = fullMatches[0]?.id
|
||||
if (matched) {
|
||||
console.log('Matched hotkey', matched)
|
||||
this._hotkey.next(matched)
|
||||
this.clearCurrentKeystrokes()
|
||||
this.emitHotkeyOn(matched)
|
||||
this.lastMatchedHotkeyStartTime = fullMatches[0].startTime
|
||||
this.lastMatchedHotkeyEndTime = fullMatches[0].endTime
|
||||
} else if (this.pressedHotkey) {
|
||||
this.emitHotkeyOff(this.pressedHotkey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private emitHotkeyOn (hotkey: string) {
|
||||
if (this.pressedHotkey) {
|
||||
this.emitHotkeyOff(this.pressedHotkey)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
emitKeyEvent (nativeEvent: KeyboardEvent): void {
|
||||
this.zone.run(() => {
|
||||
this.key.emit(nativeEvent)
|
||||
this._key.next(nativeEvent)
|
||||
})
|
||||
}
|
||||
|
||||
clearCurrentKeystrokes (): void {
|
||||
this.currentKeystrokes = []
|
||||
this.currentEvents = []
|
||||
}
|
||||
|
||||
getCurrentKeystrokes (): string[] {
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
|
||||
return stringifyKeySequence(this.currentKeystrokes)
|
||||
getCurrentKeySequence (): KeySequenceItem[] {
|
||||
this.currentEvents = this.currentEvents.filter(x => performance.now() - x.time < KEY_TIMEOUT && x.registrationTime >= this.lastMatchedHotkeyStartTime)
|
||||
return stringifyKeySequence(this.currentEvents)
|
||||
}
|
||||
|
||||
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
|
||||
return this.pressedHotkey
|
||||
}
|
||||
|
||||
getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] {
|
||||
const currentStrokes = this.getCurrentKeystrokes()
|
||||
const currentStrokes = this.getCurrentKeySequence().map(x => x.value)
|
||||
const config = this.getHotkeysConfig()
|
||||
const result: PartialHotkeyMatch[] = []
|
||||
for (const id in config) {
|
||||
|
@@ -10,46 +10,66 @@ 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[] = []
|
||||
export interface KeySequenceItem {
|
||||
value: string
|
||||
firstEvent: KeyEventData
|
||||
lastEvent: KeyEventData
|
||||
}
|
||||
|
||||
export function stringifyKeySequence (events: KeyEventData[]): KeySequenceItem[] {
|
||||
const items: KeySequenceItem[] = []
|
||||
let pressedKeys: KeySequenceItem[] = []
|
||||
events = events.slice()
|
||||
|
||||
const strictOrdering = ['Ctrl', metaKeyName, altKeyName, 'Shift']
|
||||
|
||||
function flushPressedKeys () {
|
||||
if (pressedKeys.length) {
|
||||
const v = {
|
||||
firstEvent: pressedKeys[0].firstEvent,
|
||||
lastEvent: pressedKeys[pressedKeys.length - 1].lastEvent,
|
||||
}
|
||||
pressedKeys = [
|
||||
...strictOrdering.map(x => pressedKeys.find(p => p.value === x)).filter(x => !!x) as KeySequenceItem[],
|
||||
...pressedKeys.filter(p => !strictOrdering.includes(p.value)),
|
||||
]
|
||||
items.push({
|
||||
value: pressedKeys.map(x => x.value).join('-'),
|
||||
...v,
|
||||
})
|
||||
pressedKeys = []
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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()
|
||||
@@ -72,10 +92,20 @@ export function stringifyKeySequence (events: EventData[]): string[] {
|
||||
BracketRight: ']',
|
||||
}[key] ?? key
|
||||
}
|
||||
}
|
||||
|
||||
itemKeys.push(key)
|
||||
items.push(itemKeys.join('-'))
|
||||
if (event.eventName === 'keydown') {
|
||||
pressedKeys.push({
|
||||
value: key,
|
||||
firstEvent: event,
|
||||
lastEvent: event,
|
||||
})
|
||||
}
|
||||
if (event.eventName === 'keyup') {
|
||||
flushPressedKeys()
|
||||
}
|
||||
}
|
||||
|
||||
flushPressedKeys()
|
||||
return items
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-electron",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"description": "Electron-specific bindings",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -24,6 +24,7 @@ try {
|
||||
export class ElectronPlatformService extends PlatformService {
|
||||
supportsWindowControls = true
|
||||
private configPath: string
|
||||
private _configSaveInProgress = Promise.resolve()
|
||||
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
@@ -107,7 +108,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 fs.rename(tempPath, this.configPath)
|
||||
}
|
||||
|
||||
getConfigPath (): string|null {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-local",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"description": "Tabby's local shell plugin",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-plugin-manager",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"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.149",
|
||||
"description": "Serial connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-settings",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"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()
|
||||
@@ -96,4 +96,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
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ export class HotkeyInputModalComponent extends BaseComponent {
|
||||
this.hotkeys.clearCurrentKeystrokes()
|
||||
this.subscribeUntilDestroyed(hotkeys.key, (event) => {
|
||||
this.lastKeyEvent = performance.now()
|
||||
this.value = this.hotkeys.getCurrentKeystrokes()
|
||||
this.value = this.hotkeys.getCurrentKeySequence().map(x => x.value)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
@@ -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()',
|
||||
)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-ssh",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"description": "SSH connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -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)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-telnet",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"description": "Telnet/socket connections for Tabby",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-terminal",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"description": "Tabby's terminal emulation core",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -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 {
|
||||
|
@@ -92,9 +92,6 @@ export class XTermFrontend extends Frontend {
|
||||
event.preventDefault()
|
||||
ret = false
|
||||
}
|
||||
this.hotkeysService.processKeystrokes()
|
||||
this.hotkeysService.emitKeyEvent(event)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
@@ -97,7 +97,7 @@ 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)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tabby-web-demo",
|
||||
"version": "1.0.149-nightly.4",
|
||||
"version": "1.0.149",
|
||||
"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.149",
|
||||
"description": "Web-specific bindings",
|
||||
"keywords": [
|
||||
"tabby-builtin-plugin"
|
||||
|
@@ -16,5 +16,5 @@
|
||||
"resolutions": {
|
||||
"**/util": "^0.12.0"
|
||||
},
|
||||
"version": "1.0.149-nightly.4"
|
||||
"version": "1.0.149"
|
||||
}
|
||||
|
Reference in New Issue
Block a user