Files
Asuro/src/app/app.vterminal.pas
T
Aaron fcc9809248 vfs: major overhaul — device nodes, stream I/O, mode simplification
- 5-phase VFS architecture overhaul (path resolution, dir cache, watch/notify,
  symlinks, async API)
- Add character/block device node layer with /dev/null and /dev/zero built-ins
- RegisterDevice() API for custom device nodes with read/write/size callbacks
- Merge TOpenMode + TWriteMode into single TOpenMode enum
  (omRead, omWrite, omCreate, omReadWrite, omStream)
- Add stream write support: WriteFile/WriteFileAsync now accept omStream handles
  with auto-advancing offset for both device and volume FDs
- Add PPWriteOffsetHook type and writeOffsetCallback field to TFilesystem
- Add PPReadOffsetHook-based streaming reads for FAT32 and ISO9660
- Update all callers (notepad, edit, inio, wasm runner, filepicker, filedispatch,
  vterminal, stdio) to new 2-param OpenFile API
- 77 unit tests (0 failures) covering path ops, dir cache, symlinks, watch,
  device nodes, and stream read/write
- Add doc/vfs.md — comprehensive public VFS API documentation
2026-03-08 18:07:44 +00:00

1108 lines
41 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
Prog->VTerminal - LVGL-based Visual Terminal Emulator.
Multi-instance design: each launch allocates a TVTermState record
on the heap. All callbacks retrieve their state via LVGL
user_data, so multiple terminals can coexist independently.
Commands are executed as preemptive processes via
proc.mgr.runCommand. A per-instance LVGL timer polls the
foreground process for incremental output.
@author(Kieron Morris <[email protected]>)
}
unit app.vterminal;
interface
uses
driver.video.lvgl, driver.video, driver.video.windows, driver.video.desktop, driver.hid.keyboard, debug.tracer,
core.strings, core.util, arch.x86.util, memory.heap, core.version, io.stdio, driver.storage.vfs,
proc.mgr, proc.types, core.ds.lists, core.ds.hashmap, driver.storage.filedispatch;
procedure init;
implementation
const
TERM_W = 640;
TERM_H = 400;
MAX_TEXT = 4096; { max chars in scrollback buffer }
MAX_LINE = 1024; { max input line length, matches TCommandBuffer }
HIST_SIZE = 10; { number of history entries }
DRAIN_PERIOD = 100; { ms between output-drain polls }
type
PBackgroundJob = ^TBackgroundJob;
TBackgroundJob = record
PID : uint32;
JobNum : uint32;
CmdName : array[0..31] of char;
StdOut : POutBuf;
StdErr : POutBuf;
LastDrain : uint32;
LastDrainE : uint32;
end;
PVTermState = ^TVTermState;
TVTermState = record
{ LVGL widgets }
win_id : uint32;
content : Plv_obj;
text_label : Plv_obj;
focused : boolean;
drain_timer : Plv_timer;
{ Scrollback text buffer }
text_buf : array[0..MAX_TEXT-1] of char;
text_len : uint32;
{ Current input line }
line_buf : TCommandBuffer;
line_len : uint32;
{ Command history ring }
hist : array[0..HIST_SIZE-1] of TCommandBuffer;
hist_count : uint32;
hist_head : uint32;
hist_pos : sint32;
saved_line : TCommandBuffer;
saved_len : uint32;
{ Foreground process integration }
ForegroundPID : uint32;
fg_stdout : POutBuf;
fg_stderr : POutBuf;
fg_stdin : POutBuf;
last_drain : uint32;
last_drain_err : uint32;
fg_params : PParamList;
{ Terminal process }
TerminalPID : uint32;
{ Per-terminal working directory }
cwd : pchar;
dir_stack : PLinkedListBase;
{ Background jobs }
bg_jobs : PDList;
bg_next_job : uint32;
end;
{ ============================================================
terminal_entry — process entry point for the terminal itself
Loops yielding until terminated. This makes each terminal
visible in PS and manageable via KILL/TERMINATE.
============================================================ }
procedure terminal_entry(ctx : PProcessContext);
begin
while (ctx^.State <> psFinished) and (ctx^.PendingMsg <> smKill) and (ctx^.PendingMsg <> smTerminate) do
proc.mgr.proc_yield;
end;
{ ============================================================
Internal helpers — all take state explicitly
============================================================ }
procedure appendText(state : PVTermState; s: pchar);
var
slen, i: uint32;
begin
if s = nil then exit;
slen := stringSize(s);
if slen = 0 then exit;
if state^.text_len + slen >= MAX_TEXT - 1 then begin
i := MAX_TEXT div 2;
memcpy(uint32(@state^.text_buf[i]), uint32(@state^.text_buf[0]), state^.text_len - i);
state^.text_len := state^.text_len - i;
end;
memcpy(uint32(s), uint32(@state^.text_buf[state^.text_len]), slen);
state^.text_len := state^.text_len + slen;
state^.text_buf[state^.text_len] := #0;
end;
procedure appendChar(state : PVTermState; c: char);
var
tmp: array[0..1] of char;
begin
tmp[0] := c;
tmp[1] := #0;
appendText(state, @tmp[0]);
end;
procedure refreshDisplay(state : PVTermState);
var
sb: sint32;
begin
if (state^.text_label <> nil) and (state^.content <> nil) then begin
lv_label_set_text(state^.text_label, @state^.text_buf[0]);
lv_obj_update_layout(lv_screen_active);
sb := lv_obj_get_scroll_bottom(state^.content);
if sb > 0 then
lv_obj_scroll_by(state^.content, 0, -sb, LV_ANIM_OFF);
end;
end;
procedure showPrompt(state : PVTermState);
begin
appendText(state, 'asuro:');
if state^.cwd <> nil then
appendText(state, state^.cwd)
else
appendText(state, '/');
appendText(state, '> ');
refreshDisplay(state);
end;
{ ============================================================
drainOutput — called from per-instance LVGL timer
Pulls new content from the foreground process's stdout/stderr
into the scrollback, and cleans up when the process finishes.
============================================================ }
procedure drainOutput(state : PVTermState);
var
ctx : PProcessContext;
finished : boolean;
dirty : boolean;
begin
if state^.ForegroundPID = 0 then exit;
ctx := proc.mgr.findByID(state^.ForegroundPID);
dirty := false;
{ Drain any new stdout since last check }
if (state^.fg_stdout <> nil) and (state^.fg_stdout^.len > state^.last_drain) then begin
state^.fg_stdout^.buf[state^.fg_stdout^.len] := #0;
appendText(state, @state^.fg_stdout^.buf[state^.last_drain]);
state^.last_drain := state^.fg_stdout^.len;
dirty := true;
end;
{ Drain any new stderr }
if (state^.fg_stderr <> nil) and (state^.fg_stderr^.len > state^.last_drain_err) then begin
state^.fg_stderr^.buf[state^.fg_stderr^.len] := #0;
appendText(state, @state^.fg_stderr^.buf[state^.last_drain_err]);
state^.last_drain_err := state^.fg_stderr^.len;
dirty := true;
end;
{ Check if process is done }
finished := (ctx = nil) or (ctx^.State = psFinished) or (ctx^.State = psError);
if finished then begin
if state^.fg_stdout <> nil then begin
io.stdio.freeOutBuf(state^.fg_stdout);
state^.fg_stdout := nil;
end;
if state^.fg_stderr <> nil then begin
io.stdio.freeOutBuf(state^.fg_stderr);
state^.fg_stderr := nil;
end;
if state^.fg_stdin <> nil then begin
io.stdio.freeOutBuf(state^.fg_stdin);
state^.fg_stdin := nil;
end;
if state^.fg_params <> nil then begin
io.stdio.freeParams(state^.fg_params);
state^.fg_params := nil;
end;
state^.ForegroundPID := 0;
state^.last_drain := 0;
state^.last_drain_err := 0;
showPrompt(state);
exit; { showPrompt already refreshes }
end;
if dirty then
refreshDisplay(state);
end;
{ ============================================================
drainBackgroundJobs — check background jobs for output and
reap finished ones.
============================================================ }
procedure drainBackgroundJobs(state : PVTermState);
var
i : sint32;
slotPtr : ^uint32;
job : PBackgroundJob;
ctx : PProcessContext;
finished: boolean;
dirty : boolean;
ns : pchar;
begin
if state^.bg_jobs = nil then exit;
dirty := false;
for i := sint32(DL_Size(state^.bg_jobs)) - 1 downto 0 do begin
slotPtr := DL_Get(state^.bg_jobs, uint32(i));
if slotPtr = nil then continue;
job := PBackgroundJob(slotPtr^);
if job = nil then continue;
ctx := proc.mgr.findByID(job^.PID);
finished := (ctx = nil) or (ctx^.State = psFinished) or (ctx^.State = psError);
if finished then begin
{ Drain final output }
if (job^.StdOut <> nil) and (job^.StdOut^.len > job^.LastDrain) then begin
job^.StdOut^.buf[job^.StdOut^.len] := #0;
appendText(state, @job^.StdOut^.buf[job^.LastDrain]);
dirty := true;
end;
if (job^.StdErr <> nil) and (job^.StdErr^.len > job^.LastDrainE) then begin
job^.StdErr^.buf[job^.StdErr^.len] := #0;
appendText(state, @job^.StdErr^.buf[job^.LastDrainE]);
dirty := true;
end;
{ Notify user }
appendChar(state, #10);
appendText(state, '[');
ns := intToString(job^.JobNum);
appendText(state, ns);
kfree(void(ns));
appendText(state, '] Done ');
appendText(state, @job^.CmdName[0]);
appendChar(state, #10);
dirty := true;
{ Free I/O buffers }
if job^.StdOut <> nil then io.stdio.freeOutBuf(job^.StdOut);
if job^.StdErr <> nil then io.stdio.freeOutBuf(job^.StdErr);
kfree(void(job));
DL_Delete(state^.bg_jobs, uint32(i));
end;
end;
if dirty then
refreshDisplay(state);
end;
{ LVGL timer callback wrapper }
procedure drain_timer_cb(timer: Plv_timer); cdecl;
var
state : PVTermState;
begin
state := PVTermState(lv_timer_get_user_data(timer));
if state <> nil then begin
drainOutput(state);
drainBackgroundJobs(state);
end;
end;
{ ============================================================
killForeground — terminate the running foreground process
and clean up its I/O state (called by Ctrl+C handler).
============================================================ }
procedure killForeground(state : PVTermState);
var
ctx : PProcessContext;
begin
if state^.ForegroundPID = 0 then exit;
ctx := proc.mgr.findByID(state^.ForegroundPID);
if ctx <> nil then
proc.mgr.kill(state^.ForegroundPID);
{ Drain any final output before freeing buffers }
drainOutput(state);
{ If drainOutput didn’t already clean up (process may not be reaped yet) }
if state^.fg_stdout <> nil then begin io.stdio.freeOutBuf(state^.fg_stdout); state^.fg_stdout := nil; end;
if state^.fg_stderr <> nil then begin io.stdio.freeOutBuf(state^.fg_stderr); state^.fg_stderr := nil; end;
if state^.fg_stdin <> nil then begin io.stdio.freeOutBuf(state^.fg_stdin); state^.fg_stdin := nil; end;
if state^.fg_params <> nil then begin io.stdio.freeParams(state^.fg_params); state^.fg_params := nil; end;
state^.ForegroundPID := 0;
state^.last_drain := 0;
state^.last_drain_err := 0;
appendChar(state, #10);
appendText(state, '^C');
appendChar(state, #10);
showPrompt(state);
{ Clear any partial input line }
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
end;
{ ============================================================
Per-terminal filesystem builtins
============================================================ }
procedure builtin_cd(state : PVTermState; params : PParamList);
var
Path : pchar;
Temp1 : pchar;
Temp2 : pchar;
NewDir : pchar;
Validity : TIsPathValid;
i : uint32;
begin
if ParamCount(params) < 1 then exit;
{ Reconstruct the path argument (may contain spaces) }
for i := 0 to ParamCount(params) - 1 do begin
if i = 0 then begin
Temp1 := StringCopy(GetParam(i, params));
Path := StringCopy(Temp1);
kfree(void(Temp1));
end else begin
Temp1 := StringConcat(' ', GetParam(i, params));
Temp2 := StringConcat(Path, Temp1);
kfree(void(Temp1));
kfree(void(Path));
Path := Temp2;
end;
end;
{ Resolve relative to per-terminal cwd }
Validity := driver.storage.vfs.ChangeDirectoryFrom(Path, state^.cwd, NewDir);
case Validity of
pvDirectory: begin
kfree(void(state^.cwd));
state^.cwd := NewDir;
end;
pvFile: begin
appendText(state, '"');
appendText(state, Path);
appendText(state, '" is not a directory.');
appendChar(state, #10);
end;
pvInvalid: begin
appendText(state, '"');
appendText(state, Path);
appendText(state, '" is not a valid path.');
appendChar(state, #10);
end;
end;
kfree(void(Path));
end;
procedure builtin_ls(state : PVTermState; params : PParamList);
var
Map : PHashMap;
Item : PHashItem;
i : uint32;
begin
Map := driver.storage.vfs.GetDirectoryListingFrom('.', state^.cwd);
if Map <> nil then begin
for i := 0 to Map^.Size - 1 do begin
Item := Map^.Table[i];
while Item <> nil do begin
appendText(state, ' ');
appendText(state, Item^.Key);
appendChar(state, #10);
Item := Item^.Next;
end;
end;
end else begin
appendText(state, 'An internal error occured!');
appendChar(state, #10);
end;
end;
procedure builtin_pushd(state : PVTermState; params : PParamList);
var
wd : pchar;
Output : pchar;
begin
wd := StringCopy(state^.cwd);
STRLL_Add(state^.dir_stack, wd);
Output := StringConcat(wd, ' saved to stack.');
appendText(state, Output);
appendChar(state, #10);
kfree(void(Output));
end;
procedure builtin_popd(state : PVTermState; params : PParamList);
var
wd : pchar;
Output : pchar;
NewDir : pchar;
Validity : TIsPathValid;
begin
if STRLL_Size(state^.dir_stack) > 0 then begin
wd := STRLL_Get(state^.dir_stack, STRLL_Size(state^.dir_stack) - 1);
Validity := driver.storage.vfs.ChangeDirectoryFrom(wd, state^.cwd, NewDir);
if Validity = pvDirectory then begin
kfree(void(state^.cwd));
state^.cwd := NewDir;
Output := StringConcat(wd, ' popped from the stack.');
appendText(state, Output);
appendChar(state, #10);
kfree(void(Output));
end else begin
Output := StringConcat(wd, ' popped, but was invalid!');
appendText(state, Output);
appendChar(state, #10);
kfree(void(Output));
end;
STRLL_Delete(state^.dir_stack, STRLL_Size(state^.dir_stack) - 1);
end else begin
appendText(state, 'No working directory in the stack!');
appendChar(state, #10);
end;
end;
{ ============================================================
processCommand — execute the current input line
============================================================ }
procedure processCommand(state : PVTermState);
var
params : PParamList;
uppera : pchar;
cmd : PCommand;
ctx : PProcessContext;
is_bg : boolean;
pcount : uint32;
lastParam : pchar;
job : PBackgroundJob;
slotPtr : ^uint32;
absPath : pchar;
dispatchPID : uint32;
begin
{ Null-terminate input }
state^.line_buf[state^.line_len] := 0;
{ Echo the entered line }
appendChar(state, #10);
{ Push non-empty lines into history }
if state^.line_len > 0 then begin
memcpy(uint32(@state^.line_buf[0]),
uint32(@state^.hist[state^.hist_head][0]),
state^.line_len + 1);
state^.hist_head := (state^.hist_head + 1) mod HIST_SIZE;
if state^.hist_count < HIST_SIZE then inc(state^.hist_count);
end;
state^.hist_pos := -1;
if state^.line_len = 0 then begin
showPrompt(state);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
exit;
end;
{ Parse parameters }
params := io.stdio.getParams(state^.line_buf);
if (params <> nil) and (params^.Param <> nil) then begin
uppera := stringToUpper(params^.Param);
{ CLEAR handled locally }
if stringEquals(uppera, 'CLEAR') then begin
state^.text_len := 0;
state^.text_buf[0] := #0;
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
{ Filesystem builtins — per-terminal CWD }
if stringEquals(uppera, 'CD') then begin
builtin_cd(state, params);
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
if stringEquals(uppera, 'LS') then begin
builtin_ls(state, params);
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
if stringEquals(uppera, 'PUSHD') then begin
builtin_pushd(state, params);
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
if stringEquals(uppera, 'POPD') then begin
builtin_popd(state, params);
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
{ JOBS — list background jobs }
if stringEquals(uppera, 'JOBS') then begin
if (state^.bg_jobs <> nil) and (DL_Size(state^.bg_jobs) > 0) then begin
for pcount := 0 to DL_Size(state^.bg_jobs) - 1 do begin
slotPtr := DL_Get(state^.bg_jobs, pcount);
if (slotPtr <> nil) then begin
job := PBackgroundJob(slotPtr^);
if job <> nil then begin
appendText(state, '[');
lastParam := intToString(job^.JobNum);
appendText(state, lastParam);
kfree(void(lastParam));
appendText(state, '] ');
ctx := proc.mgr.findByID(job^.PID);
if ctx <> nil then begin
case ctx^.State of
psRunning: appendText(state, 'Running ');
psReady: appendText(state, 'Running ');
psSuspended: appendText(state, 'Stopped ');
psAwaiting: appendText(state, 'Waiting ');
else
appendText(state, 'Done ');
end;
end else
appendText(state, 'Done ');
appendText(state, @job^.CmdName[0]);
appendChar(state, #10);
end;
end;
end;
end else begin
appendText(state, 'No background jobs.');
appendChar(state, #10);
end;
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
exit;
end;
{ FG — bring a background job to foreground }
if stringEquals(uppera, 'FG') then begin
if (state^.bg_jobs <> nil) and (DL_Size(state^.bg_jobs) > 0) then begin
{ Use first bg job if no arg given }
slotPtr := DL_Get(state^.bg_jobs, 0);
if (slotPtr <> nil) then begin
job := PBackgroundJob(slotPtr^);
if job <> nil then begin
{ Transfer to foreground }
state^.ForegroundPID := job^.PID;
state^.fg_stdout := job^.StdOut;
state^.fg_stderr := job^.StdErr;
state^.fg_stdin := nil;
state^.last_drain := job^.LastDrain;
state^.last_drain_err := job^.LastDrainE;
state^.fg_params := nil;
appendText(state, @job^.CmdName[0]);
appendChar(state, #10);
{ Remove from bg_jobs without freeing I/O (now owned by fg) }
kfree(void(job));
DL_Delete(state^.bg_jobs, 0);
end;
end;
end else begin
appendText(state, 'No background jobs.');
appendChar(state, #10);
showPrompt(state);
end;
kfree(void(uppera));
io.stdio.freeParams(params);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
exit;
end;
{ Check for trailing & (background launch) }
is_bg := false;
pcount := io.stdio.paramCount(params);
if pcount > 0 then begin
lastParam := io.stdio.getParam(pcount - 1, params);
if (lastParam <> nil) and (lastParam[0] = '&') and (lastParam[1] = #0) then
is_bg := true;
end;
cmd := io.stdio.findCommand(uppera);
kfree(void(uppera));
if cmd <> nil then begin
if is_bg then begin
{ --- Background launch --- }
job := PBackgroundJob(kalloc(SizeOf(TBackgroundJob)));
if job <> nil then begin
memset(uint32(job), 0, SizeOf(TBackgroundJob));
job^.StdOut := io.stdio.createOutBuf(1024);
job^.StdErr := io.stdio.createOutBuf(1024);
job^.LastDrain := 0;
job^.LastDrainE := 0;
inc(state^.bg_next_job);
job^.JobNum := state^.bg_next_job;
{ Copy command name }
pcount := stringSize(params^.Param);
if pcount > 31 then pcount := 31;
memcpy(uint32(params^.Param), uint32(@job^.CmdName[0]), pcount);
job^.CmdName[pcount] := #0;
{ Launch but don't set ForegroundPID }
ctx := proc.mgr.runCommand(
params^.Param, cmd^.method, params,
nil, job^.StdOut, job^.StdErr
);
if ctx <> nil then begin
job^.PID := ctx^.ProcessID;
if state^.bg_jobs = nil then
state^.bg_jobs := DL_New(SizeOf(uint32));
slotPtr := DL_Add(state^.bg_jobs);
slotPtr^ := uint32(job);
appendText(state, '[');
lastParam := intToString(job^.JobNum);
appendText(state, lastParam);
kfree(void(lastParam));
appendText(state, '] ');
lastParam := intToString(job^.PID);
appendText(state, lastParam);
kfree(void(lastParam));
appendChar(state, #10);
end else begin
appendText(state, 'Error: failed to create process.');
appendChar(state, #10);
io.stdio.freeOutBuf(job^.StdOut);
io.stdio.freeOutBuf(job^.StdErr);
kfree(void(job));
end;
end;
io.stdio.freeParams(params);
showPrompt(state);
end else begin
{ --- Foreground launch --- }
state^.fg_stdout := io.stdio.createOutBuf(1024);
state^.fg_stderr := io.stdio.createOutBuf(1024);
state^.fg_stdin := io.stdio.createOutBuf(0);
state^.last_drain := 0;
state^.last_drain_err := 0;
state^.fg_params := params;
ctx := proc.mgr.runCommand(
params^.Param, cmd^.method, params,
state^.fg_stdin, state^.fg_stdout, state^.fg_stderr
);
if ctx <> nil then begin
state^.ForegroundPID := ctx^.ProcessID;
end else begin
appendText(state, 'Error: failed to create process.');
appendChar(state, #10);
io.stdio.freeOutBuf(state^.fg_stdout); state^.fg_stdout := nil;
io.stdio.freeOutBuf(state^.fg_stderr); state^.fg_stderr := nil;
io.stdio.freeOutBuf(state^.fg_stdin); state^.fg_stdin := nil;
io.stdio.freeParams(params);
state^.fg_params := nil;
state^.ForegroundPID := 0;
showPrompt(state);
end;
end;
end else begin
{ No built-in command found — try file dispatch }
absPath := driver.storage.vfs.MakeAbsolutePathFrom(params^.Param, state^.cwd);
if absPath <> nil then begin
if is_bg then begin
{ Background file dispatch }
job := PBackgroundJob(kalloc(SizeOf(TBackgroundJob)));
if job <> nil then begin
memset(uint32(job), 0, SizeOf(TBackgroundJob));
job^.StdOut := io.stdio.createOutBuf(1024);
job^.StdErr := io.stdio.createOutBuf(1024);
job^.LastDrain := 0;
job^.LastDrainE := 0;
inc(state^.bg_next_job);
job^.JobNum := state^.bg_next_job;
pcount := stringSize(params^.Param);
if pcount > 31 then pcount := 31;
memcpy(uint32(params^.Param), uint32(@job^.CmdName[0]), pcount);
job^.CmdName[pcount] := #0;
dispatchPID := driver.storage.filedispatch.dispatch(absPath, params,
nil, job^.StdOut, job^.StdErr);
if dispatchPID > 0 then begin
job^.PID := dispatchPID;
if state^.bg_jobs = nil then
state^.bg_jobs := DL_New(SizeOf(uint32));
slotPtr := DL_Add(state^.bg_jobs);
slotPtr^ := uint32(job);
appendText(state, '[');
lastParam := intToString(job^.JobNum);
appendText(state, lastParam);
kfree(void(lastParam));
appendText(state, '] ');
lastParam := intToString(job^.PID);
appendText(state, lastParam);
kfree(void(lastParam));
appendChar(state, #10);
end else begin
appendText(state, 'Unknown command. Type HELP for a list.');
appendChar(state, #10);
io.stdio.freeOutBuf(job^.StdOut);
io.stdio.freeOutBuf(job^.StdErr);
kfree(void(job));
end;
end;
kfree(void(absPath));
io.stdio.freeParams(params);
showPrompt(state);
end else begin
{ Foreground file dispatch }
state^.fg_stdout := io.stdio.createOutBuf(1024);
state^.fg_stderr := io.stdio.createOutBuf(1024);
state^.fg_stdin := io.stdio.createOutBuf(0);
state^.last_drain := 0;
state^.last_drain_err := 0;
state^.fg_params := params;
dispatchPID := driver.storage.filedispatch.dispatch(absPath, params,
state^.fg_stdin, state^.fg_stdout, state^.fg_stderr);
kfree(void(absPath));
if dispatchPID > 0 then begin
state^.ForegroundPID := dispatchPID;
end else begin
appendText(state, 'Unknown command. Type HELP for a list.');
appendChar(state, #10);
io.stdio.freeOutBuf(state^.fg_stdout); state^.fg_stdout := nil;
io.stdio.freeOutBuf(state^.fg_stderr); state^.fg_stderr := nil;
io.stdio.freeOutBuf(state^.fg_stdin); state^.fg_stdin := nil;
io.stdio.freeParams(params);
state^.fg_params := nil;
state^.ForegroundPID := 0;
showPrompt(state);
end;
end;
end else begin
appendText(state, 'Unknown command. Type HELP for a list.');
appendChar(state, #10);
io.stdio.freeParams(params);
showPrompt(state);
end;
end;
end else begin
if params <> nil then io.stdio.freeParams(params);
showPrompt(state);
end;
{ Reset input line }
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
end;
{ ============================================================
LVGL event callbacks — retrieve state from user_data
============================================================ }
procedure content_click_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PVTermState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PVTermState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.content = nil then exit;
if not state^.focused then begin
state^.focused := true;
lv_group_add_obj(lvgl_get_kb_group, state^.content);
lv_group_focus_obj(state^.content);
end;
end;
procedure content_key_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
key : uint32;
state : PVTermState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_KEY then exit;
state := PVTermState(lv_event_get_user_data(e));
if state = nil then exit;
key := lv_event_get_key(e);
{ Ctrl+C — kill foreground process regardless of state }
if key = LV_KEY_CTRLC then begin
if state^.ForegroundPID <> 0 then
killForeground(state)
else begin
{ No foreground process — just print ^C and new prompt }
appendChar(state, #10);
appendText(state, '^C');
appendChar(state, #10);
state^.line_len := 0;
memset(uint32(@state^.line_buf[0]), 0, MAX_LINE);
showPrompt(state);
end;
exit;
end;
{ Block input while a foreground process is running }
if state^.ForegroundPID <> 0 then exit;
if key = LV_KEY_ENTER then begin
processCommand(state);
exit;
end;
{ Up arrow — browse history backward (older) }
if key = LV_KEY_UP then begin
if state^.hist_count > 0 then begin
if state^.hist_pos = -1 then begin
memcpy(uint32(@state^.line_buf[0]),
uint32(@state^.saved_line[0]),
state^.line_len + 1);
state^.saved_len := state^.line_len;
state^.hist_pos := 0;
end else if uint32(state^.hist_pos) < state^.hist_count - 1 then
inc(state^.hist_pos)
else
exit;
if state^.text_len >= state^.line_len then
state^.text_len := state^.text_len - state^.line_len;
state^.text_buf[state^.text_len] := #0;
state^.line_len := stringSize(
@state^.hist[(sint32(state^.hist_head) - 1 - state^.hist_pos + HIST_SIZE * 2) mod HIST_SIZE][0]);
memcpy(uint32(@state^.hist[(sint32(state^.hist_head) - 1 - state^.hist_pos + HIST_SIZE * 2) mod HIST_SIZE][0]),
uint32(@state^.line_buf[0]),
state^.line_len + 1);
appendText(state, @state^.line_buf[0]);
refreshDisplay(state);
end;
exit;
end;
{ Down arrow — browse history forward (newer) }
if key = LV_KEY_DOWN then begin
if state^.hist_pos >= 0 then begin
if state^.text_len >= state^.line_len then
state^.text_len := state^.text_len - state^.line_len;
state^.text_buf[state^.text_len] := #0;
dec(state^.hist_pos);
if state^.hist_pos >= 0 then begin
state^.line_len := stringSize(
@state^.hist[(sint32(state^.hist_head) - 1 - state^.hist_pos + HIST_SIZE * 2) mod HIST_SIZE][0]);
memcpy(uint32(@state^.hist[(sint32(state^.hist_head) - 1 - state^.hist_pos + HIST_SIZE * 2) mod HIST_SIZE][0]),
uint32(@state^.line_buf[0]),
state^.line_len + 1);
end else begin
memcpy(uint32(@state^.saved_line[0]),
uint32(@state^.line_buf[0]),
state^.saved_len + 1);
state^.line_len := state^.saved_len;
end;
appendText(state, @state^.line_buf[0]);
refreshDisplay(state);
end;
exit;
end;
if key = LV_KEY_BACKSPACE then begin
if state^.line_len > 0 then begin
dec(state^.line_len);
state^.line_buf[state^.line_len] := 0;
if state^.text_len > 0 then begin
dec(state^.text_len);
state^.text_buf[state^.text_len] := #0;
end;
refreshDisplay(state);
end;
exit;
end;
{ Printable characters (32..126) }
if (key >= 32) and (key <= 126) then begin
if state^.line_len < MAX_LINE - 1 then begin
state^.line_buf[state^.line_len] := byte(key);
inc(state^.line_len);
state^.line_buf[state^.line_len] := 0;
appendChar(state, char(key));
refreshDisplay(state);
end;
end;
end;
procedure content_defocus_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PVTermState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_DEFOCUSED then exit;
state := PVTermState(lv_event_get_user_data(e));
if state <> nil then
state^.focused := false;
end;
{ ============================================================
onClose — clean up instance
============================================================ }
procedure onClose(wid: uint32);
var
cnt : Plv_obj;
state : PVTermState;
slotPtr : ^uint32;
bgJob : PBackgroundJob;
scr_w : sint32;
begin
cnt := driver.video.windows.getWindowContent(wid);
state := nil;
if cnt <> nil then
state := PVTermState(lv_obj_get_user_data(cnt));
if state <> nil then begin
{ Kill the terminal process }
if state^.TerminalPID <> 0 then begin
proc.mgr.kill(state^.TerminalPID);
state^.TerminalPID := 0;
end;
{ Kill foreground process if running }
if state^.ForegroundPID <> 0 then begin
proc.mgr.kill(state^.ForegroundPID);
state^.ForegroundPID := 0;
end;
{ Free any remaining I/O buffers }
if state^.fg_stdout <> nil then begin io.stdio.freeOutBuf(state^.fg_stdout); state^.fg_stdout := nil; end;
if state^.fg_stderr <> nil then begin io.stdio.freeOutBuf(state^.fg_stderr); state^.fg_stderr := nil; end;
if state^.fg_stdin <> nil then begin io.stdio.freeOutBuf(state^.fg_stdin); state^.fg_stdin := nil; end;
if state^.fg_params <> nil then begin io.stdio.freeParams(state^.fg_params); state^.fg_params := nil; end;
{ Kill and free background jobs }
if state^.bg_jobs <> nil then begin
for scr_w := sint32(DL_Size(state^.bg_jobs)) - 1 downto 0 do begin
slotPtr := DL_Get(state^.bg_jobs, uint32(scr_w));
if slotPtr <> nil then begin
bgJob := PBackgroundJob(slotPtr^);
if bgJob <> nil then begin
proc.mgr.kill(bgJob^.PID);
if bgJob^.StdOut <> nil then io.stdio.freeOutBuf(bgJob^.StdOut);
if bgJob^.StdErr <> nil then io.stdio.freeOutBuf(bgJob^.StdErr);
kfree(void(bgJob));
end;
end;
end;
DL_Free(state^.bg_jobs);
state^.bg_jobs := nil;
end;
{ Remove focus }
if state^.focused and (state^.content <> nil) then
lv_group_remove_obj(state^.content);
{ Delete the drain timer }
if state^.drain_timer <> nil then begin
lv_timer_delete(state^.drain_timer);
state^.drain_timer := nil;
end;
{ Free per-terminal CWD and dir stack }
if state^.cwd <> nil then begin
kfree(void(state^.cwd));
state^.cwd := nil;
end;
if state^.dir_stack <> nil then begin
STRLL_Free(state^.dir_stack);
state^.dir_stack := nil;
end;
{ Free instance state }
kfree(void(state));
end;
driver.video.windows.destroyWindow(wid);
end;
{ ============================================================
launch — create a new terminal instance
============================================================ }
procedure launch;
var
state : PVTermState;
ctx : PProcessContext;
scr_w, scr_h : sint32;
wx, wy : sint32;
begin
{ Allocate a new instance }
state := PVTermState(kalloc(SizeOf(TVTermState)));
if state = nil then exit;
memset(uint32(state), 0, SizeOf(TVTermState));
state^.hist_pos := -1;
state^.cwd := stringCopy('/');
state^.dir_stack := STRLL_New;
scr_w := sint32(driver.video.frontBufferWidth);
scr_h := sint32(driver.video.frontBufferHeight);
wx := (scr_w - TERM_W) div 2;
wy := (scr_h - TERM_H) div 2 - 30;
state^.win_id := driver.video.windows.createWindow(
'Terminal',
wx, wy, TERM_W, TERM_H,
@onClose,
nil
);
if state^.win_id = 0 then begin
kfree(void(state));
exit;
end;
state^.content := driver.video.windows.getWindowContent(state^.win_id);
if state^.content = nil then begin
driver.video.windows.destroyWindow(state^.win_id);
kfree(void(state));
exit;
end;
{ Store state on the content object for retrieval in onClose }
lv_obj_set_user_data(state^.content, state);
{ Style: black background, terminal feel }
lv_obj_set_style_bg_color(state^.content, lv_color_make(0, 0, 0), 0);
lv_obj_set_style_bg_opa(state^.content, LV_OPA_COVER, 0);
lv_obj_set_style_pad_left(state^.content, 8, 0);
lv_obj_set_style_pad_right(state^.content, 8, 0);
lv_obj_set_style_pad_top(state^.content, 6, 0);
lv_obj_set_style_pad_bottom(state^.content, 6, 0);
lv_obj_add_flag(state^.content, LV_OBJ_FLAG_CLICKABLE);
{ Create text label }
state^.text_label := lv_label_create(state^.content);
lv_obj_set_width(state^.text_label, TERM_W - 16);
lv_obj_set_style_text_color(state^.text_label, lv_color_make(200, 255, 200), 0);
lv_obj_set_style_text_font(state^.text_label, @hack_14, 0);
lv_label_set_long_mode(state^.text_label, LV_LABEL_LONG_WRAP);
lv_label_set_text(state^.text_label, '');
{ Register event handlers with state as user_data }
lv_obj_add_event_cb(state^.content, @content_click_cb, LV_EVENT_CLICKED, state);
lv_obj_add_event_cb(state^.content, @content_key_cb, LV_EVENT_KEY, state);
lv_obj_add_event_cb(state^.content, @content_defocus_cb, LV_EVENT_DEFOCUSED, state);
{ Create per-instance drain timer }
state^.drain_timer := lv_timer_create(@drain_timer_cb, DRAIN_PERIOD, state);
{ Create terminal process so it appears in PS }
ctx := proc.mgr.create('Terminal', @terminal_entry, void(state), 1);
if ctx <> nil then begin
state^.TerminalPID := ctx^.ProcessID;
driver.video.windows.setWindowOwner(state^.win_id, ctx^.ProcessID);
end else
state^.TerminalPID := 0;
{ Welcome message }
appendText(state, 'Asuro Terminal');
appendChar(state, #10);
appendText(state, 'Type HELP for a list of commands.');
appendChar(state, #10);
appendChar(state, #10);
showPrompt(state);
end;
{ ============================================================
init — register Terminal in the driver.video.desktop program launcher
============================================================ }
procedure init;
begin
debug.tracer.push_trace('app.vterminal.init');
driver.video.desktop.registerProgram('Terminal', @launch);
debug.tracer.pop_trace;
end;
end.