diff --git a/tabby-core/src/components/appRoot.component.scss b/tabby-core/src/components/appRoot.component.scss index 1bdb011a..19184e83 100644 --- a/tabby-core/src/components/appRoot.component.scss +++ b/tabby-core/src/components/appRoot.component.scss @@ -185,3 +185,18 @@ hotkey-hint { ::ng-deep .btn-update svg { fill: cyan; } + +::ng-deep .broadcast-status-warning { + background: red; + position: absolute; + top: 0; + left: 50%; + padding: 5px 10px; + color: black; + border-radius: 0 0 5px 5px; + + width: 300px; + margin-left: -150px; + text-align: center; + font-weight: bold; +} diff --git a/tabby-core/src/configDefaults.macos.yaml b/tabby-core/src/configDefaults.macos.yaml index 8d9d40f6..6ea35df8 100644 --- a/tabby-core/src/configDefaults.macos.yaml +++ b/tabby-core/src/configDefaults.macos.yaml @@ -17,7 +17,7 @@ hotkeys: move-tab-right: - '⌘-Shift-Right' rearrange-panes: - - '⌘-Shift' + - 'Ctrl-Shift' tab-1: - '⌘-1' tab-2: diff --git a/tabby-local/src/tabContextMenu.ts b/tabby-local/src/tabContextMenu.ts index 79a02516..e7e98dde 100644 --- a/tabby-local/src/tabContextMenu.ts +++ b/tabby-local/src/tabContextMenu.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, SplitTabComponent, NotificationsService, MenuItemOptions, ProfilesService, PromptModalComponent, TranslateService } from 'tabby-core' +import { MultifocusService } from 'tabby-terminal' import { TerminalTabComponent } from './components/terminalTab.component' import { UACService } from './services/uac.service' import { TerminalService } from './services/terminal.service' @@ -65,6 +66,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider { private terminalService: TerminalService, private uac: UACService, private translate: TranslateService, + private multifocus: MultifocusService, ) { super() } @@ -131,13 +133,21 @@ export class NewTabContextMenu extends TabContextMenuItemProvider { }) } - if (tab instanceof TerminalTabComponent && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) { + if (tab instanceof TerminalTabComponent && tab.parent instanceof SplitTabComponent) { items.push({ - label: this.translate.instant('Focus all panes'), + label: this.translate.instant('Focus all tabs'), click: () => { - tab.focusAllPanes() + this.multifocus.focusAllTabs() }, }) + if (tab.parent.getAllTabs().length > 1) { + items.push({ + label: this.translate.instant('Focus all panes'), + click: () => { + this.multifocus.focusAllPanes() + }, + }) + } } return items diff --git a/tabby-ssh/src/tabContextMenu.ts b/tabby-ssh/src/tabContextMenu.ts index 28ab74be..bf48379e 100644 --- a/tabby-ssh/src/tabContextMenu.ts +++ b/tabby-ssh/src/tabContextMenu.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, HostAppService, Platform, MenuItemOptions, TranslateService } from 'tabby-core' +import { BaseTabComponent, TabContextMenuItemProvider, HostAppService, Platform, MenuItemOptions, TranslateService } from 'tabby-core' import { SSHTabComponent } from './components/sshTab.component' import { SSHService } from './services/ssh.service' @@ -17,7 +17,7 @@ export class SFTPContextMenu extends TabContextMenuItemProvider { super() } - async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise { + async getItems (tab: BaseTabComponent): Promise { if (!(tab instanceof SSHTabComponent) || !tab.profile) { return [] } diff --git a/tabby-terminal/src/api/baseTerminalTab.component.ts b/tabby-terminal/src/api/baseTerminalTab.component.ts index 7ef4db5b..1f7fd029 100644 --- a/tabby-terminal/src/api/baseTerminalTab.component.ts +++ b/tabby-terminal/src/api/baseTerminalTab.component.ts @@ -1,4 +1,4 @@ -import { Observable, Subject, Subscription, first, auditTime } from 'rxjs' +import { Observable, Subject, first, auditTime } from 'rxjs' import { Spinner } from 'cli-spinner' import colors from 'ansi-colors' import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core' @@ -12,6 +12,7 @@ import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend' import { ResizeEvent } from './interfaces' import { TerminalDecorator } from './decorator' import { SearchPanelComponent } from '../components/searchPanel.component' +import { MultifocusService } from '../services/multifocus.service' /** * A class to base your custom terminal tabs on @@ -117,6 +118,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit protected contextMenuProviders: TabContextMenuItemProvider[] protected hostWindow: HostWindowService protected translate: TranslateService + protected multifocus: MultifocusService // Deps end protected logger: Logger @@ -124,7 +126,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit protected sessionChanged = new Subject() private bellPlayer: HTMLAudioElement private termContainerSubscriptions = new SubscriptionContainer() - private allFocusModeSubscription: Subscription|null = null private sessionHandlers = new SubscriptionContainer() private spinner = new Spinner({ stream: { @@ -187,6 +188,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.contextMenuProviders = injector.get(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[] this.hostWindow = injector.get(HostWindowService) this.translate = injector.get(TranslateService) + this.multifocus = injector.get(MultifocusService) this.logger = this.log.create('baseTerminalTab') this.setTitle(this.translate.instant('Terminal')) @@ -279,9 +281,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit }[this.hostApp.platform]) }) break - case 'pane-focus-all': - this.focusAllPanes() - break case 'copy-current-path': this.copyCurrentPath() break @@ -387,7 +386,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.frontend.focus() this.blurred$.subscribe(() => { - this.cancelFocusAllPanes() + this.multifocus.cancel() }) } @@ -533,36 +532,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.frontend?.setZoom(this.zoom) } - focusAllPanes (): void { - if (this.allFocusModeSubscription) { - return - } - if (this.parent instanceof SplitTabComponent) { - const parent = this.parent - parent._allFocusMode = true - parent.layout() - this.allFocusModeSubscription = this.frontend?.input$.subscribe(data => { - for (const tab of parent.getAllTabs()) { - if (tab !== this && tab instanceof BaseTerminalTabComponent) { - tab.sendInput(data) - } - } - }) ?? null - } - } - - cancelFocusAllPanes (): void { - if (!this.allFocusModeSubscription) { - return - } - if (this.parent instanceof SplitTabComponent) { - this.allFocusModeSubscription.unsubscribe() - this.allFocusModeSubscription = null - this.parent._allFocusMode = false - this.parent.layout() - } - } - async copyCurrentPath (): Promise { let cwd: string|null = null if (this.session?.supportsWorkingDirectory()) { @@ -666,7 +635,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.termContainerSubscriptions.subscribe(this.frontend.mouseEvent$, event => { if (event.type === 'mousedown') { if (event.which === 1) { - this.cancelFocusAllPanes() + this.multifocus.cancel() } if (event.which === 2) { if (this.config.store.terminal.pasteOnMiddleClick) { diff --git a/tabby-terminal/src/config.ts b/tabby-terminal/src/config.ts index 70b8d0c7..3d02be32 100644 --- a/tabby-terminal/src/config.ts +++ b/tabby-terminal/src/config.ts @@ -115,6 +115,9 @@ export class TerminalConfigProvider extends ConfigProvider { 'pane-focus-all': [ '⌘-Shift-I', ], + 'focus-all-tabs': [ + '⌘-⌥-Shift-I', + ], 'scroll-to-top': ['Shift-PageUp'], 'scroll-up': ['⌥-PageUp'], 'scroll-down': ['⌥-PageDown'], @@ -163,6 +166,9 @@ export class TerminalConfigProvider extends ConfigProvider { 'pane-focus-all': [ 'Ctrl-Shift-I', ], + 'focus-all-tabs': [ + 'Ctrl-Alt-Shift-I', + ], 'scroll-to-top': ['Ctrl-PageUp'], 'scroll-up': ['Alt-PageUp'], 'scroll-down': ['Alt-PageDown'], @@ -209,6 +215,9 @@ export class TerminalConfigProvider extends ConfigProvider { 'pane-focus-all': [ 'Ctrl-Shift-I', ], + 'focus-all-tabs': [ + 'Ctrl-Alt-Shift-I', + ], 'scroll-to-top': ['Ctrl-PageUp'], 'scroll-up': ['Alt-PageUp'], 'scroll-down': ['Alt-PageDown'], diff --git a/tabby-terminal/src/hotkeys.ts b/tabby-terminal/src/hotkeys.ts index 22f394d1..808c17ef 100644 --- a/tabby-terminal/src/hotkeys.ts +++ b/tabby-terminal/src/hotkeys.ts @@ -77,6 +77,10 @@ export class TerminalHotkeyProvider extends HotkeyProvider { id: 'pane-focus-all', name: this.translate.instant('Focus all panes at once (broadcast)'), }, + { + id: 'focus-all-tabs', + name: this.translate.instant('Focus all tabs at once (broadcast)'), + }, { id: 'scroll-to-top', name: this.translate.instant('Scroll terminal to top'), diff --git a/tabby-terminal/src/index.ts b/tabby-terminal/src/index.ts index eaf6ee58..2f8cefb9 100644 --- a/tabby-terminal/src/index.ts +++ b/tabby-terminal/src/index.ts @@ -96,3 +96,4 @@ export * from './middleware/oscProcessing' export * from './api/middleware' export * from './session' export { LoginScriptsSettingsComponent, StreamProcessingSettingsComponent } +export { MultifocusService } from './services/multifocus.service' diff --git a/tabby-terminal/src/services/multifocus.service.ts b/tabby-terminal/src/services/multifocus.service.ts new file mode 100644 index 00000000..7e188399 --- /dev/null +++ b/tabby-terminal/src/services/multifocus.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@angular/core' +import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' +import { Subscription } from 'rxjs' +import { SplitTabComponent, TranslateService, AppService, HotkeysService } from 'tabby-core' + +@Injectable({ providedIn: 'root' }) +export class MultifocusService { + private inputSubscription: Subscription|null = null + private currentTab: BaseTerminalTabComponent|null = null + private warningElement: HTMLElement + + constructor ( + private app: AppService, + hotkeys: HotkeysService, + translate: TranslateService, + ) { + this.warningElement = document.createElement('div') + this.warningElement.className = 'broadcast-status-warning' + this.warningElement.innerText = translate.instant('Broadcast mode. Click anywhere to cancel.') + this.warningElement.style.display = 'none' + document.body.appendChild(this.warningElement) + + hotkeys.hotkey$.subscribe(hotkey => { + switch (hotkey) { + case 'focus-all-tabs': + this.focusAllTabs() + break + case 'pane-focus-all': + this.focusAllPanes() + break + } + }) + } + + start (currentTab: BaseTerminalTabComponent, tabs: BaseTerminalTabComponent[]): void { + if (this.inputSubscription) { + return + } + + if (currentTab.parent instanceof SplitTabComponent) { + const parent = currentTab.parent + parent._allFocusMode = true + parent.layout() + } + + this.currentTab = currentTab + this.inputSubscription = currentTab.frontend?.input$.subscribe(data => { + for (const tab of tabs) { + if (tab !== currentTab) { + tab.sendInput(data) + } + } + }) ?? null + } + + cancel (): void { + this.warningElement.style.display = 'none' + document.querySelector('app-root')!['style'].border = 'none' + + if (!this.inputSubscription) { + return + } + this.inputSubscription.unsubscribe() + this.inputSubscription = null + if (this.currentTab?.parent instanceof SplitTabComponent) { + this.currentTab.parent._allFocusMode = false + this.currentTab.parent.layout() + } + this.currentTab = null + } + + focusAllTabs (): void { + let currentTab = this.app.activeTab + if (currentTab && currentTab instanceof SplitTabComponent) { + currentTab = currentTab.getFocusedTab() + } + if (!currentTab || !(currentTab instanceof BaseTerminalTabComponent)) { + return + } + const tabs = this.app.tabs + .map((t => { + if (t instanceof BaseTerminalTabComponent) { + return [t] + } else if (t instanceof SplitTabComponent) { + return t.getAllTabs() + .filter(x => x instanceof BaseTerminalTabComponent) + } else { + return [] + } + }) as (_) => BaseTerminalTabComponent[]) + .flat() + this.start(currentTab, tabs) + + this.warningElement.style.display = 'block' + document.querySelector('app-root')!['style'].border = '5px solid red' + } + + focusAllPanes (): void { + const currentTab = this.app.activeTab + if (!currentTab || !(currentTab instanceof SplitTabComponent)) { + return + } + + const pane = currentTab.getFocusedTab() + if (!pane || !(pane instanceof BaseTerminalTabComponent)) { + return + } + const tabs = currentTab.getAllTabs().filter(t => t instanceof BaseTerminalTabComponent) + this.start(pane, tabs as any) + } +}