From 3e61630c6ae385bf19a04e8754cf54bfc52d2ecd Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sat, 10 Jul 2021 20:05:31 +0200 Subject: [PATCH] added a way to switch a pane's profile - fixes #1007, fixes #2963, fixes #3082, fixes #4151 --- tabby-core/src/buttonProvider.ts | 83 ++--------------- .../src/components/splitTab.component.ts | 30 +++++-- tabby-core/src/configDefaults.linux.yaml | 2 + tabby-core/src/configDefaults.macos.yaml | 2 + tabby-core/src/configDefaults.windows.yaml | 2 + tabby-core/src/hotkeys.ts | 4 + tabby-core/src/index.ts | 3 +- tabby-core/src/services/profiles.service.ts | 90 +++++++++++++++++++ tabby-core/src/tabContextMenu.ts | 65 ++++++++++++++ 9 files changed, 196 insertions(+), 85 deletions(-) diff --git a/tabby-core/src/buttonProvider.ts b/tabby-core/src/buttonProvider.ts index 7805b19a..3517198c 100644 --- a/tabby-core/src/buttonProvider.ts +++ b/tabby-core/src/buttonProvider.ts @@ -2,26 +2,19 @@ import { Injectable } from '@angular/core' import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider' -import { SelectorService } from './services/selector.service' import { HostAppService, Platform } from './api/hostApp' import { Profile } from './api/profileProvider' import { ConfigService } from './services/config.service' -import { SelectorOption } from './api/selector' import { HotkeysService } from './services/hotkeys.service' import { ProfilesService } from './services/profiles.service' -import { AppService } from './services/app.service' -import { NotificationsService } from './services/notifications.service' /** @hidden */ @Injectable() export class ButtonProvider extends ToolbarButtonProvider { constructor ( - private selector: SelectorService, - private app: AppService, private hostApp: HostAppService, - private profilesServices: ProfilesService, + private profilesService: ProfilesService, private config: ConfigService, - private notifications: NotificationsService, hotkeys: HotkeysService, ) { super() @@ -33,80 +26,14 @@ export class ButtonProvider extends ToolbarButtonProvider { } async activate () { - const recentProfiles: Profile[] = this.config.store.recentProfiles - - let options: SelectorOption[] = recentProfiles.map(p => ({ - ...this.profilesServices.selectorOptionForProfile(p), - icon: 'fas fa-history', - callback: async () => { - if (p.id) { - p = (await this.profilesServices.getProfiles()).find(x => x.id === p.id) ?? p - } - this.launchProfile(p) - }, - })) - if (recentProfiles.length) { - options.push({ - name: 'Clear recent connections', - icon: 'fas fa-eraser', - callback: async () => { - this.config.store.recentProfiles = [] - this.config.save() - }, - }) + const profile = await this.profilesService.showProfileSelector() + if (profile) { + this.launchProfile(profile) } - - let profiles = await this.profilesServices.getProfiles() - - if (!this.config.store.terminal.showBuiltinProfiles) { - profiles = profiles.filter(x => !x.isBuiltin) - } - - profiles = profiles.filter(x => !x.isTemplate) - - options = [...options, ...profiles.map((p): SelectorOption => ({ - ...this.profilesServices.selectorOptionForProfile(p), - callback: () => this.launchProfile(p), - }))] - - try { - const { SettingsTabComponent } = window['nodeRequire']('tabby-settings') - options.push({ - name: 'Manage profiles', - icon: 'fas fa-window-restore', - callback: () => this.app.openNewTabRaw({ - type: SettingsTabComponent, - inputs: { activeTab: 'profiles' }, - }), - }) - } catch { } - - if (this.profilesServices.getProviders().some(x => x.supportsQuickConnect)) { - options.push({ - name: 'Quick connect', - freeInputPattern: 'Connect to "%s"...', - icon: 'fas fa-arrow-right', - callback: query => this.quickConnect(query), - }) - } - await this.selector.show('Select profile', options) - } - - quickConnect (query: string) { - for (const provider of this.profilesServices.getProviders()) { - if (provider.supportsQuickConnect) { - const profile = provider.quickConnect(query) - if (profile) { - this.launchProfile(profile) - return - } - } - } - this.notifications.error(`Could not parse "${query}"`) } async launchProfile (profile: Profile) { - await this.profilesServices.openNewTabForProfile(profile) + await this.profilesService.openNewTabForProfile(profile) let recentProfiles = this.config.store.recentProfiles recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name) diff --git a/tabby-core/src/components/splitTab.component.ts b/tabby-core/src/components/splitTab.component.ts index 5e5d6a74..64d2b834 100644 --- a/tabby-core/src/components/splitTab.component.ts +++ b/tabby-core/src/components/splitTab.component.ts @@ -369,12 +369,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit await this.initialized$.toPromise() this.attachTabView(tab) - - setImmediate(() => { - this.layout() - this.tabAdded.next(tab) - this.focus(tab) - }) + this.onAfterTabAdded(tab) } removeTab (tab: BaseTabComponent): void { @@ -399,6 +394,21 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit } } + replaceTab (tab: BaseTabComponent, newTab: BaseTabComponent): void { + const parent = this.getParentOf(tab) + if (!parent) { + return + } + const position = parent.children.indexOf(tab) + parent.children[position] = newTab + this.detachTabView(tab) + this.attachTabView(newTab) + tab.parent = null + newTab.parent = this + this.recoveryStateChangedHint.next() + this.onAfterTabAdded(newTab) + } + /** * Moves focus in the given direction */ @@ -539,6 +549,14 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit } } + private onAfterTabAdded (tab: BaseTabComponent) { + setImmediate(() => { + this.layout() + this.tabAdded.next(tab) + this.focus(tab) + }) + } + 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) diff --git a/tabby-core/src/configDefaults.linux.yaml b/tabby-core/src/configDefaults.linux.yaml index 872445f3..a5cfca77 100644 --- a/tabby-core/src/configDefaults.linux.yaml +++ b/tabby-core/src/configDefaults.linux.yaml @@ -69,6 +69,8 @@ hotkeys: pane-maximize: - 'Ctrl-Alt-Enter' close-pane: [] + switch-profile: + - 'Ctrl-Alt-T' profile-selector: - 'Ctrl-Shift-T' pluginBlacklist: ['ssh'] diff --git a/tabby-core/src/configDefaults.macos.yaml b/tabby-core/src/configDefaults.macos.yaml index b1de561c..5c76a1c6 100644 --- a/tabby-core/src/configDefaults.macos.yaml +++ b/tabby-core/src/configDefaults.macos.yaml @@ -70,4 +70,6 @@ hotkeys: - '⌘-Shift-W' profile-selector: - '⌘-E' + switch-profile: + - '⌘-Shift-E' pluginBlacklist: ['ssh'] diff --git a/tabby-core/src/configDefaults.windows.yaml b/tabby-core/src/configDefaults.windows.yaml index 21f5e496..cb2517d6 100644 --- a/tabby-core/src/configDefaults.windows.yaml +++ b/tabby-core/src/configDefaults.windows.yaml @@ -70,6 +70,8 @@ hotkeys: pane-maximize: - 'Ctrl-Alt-Enter' close-pane: [] + switch-profile: + - 'Ctrl-Alt-T' profile-selector: - 'Ctrl-Shift-T' pluginBlacklist: [] diff --git a/tabby-core/src/hotkeys.ts b/tabby-core/src/hotkeys.ts index 2854e545..d96a82f3 100644 --- a/tabby-core/src/hotkeys.ts +++ b/tabby-core/src/hotkeys.ts @@ -170,6 +170,10 @@ export class AppHotkeyProvider extends HotkeyProvider { id: 'pane-nav-next', name: 'Focus next pane', }, + { + id: 'switch-profile', + name: 'Switch profile in the active pane', + }, { id: 'close-pane', name: 'Close focused pane', diff --git a/tabby-core/src/index.ts b/tabby-core/src/index.ts index 130bc159..419da2ae 100644 --- a/tabby-core/src/index.ts +++ b/tabby-core/src/index.ts @@ -40,7 +40,7 @@ import { HotkeysService } from './services/hotkeys.service' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { CoreConfigProvider } from './config' import { AppHotkeyProvider } from './hotkeys' -import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu' +import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu, ProfilesContextMenu } from './tabContextMenu' import { LastCLIHandler, ProfileCLIHandler } from './cli' import { ButtonProvider } from './buttonProvider' @@ -56,6 +56,7 @@ const PROVIDERS = [ { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, + { provide: TabContextMenuItemProvider, useClass: ProfilesContextMenu, multi: true }, { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, { provide: CLIHandler, useClass: ProfileCLIHandler, multi: true }, { provide: CLIHandler, useClass: LastCLIHandler, multi: true }, diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index ebb339fe..98e1bf3e 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/tabby-core/src/services/profiles.service.ts @@ -5,12 +5,16 @@ import { Profile, ProfileProvider } from '../api/profileProvider' import { SelectorOption } from '../api/selector' import { AppService } from './app.service' import { ConfigService } from './config.service' +import { NotificationsService } from './notifications.service' +import { SelectorService } from './selector.service' @Injectable({ providedIn: 'root' }) export class ProfilesService { constructor ( private app: AppService, private config: ConfigService, + private notifications: NotificationsService, + private selector: SelectorService, @Inject(ProfileProvider) private profileProviders: ProfileProvider[], ) { } @@ -60,4 +64,90 @@ export class ProfilesService { description: this.providerForProfile(profile)?.getDescription(profile), } } + + showProfileSelector (): Promise { + return new Promise(async (resolve, reject) => { + try { + const recentProfiles: Profile[] = this.config.store.recentProfiles + + let options: SelectorOption[] = recentProfiles.map(p => ({ + ...this.selectorOptionForProfile(p), + icon: 'fas fa-history', + callback: async () => { + if (p.id) { + p = (await this.getProfiles()).find(x => x.id === p.id) ?? p + } + resolve(p) + }, + })) + if (recentProfiles.length) { + options.push({ + name: 'Clear recent connections', + icon: 'fas fa-eraser', + callback: async () => { + this.config.store.recentProfiles = [] + this.config.save() + resolve(null) + }, + }) + } + + let profiles = await this.getProfiles() + + if (!this.config.store.terminal.showBuiltinProfiles) { + profiles = profiles.filter(x => !x.isBuiltin) + } + + profiles = profiles.filter(x => !x.isTemplate) + + options = [...options, ...profiles.map((p): SelectorOption => ({ + ...this.selectorOptionForProfile(p), + callback: () => resolve(p), + }))] + + try { + const { SettingsTabComponent } = window['nodeRequire']('tabby-settings') + options.push({ + name: 'Manage profiles', + icon: 'fas fa-window-restore', + callback: () => { + this.app.openNewTabRaw({ + type: SettingsTabComponent, + inputs: { activeTab: 'profiles' }, + }) + resolve(null) + }, + }) + } catch { } + + if (this.getProviders().some(x => x.supportsQuickConnect)) { + options.push({ + name: 'Quick connect', + freeInputPattern: 'Connect to "%s"...', + icon: 'fas fa-arrow-right', + callback: query => { + const profile = this.quickConnect(query) + resolve(profile) + }, + }) + } + await this.selector.show('Select profile', options) + } catch (err) { + reject(err) + } + }) + } + + async quickConnect (query: string): Promise { + for (const provider of this.getProviders()) { + if (provider.supportsQuickConnect) { + const profile = provider.quickConnect(query) + if (profile) { + return profile + } + } + } + this.notifications.error(`Could not parse "${query}"`) + return null + } } diff --git a/tabby-core/src/tabContextMenu.ts b/tabby-core/src/tabContextMenu.ts index 978e7680..809256df 100644 --- a/tabby-core/src/tabContextMenu.ts +++ b/tabby-core/src/tabContextMenu.ts @@ -7,6 +7,9 @@ import { TabHeaderComponent } from './components/tabHeader.component' import { SplitTabComponent, SplitDirection } from './components/splitTab.component' import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' import { MenuItemOptions } from './api/menu' +import { ProfilesService } from './services/profiles.service' +import { TabsService } from './services/tabs.service' +import { HotkeysService } from './services/hotkeys.service' /** @hidden */ @Injectable() @@ -203,3 +206,65 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider { return items } } + + +/** @hidden */ +@Injectable() +export class ProfilesContextMenu extends TabContextMenuItemProvider { + weight = 10 + + constructor ( + private profilesService: ProfilesService, + private tabsService: TabsService, + private app: AppService, + hotkeys: HotkeysService, + ) { + super() + hotkeys.hotkey$.subscribe(hotkey => { + if (hotkey === 'switch-profile') { + let tab = this.app.activeTab + if (tab instanceof SplitTabComponent) { + tab = tab.getFocusedTab() + if (tab) { + this.switchTabProfile(tab) + } + } + } + }) + } + + async switchTabProfile (tab: BaseTabComponent) { + const profile = await this.profilesService.showProfileSelector() + if (!profile) { + return + } + + const params = await this.profilesService.newTabParametersForProfile(profile) + if (!params) { + return + } + + if (!await tab.canClose()) { + return + } + + const newTab = this.tabsService.create(params) + ;(tab.parent as SplitTabComponent).replaceTab(tab, newTab) + + tab.destroy() + } + + async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise { + + if (!tabHeader && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) { + return [ + { + label: 'Switch profile', + click: () => this.switchTabProfile(tab), + }, + ] + } + + return [] + } +}