mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-22 12:29:53 +00:00
fixed #6164 - added command palette
This commit is contained in:
parent
856c042bb8
commit
f094db9de2
34
tabby-core/src/api/commands.ts
Normal file
34
tabby-core/src/api/commands.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { MenuItemOptions } from './menu'
|
||||
import { ToolbarButton } from './toolbarButtonProvider'
|
||||
|
||||
export class Command {
|
||||
label: string
|
||||
sublabel?: string
|
||||
click?: () => void
|
||||
|
||||
/**
|
||||
* Raw SVG icon code
|
||||
*/
|
||||
icon?: string
|
||||
|
||||
static fromToolbarButton (button: ToolbarButton): Command {
|
||||
const command = new Command()
|
||||
command.label = button.commandLabel ?? button.title
|
||||
command.click = button.click
|
||||
command.icon = button.icon
|
||||
return command
|
||||
}
|
||||
|
||||
static fromMenuItem (item: MenuItemOptions): Command {
|
||||
const command = new Command()
|
||||
command.label = item.commandLabel ?? item.label ?? ''
|
||||
command.sublabel = item.sublabel
|
||||
command.click = item.click
|
||||
return command
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandContext {
|
||||
tab?: BaseTabComponent,
|
||||
}
|
@ -18,6 +18,7 @@ export { HostAppService, Platform } from './hostApp'
|
||||
export { FileProvider } from './fileProvider'
|
||||
export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
|
||||
export { PromptModalComponent } from '../components/promptModal.component'
|
||||
export * from './commands'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService, configMerge, ConfigProxy } from '../services/config.service'
|
||||
|
@ -6,4 +6,7 @@ export interface MenuItemOptions {
|
||||
checked?: boolean
|
||||
submenu?: MenuItemOptions[]
|
||||
click?: () => void
|
||||
|
||||
/** @hidden */
|
||||
commandLabel?: string
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
import { MenuItemOptions } from './menu'
|
||||
|
||||
/**
|
||||
@ -8,5 +7,5 @@ import { MenuItemOptions } from './menu'
|
||||
export abstract class TabContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
abstract getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]>
|
||||
abstract getItems (tab: BaseTabComponent, tabHeader?: boolean): Promise<MenuItemOptions[]>
|
||||
}
|
||||
|
@ -31,6 +31,9 @@ export interface ToolbarButton {
|
||||
showInToolbar?: boolean
|
||||
|
||||
showInStartPage?: boolean
|
||||
|
||||
/** @hidden */
|
||||
commandLabel?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,3 +38,8 @@ input {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
profile-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { auditTime } from 'rxjs'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { SplitTabComponent } from './splitTab.component'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
@ -31,7 +29,6 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
public app: AppService,
|
||||
public config: ConfigService,
|
||||
public hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private platform: PlatformService,
|
||||
private zone: NgZone,
|
||||
@ -41,7 +38,7 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
this.subscribeUntilDestroyed(this.hotkeys.hotkey$, (hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
if (hotkey === 'rename-tab') {
|
||||
this.showRenameTabModal()
|
||||
this.app.renameTab(this.tab)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -58,27 +55,17 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
})
|
||||
}
|
||||
|
||||
showRenameTabModal (): void {
|
||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
modal.result.then(result => {
|
||||
this.tab.setTitle(result)
|
||||
this.tab.customTitle = result
|
||||
this.app.emitTabsChanged()
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<MenuItemOptions[]> {
|
||||
let items: MenuItemOptions[] = []
|
||||
// Top-level tab menu
|
||||
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
|
||||
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, true)))) {
|
||||
items.push({ type: 'separator' })
|
||||
items = items.concat(section)
|
||||
}
|
||||
if (this.tab instanceof SplitTabComponent) {
|
||||
const tab = this.tab.getFocusedTab()
|
||||
if (tab) {
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(tab, this)))) {
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(tab, true)))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
section = section.filter(item => !items.some(ex => ex.label === item.label))
|
||||
if (section.length) {
|
||||
@ -107,7 +94,7 @@ export class TabHeaderComponent extends BaseComponent {
|
||||
}
|
||||
|
||||
@HostListener('dblclick', ['$event']) onDoubleClick ($event: MouseEvent): void {
|
||||
this.showRenameTabModal()
|
||||
this.app.renameTab(this.tab)
|
||||
$event.stopPropagation()
|
||||
}
|
||||
|
||||
|
@ -94,3 +94,5 @@ hotkeys:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-E'
|
||||
command-selector:
|
||||
- 'Ctrl-Shift-P'
|
||||
|
@ -93,5 +93,7 @@ hotkeys:
|
||||
- '⌘-E'
|
||||
switch-profile:
|
||||
- '⌘-Shift-E'
|
||||
command-selector:
|
||||
- '⌘-Shift-P'
|
||||
appearance:
|
||||
vibrancy: true
|
||||
|
@ -95,3 +95,5 @@ hotkeys:
|
||||
- 'Ctrl-Alt-T'
|
||||
profile-selector:
|
||||
- 'Ctrl-Shift-E'
|
||||
command-selector:
|
||||
- 'Ctrl-Shift-P'
|
||||
|
@ -8,6 +8,10 @@ import { PartialProfile, Profile } from './api'
|
||||
@Injectable()
|
||||
export class AppHotkeyProvider extends HotkeyProvider {
|
||||
hotkeys: HotkeyDescription[] = [
|
||||
{
|
||||
id: 'command-selector',
|
||||
name: this.translate.instant('Show command selector'),
|
||||
},
|
||||
{
|
||||
id: 'profile-selector',
|
||||
name: this.translate.instant('Show profile selector'),
|
||||
@ -18,7 +22,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||
},
|
||||
{
|
||||
id: 'rename-tab',
|
||||
name: this.translate.instant('Rename Tab'),
|
||||
name: this.translate.instant('Rename tab'),
|
||||
},
|
||||
{
|
||||
id: 'close-tab',
|
||||
|
@ -44,6 +44,7 @@ import { ConfigService } from './services/config.service'
|
||||
import { VaultFileProvider } from './services/vault.service'
|
||||
import { HotkeysService } from './services/hotkeys.service'
|
||||
import { LocaleService, TranslateServiceWrapper } from './services/locale.service'
|
||||
import { CommandService } from './services/commands.service'
|
||||
|
||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
@ -161,6 +162,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
config: ConfigService,
|
||||
platform: PlatformService,
|
||||
hotkeys: HotkeysService,
|
||||
commands: CommandService,
|
||||
public locale: LocaleService,
|
||||
private translate: TranslateService,
|
||||
private profilesService: ProfilesService,
|
||||
@ -195,6 +197,9 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||
}
|
||||
this.showSelector(provider)
|
||||
}
|
||||
if (hotkey === 'command-selector') {
|
||||
commands.showSelector()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Observable, Subject, AsyncSubject, takeUntil, debounceTime } from 'rxjs'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable, Subject, AsyncSubject, takeUntil, debounceTime } from 'rxjs'
|
||||
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { SplitTabComponent } from '../components/splitTab.component'
|
||||
import { RenameTabModalComponent } from '../components/renameTabModal.component'
|
||||
import { SelectorOption } from '../api/selector'
|
||||
import { RecoveryToken } from '../api/tabRecovery'
|
||||
import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess'
|
||||
@ -80,6 +82,7 @@ export class AppService {
|
||||
private tabRecovery: TabRecoveryService,
|
||||
private tabsService: TabsService,
|
||||
private selector: SelectorService,
|
||||
private ngbModal: NgbModal,
|
||||
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
|
||||
) {
|
||||
this.tabsChanged$.subscribe(() => {
|
||||
@ -318,6 +321,16 @@ export class AppService {
|
||||
this.tabs[i2] = a
|
||||
}
|
||||
|
||||
renameTab (tab: BaseTabComponent): void {
|
||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = tab.customTitle || tab.title
|
||||
modal.result.then(result => {
|
||||
tab.setTitle(result)
|
||||
tab.customTitle = result
|
||||
this.emitTabsChanged()
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabsChanged (): void {
|
||||
this.tabsChanged.next()
|
||||
|
86
tabby-core/src/services/commands.service.ts
Normal file
86
tabby-core/src/services/commands.service.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Inject, Injectable, Optional } from '@angular/core'
|
||||
import { AppService, Command, CommandContext, ConfigService, MenuItemOptions, SplitTabComponent, TabContextMenuItemProvider, ToolbarButton, ToolbarButtonProvider, TranslateService } from '../api'
|
||||
import { SelectorService } from './selector.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CommandService {
|
||||
constructor (
|
||||
private selector: SelectorService,
|
||||
private config: ConfigService,
|
||||
private app: AppService,
|
||||
private translate: TranslateService,
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
) {
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
async getCommands (context: CommandContext): Promise<Command[]> {
|
||||
let buttons: ToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
buttons = buttons
|
||||
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||
|
||||
let items: MenuItemOptions[] = []
|
||||
if (context.tab) {
|
||||
for (const tabHeader of [false, true]) {
|
||||
// Top-level tab menu
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(context.tab!, tabHeader)))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
section = section.filter(item => !items.some(ex => ex.label === item.label))
|
||||
items = items.concat(section)
|
||||
}
|
||||
if (context.tab instanceof SplitTabComponent) {
|
||||
const tab = context.tab.getFocusedTab()
|
||||
if (tab) {
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(tab, tabHeader)))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
section = section.filter(item => !items.some(ex => ex.label === item.label))
|
||||
items = items.concat(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items = items.filter(x => (x.enabled ?? true) && x.type !== 'separator')
|
||||
|
||||
const flatItems: MenuItemOptions[] = []
|
||||
function flattenItem (item: MenuItemOptions, prefix?: string): void {
|
||||
if (item.submenu) {
|
||||
item.submenu.forEach(x => flattenItem(x, (prefix ? `${prefix} > ` : '') + (item.commandLabel ?? item.label)))
|
||||
} else {
|
||||
flatItems.push({
|
||||
...item,
|
||||
label: (prefix ? `${prefix} > ` : '') + (item.commandLabel ?? item.label),
|
||||
})
|
||||
}
|
||||
}
|
||||
items.forEach(x => flattenItem(x))
|
||||
|
||||
let commands = buttons.map(x => Command.fromToolbarButton(x))
|
||||
commands = commands.concat(flatItems.map(x => Command.fromMenuItem(x)))
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
async showSelector (): Promise<void> {
|
||||
const context: CommandContext = {}
|
||||
const tab = this.app.activeTab
|
||||
if (tab instanceof SplitTabComponent) {
|
||||
context.tab = tab.getFocusedTab() ?? undefined
|
||||
}
|
||||
const commands = await this.getCommands(context)
|
||||
await this.selector.show(
|
||||
this.translate.instant('Commands'),
|
||||
commands.map(c => ({
|
||||
name: c.label,
|
||||
callback: c.click,
|
||||
description: c.sublabel,
|
||||
icon: c.icon,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import { TranslateService } from '@ngx-translate/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { AppService } from './services/app.service'
|
||||
import { BaseTabComponent } from './components/baseTab.component'
|
||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||
import { SplitTabComponent, SplitDirection } from './components/splitTab.component'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
import { MenuItemOptions } from './api/menu'
|
||||
@ -32,6 +31,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
|
||||
let items: MenuItemOptions[] = [
|
||||
{
|
||||
label: this.translate.instant('Close'),
|
||||
commandLabel: this.translate.instant('Close tab'),
|
||||
click: () => {
|
||||
if (this.app.tabs.includes(tab)) {
|
||||
this.app.closeTab(tab, true)
|
||||
@ -80,6 +80,12 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
|
||||
l: this.translate.instant('Left'),
|
||||
t: this.translate.instant('Up'),
|
||||
}[dir],
|
||||
commandLabel: {
|
||||
r: this.translate.instant('Split to the right'),
|
||||
b: this.translate.instant('Split to the down'),
|
||||
l: this.translate.instant('Split to the left'),
|
||||
t: this.translate.instant('Split to the up'),
|
||||
}[dir],
|
||||
click: () => {
|
||||
(tab.parent as SplitTabComponent).splitTab(tab, dir)
|
||||
},
|
||||
@ -104,7 +110,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: boolean): Promise<MenuItemOptions[]> {
|
||||
let items: MenuItemOptions[] = []
|
||||
if (tabHeader) {
|
||||
const currentColor = TAB_COLORS.find(x => x.value === tab.color)?.name
|
||||
@ -112,14 +118,19 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
...items,
|
||||
{
|
||||
label: this.translate.instant('Rename'),
|
||||
click: () => tabHeader.showRenameTabModal(),
|
||||
commandLabel: this.translate.instant('Rename tab'),
|
||||
click: () => {
|
||||
this.app.renameTab(tab)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: this.translate.instant('Duplicate'),
|
||||
commandLabel: this.translate.instant('Duplicate tab'),
|
||||
click: () => this.app.duplicateTab(tab),
|
||||
},
|
||||
{
|
||||
label: this.translate.instant('Color'),
|
||||
commandLabel: this.translate.instant('Change tab color'),
|
||||
sublabel: currentColor ? this.translate.instant(currentColor) : undefined,
|
||||
submenu: TAB_COLORS.map(color => ({
|
||||
label: this.translate.instant(color.name) ?? color.name,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService, MenuItemOptions, ProfilesService, PromptModalComponent, TranslateService } from 'tabby-core'
|
||||
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, SplitTabComponent, NotificationsService, MenuItemOptions, ProfilesService, PromptModalComponent, TranslateService } from 'tabby-core'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { UACService } from './services/uac.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
@ -18,7 +18,7 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> {
|
||||
if (!(tab instanceof TerminalTabComponent)) {
|
||||
return []
|
||||
}
|
||||
@ -69,7 +69,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: boolean): Promise<MenuItemOptions[]> {
|
||||
const profiles = (await this.profilesService.getProfiles()).filter(x => x.type === 'local') as LocalProfile[]
|
||||
|
||||
const items: MenuItemOptions[] = [
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable, Optional, Inject } from '@angular/core'
|
||||
import { BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, NotificationsService, MenuItemOptions, TranslateService } from 'tabby-core'
|
||||
import { BaseTabComponent, TabContextMenuItemProvider, NotificationsService, MenuItemOptions, TranslateService } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
|
||||
import { TerminalContextMenuItemProvider } from './api/contextMenuProvider'
|
||||
|
||||
@ -15,7 +15,7 @@ export class CopyPasteContextMenu extends TabContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: boolean): Promise<MenuItemOptions[]> {
|
||||
if (tabHeader) {
|
||||
return []
|
||||
}
|
||||
@ -77,7 +77,7 @@ export class LegacyContextMenu extends TabContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
|
||||
async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> {
|
||||
if (!this.contextMenuProviders) {
|
||||
return []
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user