diff --git a/desktop/src/app/components/apps/terminal/terminal.component.html b/desktop/src/app/components/apps/terminal/terminal.component.html new file mode 100644 index 0000000..89009ce --- /dev/null +++ b/desktop/src/app/components/apps/terminal/terminal.component.html @@ -0,0 +1,19 @@ +
+
+ @for (line of lines(); track $index) { +
{{ line }}
+ } +
+ > + +
+
+
diff --git a/desktop/src/app/components/apps/terminal/terminal.component.scss b/desktop/src/app/components/apps/terminal/terminal.component.scss new file mode 100644 index 0000000..4fbf9e2 --- /dev/null +++ b/desktop/src/app/components/apps/terminal/terminal.component.scss @@ -0,0 +1,51 @@ +:host { + display: block; + width: 100%; + height: 100%; +} + +.terminal { + width: 100%; + height: 100%; + background: #0c0c0c; + color: #cccccc; + font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; + font-size: 14px; + line-height: 1.4; + display: flex; + flex-direction: column; +} + +.terminal-body { + flex: 1; + overflow-y: auto; + padding: 8px 12px; + white-space: pre-wrap; + word-break: break-all; +} + +.terminal-line { + min-height: 1.4em; +} + +.terminal-input-line { + display: flex; + align-items: center; +} + +.prompt { + color: #6ec6ff; + margin-right: 8px; + user-select: none; +} + +.terminal-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: #cccccc; + font-family: inherit; + font-size: inherit; + caret-color: #ccc; +} diff --git a/desktop/src/app/components/apps/terminal/terminal.component.ts b/desktop/src/app/components/apps/terminal/terminal.component.ts new file mode 100644 index 0000000..db52354 --- /dev/null +++ b/desktop/src/app/components/apps/terminal/terminal.component.ts @@ -0,0 +1,110 @@ +import { Component, ElementRef, ViewChild, AfterViewInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-terminal', + standalone: true, + imports: [CommonModule], + templateUrl: './terminal.component.html', + styleUrl: './terminal.component.scss', +}) +export class TerminalComponent implements AfterViewInit { + @ViewChild('terminalBody') terminalBody!: ElementRef; + @ViewChild('termInput') termInput!: ElementRef; + + /** Command history for up/down arrow navigation. */ + private history: string[] = []; + private historyIndex = -1; + + /** Lines of terminal output. */ + readonly lines = signal([ + 'Butterfly Terminal v0.1', + 'Type "help" for available commands.', + '', + ]); + + ngAfterViewInit(): void { + this.termInput?.nativeElement.focus(); + } + + onInput(event: Event): void { + // Just keep focus. + } + + onKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter') { + const input = this.termInput.nativeElement; + const cmd = input.value.trim(); + input.value = ''; + if (cmd) { + this.history.push(cmd); + this.historyIndex = this.history.length; + this.executeCommand(cmd); + } + this.lines.update((l) => [...l, `> ${cmd}`]); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + if (this.historyIndex > 0) { + this.historyIndex--; + this.termInput.nativeElement.value = this.history[this.historyIndex]; + } + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.termInput.nativeElement.value = this.history[this.historyIndex]; + } else { + this.historyIndex = this.history.length; + this.termInput.nativeElement.value = ''; + } + } + } + + private executeCommand(cmd: string): void { + const parts = cmd.split(/\s+/); + const command = parts[0].toLowerCase(); + + switch (command) { + case 'help': + this.addOutput('Available commands: help, clear, echo, date, uptime, whoami, about'); + break; + case 'clear': + this.lines.set([]); + return; + case 'echo': + this.addOutput(parts.slice(1).join(' ')); + break; + case 'date': + this.addOutput(new Date().toString()); + break; + case 'uptime': + this.addOutput(`System uptime: ${Math.floor(performance.now() / 1000)}s`); + break; + case 'whoami': + this.addOutput('butterfly-user'); + break; + case 'about': + this.addOutput('Butterfly Desktop Environment v0.1'); + this.addOutput('A browser-based remote desktop environment.'); + break; + default: + this.addOutput(`Unknown command: ${command}. Type "help" for available commands.`); + } + } + + private addOutput(text: string): void { + this.lines.update((l) => [...l, text]); + } + + /** Focus the input when the terminal is clicked. */ + onClick(): void { + this.termInput?.nativeElement.focus(); + } + + /** Called after view updates to scroll to bottom. */ + scrollToBottom(): void { + if (this.terminalBody) { + this.terminalBody.nativeElement.scrollTop = this.terminalBody.nativeElement.scrollHeight; + } + } +}