Compare commits

..

12 Commits

Author SHA1 Message Date
Eugene Pankov
cc6cfec907 added a default color to split tab spanners 2021-07-31 18:59:52 +02:00
Eugene Pankov
4ecfcfda36 ui tweaks 2021-07-31 18:02:39 +02:00
Eugene Pankov
c5681b1376 allow rearranging panes within a tab 2021-07-31 17:57:43 +02:00
Eugene Pankov
1fc57018e3 allow modifier-only hotkeys 2021-07-31 17:18:03 +02:00
Eugene Pankov
8b8bacdf69 fixed config file getting spontaneously erased - fixes #4293 2021-07-31 15:28:10 +02:00
Eugene Pankov
94819019ec bumped plugins 2021-07-29 21:20:41 +02:00
Eugene Pankov
7b37035f75 toolbar dropdown ui fix 2021-07-29 21:20:34 +02:00
Eugene Pankov
a5ef3507c3 toolbar ui fix 2021-07-29 21:20:29 +02:00
Eugene Pankov
b9c6d30678 include titles of all panes in a split tab's title 2021-07-29 21:20:25 +02:00
Eugene Pankov
009556f984 possible fix for #4293 2021-07-29 21:07:47 +02:00
Eugene Pankov
87007d5ae3 demo progress report fix 2021-07-29 21:07:31 +02:00
Eugene Pankov
61ea2c77c8 config sync ui updates 2021-07-29 21:07:22 +02:00
39 changed files with 372 additions and 107 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -152,6 +152,14 @@ export interface SplitDropZoneInfo {
(tabDropped)='onTabDropped($event, dropZone)' (tabDropped)='onTabDropped($event, dropZone)'
> >
</split-tab-drop-zone> </split-tab-drop-zone>
<split-tab-pane-label
*ngFor='let tab of getAllTabs()'
cdkDropList
cdkAutoDropGroup='app-tabs'
[tab]='tab'
[parent]='this'
>
</split-tab-pane-label>
`, `,
styles: [require('./splitTab.component.scss')], styles: [require('./splitTab.component.scss')],
}) })
@@ -374,6 +382,10 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
if (thing instanceof BaseTabComponent) { if (thing instanceof BaseTabComponent) {
if (thing.parent instanceof SplitTabComponent) {
thing.parent.removeTab(thing)
}
thing.removeFromContainer()
thing.parent = this thing.parent = this
} }
@@ -576,16 +588,20 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
this.layoutInternal(this.root, 0, 0, 100, 100) this.layoutInternal(this.root, 0, 0, 100, 100)
} }
private updateTitle (): void {
this.setTitle(this.getAllTabs().map(x => x.title).join(' | '))
}
private attachTabView (tab: BaseTabComponent) { private attachTabView (tab: BaseTabComponent) {
const ref = tab.insertIntoContainer(this.viewContainer) const ref = tab.insertIntoContainer(this.viewContainer)
this.viewRefs.set(tab, ref) this.viewRefs.set(tab, ref)
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab)) tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t)) tab.subscribeUntilDestroyed(tab.titleChange$, () => this.updateTitle())
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity()) tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p)) tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
if (tab.title) { if (tab.title) {
this.setTitle(tab.title) this.updateTitle()
} }
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => { tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
this.recoveryStateChangedHint.next() this.recoveryStateChangedHint.next()

View File

@@ -34,7 +34,7 @@ export class SplitTabDropZoneComponent extends SelfPositioningComponent {
) { ) {
super(element) super(element)
this.subscribeUntilDestroyed(app.tabDragActive$, tab => { this.subscribeUntilDestroyed(app.tabDragActive$, tab => {
this.isActive = !!tab && tab !== this.parent this.isActive = !!tab && tab !== this.parent && tab !== this.dropZone.relativeToTab
this.layout() this.layout()
}) })
} }

View File

@@ -0,0 +1,35 @@
:host {
position: absolute;
background: rgba(255, 255, 255, .25);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
opacity: 0;
transition: .125s opacity cubic-bezier(0.86, 0, 0.07, 1);
}
div {
background: rgba(0, 0, 0, .7);
padding: 20px 30px;
font-size: 18px;
color: #fff;
display: flex;
align-items: center;
border-radius: 5px;
cursor: move;
}
:host.active {
opacity: 1;
> div {
pointer-events: initial;
}
}
label {
margin: 0;
cursor: move;
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input, HostBinding, ElementRef } from '@angular/core'
import { HotkeysService } from '../services/hotkeys.service'
import { AppService } from '../services/app.service'
import { BaseTabComponent } from './baseTab.component'
import { SelfPositioningComponent } from './selfPositioning.component'
/** @hidden */
@Component({
selector: 'split-tab-pane-label',
template: `
<div
cdkDrag
[cdkDragData]='tab'
(cdkDragStarted)='onTabDragStart(tab)'
(cdkDragEnded)='onTabDragEnd()'
>
<i class="fa fa-window-maximize mr-3"></i>
<label>{{tab.title}}</label>
</div>
`,
styles: [require('./splitTabPaneLabel.component.scss')],
})
export class SplitTabPaneLabelComponent extends SelfPositioningComponent {
@Input() tab: BaseTabComponent
@Input() parent: BaseTabComponent
@HostBinding('class.active') isActive = false
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (
element: ElementRef,
hotkeys: HotkeysService,
private app: AppService,
) {
super(element)
this.subscribeUntilDestroyed(hotkeys.hotkey$, hk => {
if (hk === 'rearrange-panes' && this.parent.hasFocus) {
this.isActive = true
this.layout()
}
})
this.subscribeUntilDestroyed(hotkeys.hotkeyOff$, hk => {
if (hk === 'rearrange-panes') {
this.isActive = false
}
})
}
ngOnChanges () {
this.layout()
}
onTabDragStart (tab: BaseTabComponent): void {
this.app.emitTabDragStarted(tab)
}
onTabDragEnd (): void {
setTimeout(() => {
this.app.emitTabDragEnded()
this.app.emitTabsChanged()
})
}
layout () {
const tabElement: HTMLElement|undefined = this.tab.viewContainerEmbeddedRef?.rootNodes[0]
if (!tabElement) {
// being destroyed
return
}
this.setDimensions(
tabElement.offsetLeft,
tabElement.offsetTop,
tabElement.clientWidth,
tabElement.clientHeight,
'px'
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import { SelectorModalComponent } from './components/selectorModal.component'
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component' import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component' import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component' import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component'
import { SplitTabPaneLabelComponent } from './components/splitTabPaneLabel.component'
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component' import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
import { WelcomeTabComponent } from './components/welcomeTab.component' import { WelcomeTabComponent } from './components/welcomeTab.component'
import { TransfersMenuComponent } from './components/transfersMenu.component' import { TransfersMenuComponent } from './components/transfersMenu.component'
@@ -100,6 +101,7 @@ const PROVIDERS = [
SplitTabComponent, SplitTabComponent,
SplitTabSpannerComponent, SplitTabSpannerComponent,
SplitTabDropZoneComponent, SplitTabDropZoneComponent,
SplitTabPaneLabelComponent,
UnlockVaultModalComponent, UnlockVaultModalComponent,
WelcomeTabComponent, WelcomeTabComponent,
TransfersMenuComponent, TransfersMenuComponent,

View File

@@ -194,6 +194,10 @@ export class ConfigService {
} }
async save (): Promise<void> { async save (): Promise<void> {
await this.ready$
if (!this._store) {
throw new Error('Cannot save an empty store')
}
// Scrub undefined values // Scrub undefined values
let cleanStore = JSON.parse(JSON.stringify(this._store)) let cleanStore = JSON.parse(JSON.stringify(this._store))
cleanStore = await this.maybeEncryptConfig(cleanStore) cleanStore = await this.maybeEncryptConfig(cleanStore)

View File

@@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core' import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider' import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence, EventData } from './hotkeys.util' import { stringifyKeySequence, KeyEventData, KeySequenceItem } from './hotkeys.util'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { HostAppService, Platform } from '../api/hostApp' import { HostAppService, Platform } from '../api/hostApp'
import { deprecate } from 'util' import { deprecate } from 'util'
@@ -27,10 +27,25 @@ export class HotkeysService {
*/ */
get hotkey$ (): Observable<string> { return this._hotkey } get hotkey$ (): Observable<string> { return this._hotkey }
/**
* Fired for once hotkey is released
*/
get hotkeyOff$ (): Observable<string> { return this._hotkeyOff }
/**
* Fired for each recognized hotkey
*/
get key$ (): Observable<KeyboardEvent> { return this._key }
private _hotkey = new Subject<string>() 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 disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = [] private hotkeyDescriptions: HotkeyDescription[] = []
private pressedHotkey: string|null = null
private lastMatchedHotkeyStartTime = performance.now()
private lastMatchedHotkeyEndTime = performance.now()
private constructor ( private constructor (
private zone: NgZone, private zone: NgZone,
@@ -39,12 +54,10 @@ export class HotkeysService {
hostApp: HostAppService, hostApp: HostAppService,
) { ) {
const events = ['keydown', 'keyup'] const events = ['keydown', 'keyup']
events.forEach(event => { events.forEach(eventType => {
document.addEventListener(event, (nativeEvent: KeyboardEvent) => { document.addEventListener(eventType, (nativeEvent: KeyboardEvent) => {
if (document.querySelectorAll('input:focus').length === 0) { if (eventType === 'keyup' || document.querySelectorAll('input:focus').length === 0) {
this.pushKeystroke(event, nativeEvent) this.pushKeystroke(eventType, nativeEvent)
this.processKeystrokes()
this.emitKeyEvent(nativeEvent)
if (hostApp.platform === Platform.Web) { if (hostApp.platform === Platform.Web) {
nativeEvent.preventDefault() nativeEvent.preventDefault()
nativeEvent.stopPropagation() nativeEvent.stopPropagation()
@@ -60,6 +73,8 @@ export class HotkeysService {
// deprecated // deprecated
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h)) this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$') this.matchedHotkey.subscribe = deprecate(s => this.hotkey$.subscribe(s), 'matchedHotkey is deprecated, use hotkey$')
this.key$.subscribe(e => this.key.emit(e))
} }
/** /**
@@ -70,7 +85,10 @@ export class HotkeysService {
*/ */
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void { pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
nativeEvent['event'] = name nativeEvent['event'] = name
this.currentKeystrokes.push({ if (nativeEvent.timeStamp && this.currentEvents.find(x => x.time === nativeEvent.timeStamp)) {
return
}
this.currentEvents.push({
ctrlKey: nativeEvent.ctrlKey, ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey, metaKey: nativeEvent.metaKey,
altKey: nativeEvent.altKey, altKey: nativeEvent.altKey,
@@ -78,8 +96,11 @@ export class HotkeysService {
code: nativeEvent.code, code: nativeEvent.code,
key: nativeEvent.key, key: nativeEvent.key,
eventName: name, 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 { processKeystrokes (): void {
if (this.isEnabled()) { if (this.isEnabled()) {
this.zone.run(() => { this.zone.run(() => {
const matched = this.getCurrentFullyMatchedHotkey() let fullMatches: {
if (matched) { id: string,
console.log('Matched hotkey', matched) sequence: string[],
this._hotkey.next(matched) startTime: number,
this.clearCurrentKeystrokes() endTime: number,
} }[] = []
})
}
}
emitKeyEvent (nativeEvent: KeyboardEvent): void { const currentSequence = this.getCurrentKeySequence()
this.zone.run(() => {
this.key.emit(nativeEvent)
})
}
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() const config = this.getHotkeysConfig()
for (const id in config) { for (const id in config) {
for (const sequence of config[id]) { for (const sequence of config[id]) {
if (currentStrokes.length < sequence.length) { if (currentSequence.length < sequence.length) {
continue continue
} }
if (sequence.every( if (sequence.every(
(x: string, index: number) => (x: string, index: number) =>
x.toLowerCase() === x.toLowerCase() ===
currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase() currentSequence[currentSequence.length - sequence.length + index].value.toLowerCase()
)) { )) {
return id fullMatches.push({
id: id,
sequence,
startTime: currentSequence[currentSequence.length - sequence.length].firstEvent.registrationTime,
endTime: currentSequence[currentSequence.length - 1].lastEvent.registrationTime,
})
} }
} }
} }
return null
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) {
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.next(nativeEvent)
})
}
clearCurrentKeystrokes (): void {
this.currentEvents = []
}
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 {
return this.pressedHotkey
} }
getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] { getCurrentPartiallyMatchedHotkeys (): PartialHotkeyMatch[] {
const currentStrokes = this.getCurrentKeystrokes() const currentStrokes = this.getCurrentKeySequence().map(x => x.value)
const config = this.getHotkeysConfig() const config = this.getHotkeysConfig()
const result: PartialHotkeyMatch[] = [] const result: PartialHotkeyMatch[] = []
for (const id in config) { for (const id in config) {

View File

@@ -10,46 +10,66 @@ export const altKeyName = {
linux: 'Alt', linux: 'Alt',
}[process.platform] }[process.platform]
export interface EventData { export interface KeyEventData {
ctrlKey: boolean ctrlKey?: boolean
metaKey: boolean metaKey?: boolean
altKey: boolean altKey?: boolean
shiftKey: boolean shiftKey?: boolean
key: string key: string
code: string code: string
eventName: string eventName: string
time: number time: number
registrationTime: number
} }
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/ const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export function stringifyKeySequence (events: EventData[]): string[] { export interface KeySequenceItem {
const items: string[] = [] value: string
firstEvent: KeyEventData
lastEvent: KeyEventData
}
export function stringifyKeySequence (events: KeyEventData[]): KeySequenceItem[] {
const items: KeySequenceItem[] = []
let pressedKeys: KeySequenceItem[] = []
events = events.slice() 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) { while (events.length > 0) {
const event = events.shift()! 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)) { // eslint-disable-next-line @typescript-eslint/init-declarations
// TODO make this optional? let key: string
continue if (event.key === 'Control') {
} key = 'Ctrl'
} else if (event.key === 'Meta') {
let key = event.code 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)) { if (REGEX_LATIN_KEYNAME.test(event.key)) {
// Handle Dvorak etc via the reported "character" instead of the scancode // Handle Dvorak etc via the reported "character" instead of the scancode
key = event.key.toUpperCase() key = event.key.toUpperCase()
@@ -72,10 +92,20 @@ export function stringifyKeySequence (events: EventData[]): string[] {
BracketRight: ']', BracketRight: ']',
}[key] ?? key }[key] ?? key
} }
}
itemKeys.push(key) if (event.eventName === 'keydown') {
items.push(itemKeys.join('-')) pressedKeys.push({
value: key,
firstEvent: event,
lastEvent: event,
})
}
if (event.eventName === 'keyup') {
flushPressedKeys()
} }
} }
flushPressedKeys()
return items return items
} }

View File

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

View File

@@ -24,6 +24,7 @@ try {
export class ElectronPlatformService extends PlatformService { export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true supportsWindowControls = true
private configPath: string private configPath: string
private _configSaveInProgress = Promise.resolve()
constructor ( constructor (
private hostApp: HostAppService, private hostApp: HostAppService,
@@ -107,7 +108,17 @@ export class ElectronPlatformService extends PlatformService {
} }
async saveConfig (content: string): Promise<void> { async saveConfig (content: string): Promise<void> {
await fs.writeFile(this.configPath, content, 'utf8') try {
await this._configSaveInProgress
} catch { }
this._configSaveInProgress = this._saveConfigInternal(content)
await this._configSaveInProgress
}
async _saveConfigInternal (content: string): Promise<void> {
const tempPath = this.configPath + '.new'
await fs.writeFile(tempPath, content, 'utf8')
await fs.rename(tempPath, this.configPath)
} }
getConfigPath (): string|null { getConfigPath (): string|null {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,10 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
constructor ( constructor (
public config: ConfigService, public config: ConfigService,
public platform: PlatformService,
private configSync: ConfigSyncService, private configSync: ConfigSyncService,
private hostApp: HostAppService, private hostApp: HostAppService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
private platform: PlatformService,
private notifications: NotificationsService, private notifications: NotificationsService,
) { ) {
super() super()
@@ -96,4 +96,12 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
await this.configSync.download() await this.configSync.download()
this.notifications.info('Config downloaded') this.notifications.info('Config downloaded')
} }
hasMatchingRemoteConfig () {
return !!this.configs?.find(c => this.isActiveConfig(c))
}
isActiveConfig (c: Config) {
return c.id === this.config.store.configSync.configID
}
} }

View File

@@ -50,7 +50,7 @@ export class HotkeyInputModalComponent extends BaseComponent {
this.hotkeys.clearCurrentKeystrokes() this.hotkeys.clearCurrentKeystrokes()
this.subscribeUntilDestroyed(hotkeys.key, (event) => { this.subscribeUntilDestroyed(hotkeys.key, (event) => {
this.lastKeyEvent = performance.now() this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes() this.value = this.hotkeys.getCurrentKeySequence().map(x => x.value)
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
}) })

View File

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

View File

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

View File

@@ -6,7 +6,12 @@
i.fas.fa-xs.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open') 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}} 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) button.btn.btn-sm.btn-link(ngbDropdownToggle)
i.far.fa-lightbulb i.far.fa-lightbulb
.bg-dark(ngbDropdownMenu) .bg-dark(ngbDropdownMenu)

View File

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

View File

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

View File

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

View File

@@ -92,9 +92,6 @@ export class XTermFrontend extends Frontend {
event.preventDefault() event.preventDefault()
ret = false ret = false
} }
this.hotkeysService.processKeystrokes()
this.hotkeysService.emitKeyEvent(event)
return ret return ret
} }

View File

@@ -97,7 +97,7 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
htermHandler: 'onKeyUp_', htermHandler: 'onKeyUp_',
}, },
] ]
events.forEach((event) => { events.forEach(event => {
const oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler] const oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) { hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
hotkeys.pushKeystroke(event.name, nativeEvent) hotkeys.pushKeystroke(event.name, nativeEvent)

View File

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

View File

@@ -64,7 +64,7 @@ export class Session extends BaseSession {
}, 2000) }, 2000)
}) })
this.vm.add_listener('download-progress', (e) => { 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.vm.add_listener('download-error', (e) => {
this.emitMessage(`\r\nDownload error: ${e}\r\n`) this.emitMessage(`\r\nDownload error: ${e}\r\n`)

View File

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

View File

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