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