desktop: components/apps/terminal — basic terminal emulator with command history, help/clear/echo/date/uptime/whoami
This commit is contained in:
parent
6b4c6419ce
commit
7496fbeced
@ -0,0 +1,19 @@
|
|||||||
|
<div class="terminal" (click)="onClick()">
|
||||||
|
<div class="terminal-body" #terminalBody>
|
||||||
|
@for (line of lines(); track $index) {
|
||||||
|
<div class="terminal-line">{{ line }}</div>
|
||||||
|
}
|
||||||
|
<div class="terminal-input-line">
|
||||||
|
<span class="prompt">></span>
|
||||||
|
<input
|
||||||
|
#termInput
|
||||||
|
class="terminal-input"
|
||||||
|
type="text"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
110
desktop/src/app/components/apps/terminal/terminal.component.ts
Normal file
110
desktop/src/app/components/apps/terminal/terminal.component.ts
Normal file
@ -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<HTMLDivElement>;
|
||||||
|
@ViewChild('termInput') termInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
/** Command history for up/down arrow navigation. */
|
||||||
|
private history: string[] = [];
|
||||||
|
private historyIndex = -1;
|
||||||
|
|
||||||
|
/** Lines of terminal output. */
|
||||||
|
readonly lines = signal<string[]>([
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user