Skip to content

Commit

Permalink
fixed #6164 - added command palette
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny committed Nov 1, 2022
1 parent 856c042 commit f094db9
Show file tree
Hide file tree
Showing 17 changed files with 187 additions and 30 deletions.
34 changes: 34 additions & 0 deletions tabby-core/src/api/commands.ts
Original file line number Diff line number Diff line change
@@ -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,
}
1 change: 1 addition & 0 deletions tabby-core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions tabby-core/src/api/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export interface MenuItemOptions {
checked?: boolean
submenu?: MenuItemOptions[]
click?: () => void

/** @hidden */
commandLabel?: string
}
3 changes: 1 addition & 2 deletions tabby-core/src/api/tabContextMenuProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BaseTabComponent } from '../components/baseTab.component'
import { TabHeaderComponent } from '../components/tabHeader.component'
import { MenuItemOptions } from './menu'

/**
Expand All @@ -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[]>
}
3 changes: 3 additions & 0 deletions tabby-core/src/api/toolbarButtonProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export interface ToolbarButton {
showInToolbar?: boolean

showInStartPage?: boolean

/** @hidden */
commandLabel?: string
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tabby-core/src/components/selectorModal.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ input {
border-radius: 0;
border: none;
}

profile-icon {
width: 14px;
height: 14px;
}
21 changes: 4 additions & 17 deletions tabby-core/src/components/tabHeader.component.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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)
}
}
})
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
}

Expand Down
2 changes: 2 additions & 0 deletions tabby-core/src/configDefaults.linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,5 @@ hotkeys:
- 'Ctrl-Alt-T'
profile-selector:
- 'Ctrl-Shift-E'
command-selector:
- 'Ctrl-Shift-P'
2 changes: 2 additions & 0 deletions tabby-core/src/configDefaults.macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,7 @@ hotkeys:
- '⌘-E'
switch-profile:
- '⌘-Shift-E'
command-selector:
- '⌘-Shift-P'
appearance:
vibrancy: true
2 changes: 2 additions & 0 deletions tabby-core/src/configDefaults.windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,5 @@ hotkeys:
- 'Ctrl-Alt-T'
profile-selector:
- 'Ctrl-Shift-E'
command-selector:
- 'Ctrl-Shift-P'
6 changes: 5 additions & 1 deletion tabby-core/src/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions tabby-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -195,6 +197,9 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
}
this.showSelector(provider)
}
if (hotkey === 'command-selector') {
commands.showSelector()
}
})
}

Expand Down
15 changes: 14 additions & 1 deletion tabby-core/src/services/app.service.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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()
Expand Down
86 changes: 86 additions & 0 deletions tabby-core/src/services/commands.service.ts
Original file line number Diff line number Diff line change
@@ -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,
})),
)
}
}
17 changes: 14 additions & 3 deletions tabby-core/src/tabContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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)
},
Expand All @@ -104,22 +110,27 @@ 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
items = [
...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,
Expand Down

0 comments on commit f094db9

Please sign in to comment.