added a way to switch a pane's profile - fixes #1007, fixes #2963, fixes #3082, fixes #4151

上级 6f912dc1
......@@ -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<void>[] = 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()
},
})
}
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<void> => ({
...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
}
}
const profile = await this.profilesService.showProfileSelector()
if (profile) {
this.launchProfile(profile)
}
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)
......
......@@ -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)
......
......@@ -69,6 +69,8 @@ hotkeys:
pane-maximize:
- 'Ctrl-Alt-Enter'
close-pane: []
switch-profile:
- 'Ctrl-Alt-T'
profile-selector:
- 'Ctrl-Shift-T'
pluginBlacklist: ['ssh']
......@@ -70,4 +70,6 @@ hotkeys:
- '⌘-Shift-W'
profile-selector:
- '⌘-E'
switch-profile:
- '⌘-Shift-E'
pluginBlacklist: ['ssh']
......@@ -70,6 +70,8 @@ hotkeys:
pane-maximize:
- 'Ctrl-Alt-Enter'
close-pane: []
switch-profile:
- 'Ctrl-Alt-T'
profile-selector:
- 'Ctrl-Shift-T'
pluginBlacklist: []
......@@ -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',
......
......@@ -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 },
......
......@@ -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<Profile|null> {
return new Promise<Profile|null>(async (resolve, reject) => {
try {
const recentProfiles: Profile[] = this.config.store.recentProfiles
let options: SelectorOption<void>[] = 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<void> => ({
...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<Profile|null> {
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
}
}
......@@ -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<MenuItemOptions[]> {
if (!tabHeader && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) {
return [
{
label: 'Switch profile',
click: () => this.switchTabProfile(tab),
},
]
}
return []
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册