import { Component, inject } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; import { BRANDING } from '@shared/branding'; import { ConnectionManager } from './connection-manager/connection-manager'; import { ConnectionsService } from './connections.service'; import { ChmodDialog } from './dialogs/chmod-dialog'; import { ConfirmDialog } from './dialogs/confirm-dialog'; import { OverwriteDialog } from './dialogs/overwrite-dialog'; import { PromptDialog } from './dialogs/prompt-dialog'; import { DialogsService } from './dialogs.service'; import { FileActionsService } from './file-actions.service'; import { FilePanel } from './file-panel/file-panel'; import { PanelsService } from './panels.service'; import { RpcService } from './rpc.service'; import { StatusBar } from './status-bar/status-bar'; import { TabBar } from './tab-bar/tab-bar'; import { ToastService } from './toast.service'; import { Toasts } from './toasts/toasts'; import { TransferBar } from './transfer-bar/transfer-bar'; import { TransfersService } from './transfers.service'; @Component({ selector: 'app-root', imports: [ NgIcon, FilePanel, TabBar, StatusBar, PromptDialog, ConfirmDialog, ChmodDialog, OverwriteDialog, Toasts, TransferBar, ConnectionManager, ], templateUrl: './app.html', host: { '(window:keydown)': 'onWindowKeydown($event)' }, }) export class App { readonly rpc = inject(RpcService); readonly toast = inject(ToastService); readonly dialogs = inject(DialogsService); readonly panels = inject(PanelsService); readonly actions = inject(FileActionsService); readonly connections = inject(ConnectionsService); private readonly transfers = inject(TransfersService); constructor() { document.title = BRANDING.appName; if (this.rpc.available) { this.rpc.onMenuAction((action) => this.handleMenuAction(action)); void this.promptForEditLeftovers(); } } /** Temp files from external edits in a previous run (crash / quit mid-edit). */ private async promptForEditLeftovers(): Promise { const res = await this.rpc.request.getEditLeftovers({}); if (!res.ok || !res.names.length) return; const shown = res.names.slice(0, 5).join(', '); const more = res.names.length > 5 ? `, +${res.names.length - 5} more` : ''; this.dialogs.confirm.set({ title: 'Leftover edited files', body: `A previous session left ${res.names.length} temporary edited file(s) behind (${shown}${more}). Clear them?`, confirmLabel: 'Clear', destructive: true, onConfirm: () => void this.rpc.request.clearEditLeftovers({}), }); } /** Native application-menu actions forwarded from the bun process. */ private handleMenuAction(action: string): void { if (action.startsWith('ctx-')) { this.handleContextAction(action); return; } if (action.startsWith('connect:')) { const id = action.slice('connect:'.length); const connection = this.connections.connections().find((c) => c.id === id); if (connection) { void this.panels.connectPanel( this.panels.activeSide(), connection, connection.name, connection.localDir, ); } return; } switch (action) { case 'new-tab': this.panels.newWorkspace(); break; case 'close-tab': this.panels.closeWorkspace(this.panels.activeWorkspaceId()); break; case 'new-folder': this.actions.mkdir(this.panels.active()); break; case 'new-file': this.actions.createFile(this.panels.active()); break; case 'edit-file': void this.actions.edit(this.panels.active()); break; case 'refresh-active': void this.panels.active().refresh(); break; case 'manage-connections': this.dialogs.manager.set(true); break; } } /** Tab (workspace) shortcuts — panel-level keys live on the panels themselves. */ onWindowKeydown(event: KeyboardEvent): void { const mod = event.metaKey || event.ctrlKey; if (mod && event.key.toLowerCase() === 't') { event.preventDefault(); this.panels.newWorkspace(); } else if (mod && event.key.toLowerCase() === 'w') { event.preventDefault(); this.panels.closeWorkspace(this.panels.activeWorkspaceId()); } else if (event.ctrlKey && event.key === 'Tab') { event.preventDefault(); this.panels.cycleWorkspace(event.shiftKey ? -1 : 1); } else if (mod && !event.shiftKey && !event.altKey) { // ⌘1–⌘8 jump to that tab, ⌘9 to the last (Ctrl+digit elsewhere), like // Firefox. Matched on event.code so the digit row works on any layout. const digit = /^(?:Digit|Numpad)([1-9])$/.exec(event.code)?.[1]; if (!digit) return; event.preventDefault(); this.panels.goToWorkspace(digit === '9' ? -1 : Number(digit) - 1); } } /** Context-menu actions apply to the row/panel remembered at right-click time. */ private handleContextAction(action: string): void { const target = this.actions.menuTarget; if (!target) return; const { panel, side, entry } = target; switch (action) { case 'ctx-open': if (entry) panel.open(entry); break; case 'ctx-edit': void this.actions.edit(panel); break; case 'ctx-copy': void this.transfers.copyToOtherPanel(side); break; case 'ctx-rename': this.actions.rename(panel); break; case 'ctx-chmod': this.actions.chmod(panel); break; case 'ctx-delete': this.actions.delete(panel); break; case 'ctx-trash': if (entry) void this.actions.trash(panel, entry); break; case 'ctx-new-folder': this.actions.mkdir(panel); break; case 'ctx-new-file': this.actions.createFile(panel); break; case 'ctx-refresh': void panel.refresh(); break; } } }