- 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
1108 lines
41 KiB
ObjectPascal
1108 lines
41 KiB
ObjectPascal
{
|
||
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.
|