diff --git a/package.json b/package.json index 7c577312..61d447ba 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "fast-glob": "^3.3.3", "get-port": "^7.1.0", "junk": "^4.0.1", + "open": "^10.1.2", "picomatch": "^4.0.2", "pretty-hrtime": "^1.0.3", "tmp-cache": "^1.1.0", diff --git a/src/dev_server.ts b/src/dev_server.ts index 6a22415d..ce398d66 100644 --- a/src/dev_server.ts +++ b/src/dev_server.ts @@ -20,6 +20,7 @@ import { type UnWrapLazyImport } from '@poppinss/utils/types' import debug from './debug.ts' import { FileSystem } from './file_system.ts' +import { ShortcutsManager } from './shortcuts_manager.ts' import type { DevServerOptions } from './types/common.ts' import { type DevServerHooks, type WatcherHooks } from './types/hooks.ts' import { getPort, loadHooks, parseConfig, runNode, throttle, watch } from './utils.ts' @@ -68,6 +69,11 @@ export class DevServer { */ #httpServer?: ResultPromise + /** + * Keyboard shortcuts manager + */ + #shortcutsManager?: ShortcutsManager + /** * Filesystem is used to decide which files to watch or entertain when * using hot-hook @@ -103,6 +109,29 @@ export class DevServer { await this.#startHTTPServer(this.#stickyPort) }, 'restartHTTPServer') + /** + * Sets up keyboard shortcuts + */ + #setupKeyboardShortcuts() { + this.#shortcutsManager = new ShortcutsManager({ + logger: this.ui.logger, + callbacks: { + onRestart: () => this.#restartHTTPServer(), + onClear: () => this.#clearScreen(), + onQuit: () => this.close(), + }, + }) + + this.#shortcutsManager.setup() + } + + /** + * Cleanup keyboard shortcuts + */ + #cleanupKeyboardShortcuts() { + this.#shortcutsManager?.cleanup() + } + /** * CLI UI to log colorful messages */ @@ -152,15 +181,20 @@ export class DevServer { */ async #postServerReady(message: { port: number; host: string; duration?: [number, number] }) { const host = message.host === '0.0.0.0' ? '127.0.0.1' : message.host + const serverUrl = `http://${host}:${message.port}` + this.#shortcutsManager?.setServerUrl(serverUrl) + const displayMessage = this.ui .sticker() - .add(`Server address: ${this.ui.colors.cyan(`http://${host}:${message.port}`)}`) + .add(`Server address: ${this.ui.colors.cyan(serverUrl)}`) .add(`Mode: ${this.ui.colors.cyan(this.mode)}`) if (message.duration) { displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`) } + displayMessage.add(`Press ${this.ui.colors.dim('h')} to show help`) + /** * Run hooks before displaying the "displayMessage". It will allow hooks to add * custom lines to the display message. @@ -392,6 +426,7 @@ export class DevServer { * Close watchers and the running child process */ async close() { + this.#cleanupKeyboardShortcuts() await this.#watcher?.close() if (this.#httpServer) { this.#httpServer.removeAllListeners() @@ -434,6 +469,7 @@ export class DevServer { } this.#clearScreen() + this.#setupKeyboardShortcuts() this.ui.logger.info('starting HTTP server...') await this.#startHTTPServer(this.#stickyPort) } @@ -460,6 +496,7 @@ export class DevServer { this.#registerServerRestartHooks() this.#clearScreen() + this.#setupKeyboardShortcuts() this.ui.logger.info('starting HTTP server...') await this.#startHTTPServer(this.#stickyPort) diff --git a/src/shortcuts_manager.ts b/src/shortcuts_manager.ts new file mode 100644 index 00000000..4c9975d6 --- /dev/null +++ b/src/shortcuts_manager.ts @@ -0,0 +1,143 @@ +/* + * @adonisjs/assembler + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Logger } from '@poppinss/cliui' + +/** + * Keyboard shortcut definition + */ +export interface KeyboardShortcut { + key: string + description: string + handler: () => void +} + +/** + * Callbacks for keyboard shortcuts actions + */ +export interface KeyboardShortcutsCallbacks { + onRestart: () => void + onClear: () => void + onQuit: () => void +} + +/** + * Shortcuts manager options + */ +export interface ShortcutsManagerOptions { + logger: Logger + callbacks: KeyboardShortcutsCallbacks +} + +/** + * Manages keyboard shortcuts for development server + */ +export class ShortcutsManager { + #logger: Logger + #callbacks: KeyboardShortcutsCallbacks + #serverUrl?: string + #keyPressHandler?: (data: Buffer) => void + + #shortcuts: KeyboardShortcut[] = [ + { + key: 'r', + description: 'restart server', + handler: () => { + this.#logger.log('') + this.#logger.info('Manual restart triggered...') + this.#callbacks.onRestart() + }, + }, + { + key: 'c', + description: 'clear console', + handler: () => { + this.#callbacks.onClear() + this.#logger.info('Console cleared') + }, + }, + { + key: 'o', + description: 'open in browser', + handler: () => this.#handleOpenBrowser(), + }, + { + key: 'h', + description: 'show this help', + handler: () => this.showHelp(), + }, + ] + + constructor(options: ShortcutsManagerOptions) { + this.#logger = options.logger + this.#callbacks = options.callbacks + } + + /** + * Set server url for opening in browser + */ + setServerUrl(url: string) { + this.#serverUrl = url + } + + /** + * Initialize keyboard shortcuts + */ + setup() { + if (!process.stdin.isTTY) return + + process.stdin.setRawMode(true) + + this.#keyPressHandler = (data: Buffer) => this.#handleKeyPress(data.toString()) + process.stdin.on('data', this.#keyPressHandler) + } + + /** + * Handle key press events + */ + #handleKeyPress(key: string) { + // Handle Ctrl+C (0x03) and Ctrl+D (0x04) + if (key === '\u0003' || key === '\u0004') return this.#callbacks.onQuit() + + const shortcut = this.#shortcuts.find((s) => s.key === key) + if (shortcut) shortcut.handler() + } + + /** + * Handle opening browser + */ + async #handleOpenBrowser() { + this.#logger.log('') + this.#logger.info(`Opening ${this.#serverUrl}...`) + + const { default: open } = await import('open') + open(this.#serverUrl!) + } + + /** + * Show available keyboard shortcuts + */ + showHelp() { + this.#logger.log('') + this.#logger.log('Available shortcuts:') + + this.#shortcuts.forEach(({ key, description }) => this.#logger.log(`ยท ${key}: ${description}`)) + } + + /** + * Cleanup keyboard shortcuts + */ + cleanup() { + if (!process.stdin.isTTY) return + + process.stdin.setRawMode(false) + process.stdin.removeListener('data', this.#keyPressHandler!) + this.#keyPressHandler = undefined + } +} diff --git a/tests/dev_server.spec.ts b/tests/dev_server.spec.ts index 685e892d..edfdf096 100644 --- a/tests/dev_server.spec.ts +++ b/tests/dev_server.spec.ts @@ -81,7 +81,8 @@ test.group('DevServer', () => { stream: 'stdout', }, { - message: 'Server address: cyan(http://localhost:3334)\nMode: cyan(static)', + message: + 'Server address: cyan(http://localhost:3334)\nMode: cyan(static)\nPress dim(h) to show help', stream: 'stdout', }, ]) @@ -139,7 +140,8 @@ test.group('DevServer', () => { stream: 'stdout', }, { - message: 'Server address: cyan(http://localhost:3335)\nMode: cyan(watch)', + message: + 'Server address: cyan(http://localhost:3335)\nMode: cyan(watch)\nPress dim(h) to show help', stream: 'stdout', }, ])