mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-18 14:16:05 +00:00
This commit is contained in:
@@ -2,26 +2,19 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
|
||||||
import { SelectorService } from './services/selector.service'
|
|
||||||
import { HostAppService, Platform } from './api/hostApp'
|
import { HostAppService, Platform } from './api/hostApp'
|
||||||
import { Profile } from './api/profileProvider'
|
import { Profile } from './api/profileProvider'
|
||||||
import { ConfigService } from './services/config.service'
|
import { ConfigService } from './services/config.service'
|
||||||
import { SelectorOption } from './api/selector'
|
|
||||||
import { HotkeysService } from './services/hotkeys.service'
|
import { HotkeysService } from './services/hotkeys.service'
|
||||||
import { ProfilesService } from './services/profiles.service'
|
import { ProfilesService } from './services/profiles.service'
|
||||||
import { AppService } from './services/app.service'
|
|
||||||
import { NotificationsService } from './services/notifications.service'
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ButtonProvider extends ToolbarButtonProvider {
|
export class ButtonProvider extends ToolbarButtonProvider {
|
||||||
constructor (
|
constructor (
|
||||||
private selector: SelectorService,
|
|
||||||
private app: AppService,
|
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private profilesServices: ProfilesService,
|
private profilesService: ProfilesService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private notifications: NotificationsService,
|
|
||||||
hotkeys: HotkeysService,
|
hotkeys: HotkeysService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@@ -33,80 +26,14 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activate () {
|
async activate () {
|
||||||
const recentProfiles: Profile[] = this.config.store.recentProfiles
|
const profile = await this.profilesService.showProfileSelector()
|
||||||
|
if (profile) {
|
||||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
this.launchProfile(profile)
|
||||||
...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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.notifications.error(`Could not parse "${query}"`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchProfile (profile: Profile) {
|
async launchProfile (profile: Profile) {
|
||||||
await this.profilesServices.openNewTabForProfile(profile)
|
await this.profilesService.openNewTabForProfile(profile)
|
||||||
|
|
||||||
let recentProfiles = this.config.store.recentProfiles
|
let recentProfiles = this.config.store.recentProfiles
|
||||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
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()
|
await this.initialized$.toPromise()
|
||||||
|
|
||||||
this.attachTabView(tab)
|
this.attachTabView(tab)
|
||||||
|
this.onAfterTabAdded(tab)
|
||||||
setImmediate(() => {
|
|
||||||
this.layout()
|
|
||||||
this.tabAdded.next(tab)
|
|
||||||
this.focus(tab)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTab (tab: BaseTabComponent): void {
|
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
|
* 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) {
|
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
|
||||||
const size = root.orientation === 'v' ? h : w
|
const size = root.orientation === 'v' ? h : w
|
||||||
const sizes = root.ratios.map(ratio => ratio * size)
|
const sizes = root.ratios.map(ratio => ratio * size)
|
||||||
|
@@ -69,6 +69,8 @@ hotkeys:
|
|||||||
pane-maximize:
|
pane-maximize:
|
||||||
- 'Ctrl-Alt-Enter'
|
- 'Ctrl-Alt-Enter'
|
||||||
close-pane: []
|
close-pane: []
|
||||||
|
switch-profile:
|
||||||
|
- 'Ctrl-Alt-T'
|
||||||
profile-selector:
|
profile-selector:
|
||||||
- 'Ctrl-Shift-T'
|
- 'Ctrl-Shift-T'
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -70,4 +70,6 @@ hotkeys:
|
|||||||
- '⌘-Shift-W'
|
- '⌘-Shift-W'
|
||||||
profile-selector:
|
profile-selector:
|
||||||
- '⌘-E'
|
- '⌘-E'
|
||||||
|
switch-profile:
|
||||||
|
- '⌘-Shift-E'
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -70,6 +70,8 @@ hotkeys:
|
|||||||
pane-maximize:
|
pane-maximize:
|
||||||
- 'Ctrl-Alt-Enter'
|
- 'Ctrl-Alt-Enter'
|
||||||
close-pane: []
|
close-pane: []
|
||||||
|
switch-profile:
|
||||||
|
- 'Ctrl-Alt-T'
|
||||||
profile-selector:
|
profile-selector:
|
||||||
- 'Ctrl-Shift-T'
|
- 'Ctrl-Shift-T'
|
||||||
pluginBlacklist: []
|
pluginBlacklist: []
|
||||||
|
@@ -170,6 +170,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||||||
id: 'pane-nav-next',
|
id: 'pane-nav-next',
|
||||||
name: 'Focus next pane',
|
name: 'Focus next pane',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'switch-profile',
|
||||||
|
name: 'Switch profile in the active pane',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'close-pane',
|
id: 'close-pane',
|
||||||
name: 'Close focused pane',
|
name: 'Close focused pane',
|
||||||
|
@@ -40,7 +40,7 @@ import { HotkeysService } from './services/hotkeys.service'
|
|||||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||||
import { CoreConfigProvider } from './config'
|
import { CoreConfigProvider } from './config'
|
||||||
import { AppHotkeyProvider } from './hotkeys'
|
import { AppHotkeyProvider } from './hotkeys'
|
||||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu'
|
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu, ProfilesContextMenu } from './tabContextMenu'
|
||||||
import { LastCLIHandler, ProfileCLIHandler } from './cli'
|
import { LastCLIHandler, ProfileCLIHandler } from './cli'
|
||||||
import { ButtonProvider } from './buttonProvider'
|
import { ButtonProvider } from './buttonProvider'
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ const PROVIDERS = [
|
|||||||
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
||||||
{ provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true },
|
{ provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true },
|
||||||
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
||||||
|
{ provide: TabContextMenuItemProvider, useClass: ProfilesContextMenu, multi: true },
|
||||||
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
||||||
{ provide: CLIHandler, useClass: ProfileCLIHandler, multi: true },
|
{ provide: CLIHandler, useClass: ProfileCLIHandler, multi: true },
|
||||||
{ provide: CLIHandler, useClass: LastCLIHandler, multi: true },
|
{ provide: CLIHandler, useClass: LastCLIHandler, multi: true },
|
||||||
|
@@ -5,12 +5,16 @@ import { Profile, ProfileProvider } from '../api/profileProvider'
|
|||||||
import { SelectorOption } from '../api/selector'
|
import { SelectorOption } from '../api/selector'
|
||||||
import { AppService } from './app.service'
|
import { AppService } from './app.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
|
import { NotificationsService } from './notifications.service'
|
||||||
|
import { SelectorService } from './selector.service'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ProfilesService {
|
export class ProfilesService {
|
||||||
constructor (
|
constructor (
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
|
private notifications: NotificationsService,
|
||||||
|
private selector: SelectorService,
|
||||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider[],
|
@Inject(ProfileProvider) private profileProviders: ProfileProvider[],
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -60,4 +64,90 @@ export class ProfilesService {
|
|||||||
description: this.providerForProfile(profile)?.getDescription(profile),
|
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 { SplitTabComponent, SplitDirection } from './components/splitTab.component'
|
||||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||||
import { MenuItemOptions } from './api/menu'
|
import { MenuItemOptions } from './api/menu'
|
||||||
|
import { ProfilesService } from './services/profiles.service'
|
||||||
|
import { TabsService } from './services/tabs.service'
|
||||||
|
import { HotkeysService } from './services/hotkeys.service'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -203,3 +206,65 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
|||||||
return items
|
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 []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user