Files
Asuro/src/app/app.edit.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

650 lines
19 KiB
ObjectPascal

{
Prog->app.edit - Simple text editor
@author(Aaron Hance <[email protected]>)
Controls:
Arrow keys - Move cursor
Backspace - Delete character before cursor
Enter - New line
Tab - Insert 4 spaces
ESC - Close editor
}
unit app.edit;
interface
uses
io.syslog,
driver.hid.keyboard,
memory.heap,
driver.storage.types,
core.strings,
io.stdio,
debug.tracer,
core.util, arch.x86.util,
driver.storage.vfs;
procedure init();
implementation
const
ED_WIDTH = 60;
ED_HEIGHT = 20;
MAX_LINES = 256;
MAX_COLS = 120;
TEXT_ROWS = 19; { ED_HEIGHT - 1, last row is status bar }
TAB_SIZE = 4;
var
Handle : uint32 = 0;
Colors : uint32;
ColorsHL : uint32;
ColorsST : uint32;
Lines : array[0..255] of pchar;
LineLen : array[0..255] of uint32;
NumLines : uint32 = 0;
CurX : uint32 = 0;
CurY : uint32 = 0;
ScrollY : uint32 = 0;
Dirty : boolean = false;
Active : boolean = false;
FilePath : pchar = nil;
{ ------------------------------------------------------------------ }
{ Buffer management }
{ ------------------------------------------------------------------ }
procedure AllocLine(idx : uint32);
begin
Lines[idx] := pchar(kalloc(MAX_COLS + 1));
memset(uint32(Lines[idx]), 0, MAX_COLS + 1);
LineLen[idx] := 0;
end;
procedure FreeLine(idx : uint32);
begin
if Lines[idx] <> nil then begin
kfree(puint32(Lines[idx]));
Lines[idx] := nil;
end;
LineLen[idx] := 0;
end;
procedure ResetBuffer;
var
i : uint32;
begin
for i := 0 to MAX_LINES - 1 do begin
if Lines[i] <> nil then FreeLine(i);
end;
AllocLine(0);
NumLines := 1;
CurX := 0;
CurY := 0;
ScrollY := 0;
Dirty := false;
end;
procedure EnsureVisible;
begin
if CurY < ScrollY then
ScrollY := CurY;
if CurY >= ScrollY + TEXT_ROWS then
ScrollY := CurY - TEXT_ROWS + 1;
end;
procedure SaveFile;
var
fError : driver.storage.types.TError;
fHandle : driver.storage.vfs.TFileHandle;
totalLen : uint32;
buf : pchar;
pos : uint32;
i : uint32;
begin
debug.tracer.push_trace('edit.SaveFile.enter');
if FilePath = nil then begin
io.syslog.writestringln('No file path. Use: EDIT <path>');
exit;
end;
debug.tracer.push_trace('edit.SaveFile.calcSize');
{ Calculate total size: sum of line lengths + newlines }
totalLen := 0;
for i := 0 to NumLines - 1 do begin
totalLen := totalLen + LineLen[i] + 1; { +1 for newline }
end;
if totalLen > 0 then dec(totalLen); { No trailing newline }
buf := pchar(kalloc(totalLen + 1));
memset(uint32(buf), 0, totalLen + 1);
pos := 0;
for i := 0 to NumLines - 1 do begin
if LineLen[i] > 0 then begin
memcpy(uint32(Lines[i]), uint32(@buf[pos]), LineLen[i]);
pos := pos + LineLen[i];
end;
if i < NumLines - 1 then begin
buf[pos] := char(10); { LF newline }
pos := pos + 1;
end;
end;
{ Open for writing }
debug.tracer.push_trace('edit.SaveFile.openRW');
fHandle := driver.storage.vfs.OpenFile(FilePath, omReadWrite, @fError);
if fHandle <> 0 then begin
debug.tracer.push_trace('edit.SaveFile.writeFile');
driver.storage.vfs.WriteFile(fHandle, 0, puint8(buf), totalLen);
debug.tracer.push_trace('edit.SaveFile.closeFile');
driver.storage.vfs.CloseFile(fHandle);
Dirty := false;
io.syslog.writestringln('File saved.');
end else begin
{ Try write-only for new file }
debug.tracer.push_trace('edit.SaveFile.openWO');
fHandle := driver.storage.vfs.OpenFile(FilePath, omCreate, @fError);
if fHandle <> 0 then begin
debug.tracer.push_trace('edit.SaveFile.writeFileNew');
driver.storage.vfs.WriteFile(fHandle, 0, puint8(buf), totalLen);
debug.tracer.push_trace('edit.SaveFile.closeFileNew');
driver.storage.vfs.CloseFile(fHandle);
Dirty := false;
io.syslog.writestringln('File saved.');
end else begin
io.syslog.writestringln('Error saving file.');
end;
end;
kfree(puint32(buf));
debug.tracer.push_trace('edit.SaveFile.exit');
end;
procedure LoadFile;
var
fError : driver.storage.types.TError;
fHandle : driver.storage.vfs.TFileHandle;
fSize : uint32;
buf : pchar;
bytesRead: uint32;
i : uint32;
lineStart: uint32;
ch : char;
begin
debug.tracer.push_trace('edit.LoadFile.enter');
if FilePath = nil then exit;
debug.tracer.push_trace('edit.LoadFile.openFile');
fHandle := driver.storage.vfs.OpenFile(FilePath, omRead, @fError);
debug.tracer.push_trace('edit.LoadFile.openFile.done');
if (fHandle = 0) or (fError <> eNone) then begin
io.syslog.writestringln('New file.');
exit;
end;
debug.tracer.push_trace('edit.LoadFile.readData');
{ Get size from the open file entry (dataSize is already loaded) }
fSize := 0;
buf := pchar(kalloc(32768)); { 32KB max }
memset(uint32(buf), 0, 32768);
debug.tracer.push_trace('edit.LoadFile.readFile');
bytesRead := driver.storage.vfs.ReadFile(fHandle, 0, puint8(buf), 32768);
debug.tracer.push_trace('edit.LoadFile.closeFile');
driver.storage.vfs.CloseFile(fHandle);
if bytesRead = 0 then begin
kfree(puint32(buf));
io.syslog.writestringln('Empty file.');
exit;
end;
{ Parse buffer into lines, split on LF (10) or CR (13) }
ResetBuffer;
NumLines := 0;
AllocLine(0);
NumLines := 1;
lineStart := 0;
for i := 0 to bytesRead - 1 do begin
ch := buf[i];
if ch = char(13) then continue; { Skip CR }
if ch = char(10) then begin
{ Start a new line }
if NumLines < MAX_LINES then begin
AllocLine(NumLines);
NumLines := NumLines + 1;
end;
end else if ch = char(0) then begin
break; { End of data }
end else begin
{ Append char to current line }
if LineLen[NumLines - 1] < MAX_COLS - 1 then begin
Lines[NumLines - 1][LineLen[NumLines - 1]] := ch;
LineLen[NumLines - 1] := LineLen[NumLines - 1] + 1;
Lines[NumLines - 1][LineLen[NumLines - 1]] := char(0);
end;
end;
end;
kfree(puint32(buf));
CurX := 0;
CurY := 0;
ScrollY := 0;
Dirty := false;
io.syslog.writestringln('File loaded.');
debug.tracer.push_trace('edit.LoadFile.exit');
end;
{ ------------------------------------------------------------------ }
{ Drawing }
{ ------------------------------------------------------------------ }
procedure Draw;
var
row, col : uint32;
lineIdx : uint32;
ch : char;
attr : uint32;
posStr : pchar;
i : uint32;
stCol : uint32;
begin
if Handle = 0 then exit;
if not Active then exit;
{ stub: clearWNDEx };
{ Draw text area }
for row := 0 to TEXT_ROWS - 1 do begin
lineIdx := ScrollY + row;
{ stub: setCursorPosWND };
if lineIdx < NumLines then begin
for col := 0 to ED_WIDTH - 1 do begin
if col < LineLen[lineIdx] then
ch := Lines[lineIdx][col]
else
ch := ' ';
if (lineIdx = CurY) and (col = CurX) then
attr := ColorsHL
else
attr := Colors;
{ stub: writecharexWND };
end;
end else begin
{ Past end of file }
{ stub: writecharexWND };
end;
end;
{ Draw status bar — limit to ED_WIDTH-1 chars so the cursor
never advances past WND_W on the last row, which would
trigger _newlineWND and scroll the entire buffer up. }
{ stub: setCursorPosWND };
stCol := 0;
{ ' ESC:Exit ^S:Save' }
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
{ Dirty flag }
if Dirty then begin
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
end else begin
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
end;
{ File path (truncated if needed) }
if (FilePath <> nil) and (stCol < ED_WIDTH - 1) then begin
{ stub: writecharexWND }; inc(stCol);
i := 0;
while (stCol < ED_WIDTH - 1) and (FilePath[i] <> char(0)) do begin
{ stub: writecharexWND };
inc(stCol);
inc(i);
end;
end;
{ Position info }
posStr := intToString(CurY + 1);
if (posStr <> nil) and (stCol < ED_WIDTH - 1) then begin
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
i := 0;
while (stCol < ED_WIDTH - 1) and (posStr[i] <> char(0)) do begin
{ stub: writecharexWND };
inc(stCol);
inc(i);
end;
end;
if posStr <> nil then kfree(puint32(posStr));
posStr := intToString(CurX + 1);
if (posStr <> nil) and (stCol < ED_WIDTH - 1) then begin
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
if stCol < ED_WIDTH - 1 then begin { stub: writecharexWND }; inc(stCol); end;
i := 0;
while (stCol < ED_WIDTH - 1) and (posStr[i] <> char(0)) do begin
{ stub: writecharexWND };
inc(stCol);
inc(i);
end;
end;
if posStr <> nil then kfree(puint32(posStr));
{ Pad remainder — stop 1 short of ED_WIDTH }
while stCol < ED_WIDTH - 1 do begin
{ stub: writecharexWND };
inc(stCol);
end;
end;
{ ------------------------------------------------------------------ }
{ Text editing operations }
{ ------------------------------------------------------------------ }
procedure InsertChar(ch : char);
var
i : uint32;
begin
if LineLen[CurY] >= MAX_COLS - 1 then exit;
{ Shift characters right from end to CurX }
i := LineLen[CurY];
while i > CurX do begin
Lines[CurY][i] := Lines[CurY][i - 1];
dec(i);
end;
Lines[CurY][CurX] := ch;
LineLen[CurY] := LineLen[CurY] + 1;
Lines[CurY][LineLen[CurY]] := char(0);
CurX := CurX + 1;
Dirty := true;
end;
procedure InsertNewLine;
var
tailLen : uint32;
i : uint32;
begin
if NumLines >= MAX_LINES - 1 then exit;
{ Shift all lines below CurY down by one }
i := NumLines;
while i > CurY + 1 do begin
Lines[i] := Lines[i - 1];
LineLen[i] := LineLen[i - 1];
dec(i);
end;
{ Allocate the new line }
AllocLine(CurY + 1);
{ Copy text after cursor to new line }
tailLen := LineLen[CurY] - CurX;
if tailLen > 0 then begin
memcpy(uint32(@Lines[CurY][CurX]), uint32(Lines[CurY + 1]), tailLen);
LineLen[CurY + 1] := tailLen;
Lines[CurY + 1][tailLen] := char(0);
end;
{ Truncate current line at cursor }
LineLen[CurY] := CurX;
Lines[CurY][CurX] := char(0);
NumLines := NumLines + 1;
CurX := 0;
CurY := CurY + 1;
Dirty := true;
EnsureVisible;
end;
procedure DeleteBack;
var
i : uint32;
prevLen : uint32;
begin
if (CurX = 0) and (CurY = 0) then exit;
if CurX > 0 then begin
{ Delete character before cursor on this line }
i := CurX - 1;
while i < LineLen[CurY] - 1 do begin
Lines[CurY][i] := Lines[CurY][i + 1];
i := i + 1;
end;
LineLen[CurY] := LineLen[CurY] - 1;
Lines[CurY][LineLen[CurY]] := char(0);
CurX := CurX - 1;
Dirty := true;
end else begin
{ At column 0: merge this line into previous }
prevLen := LineLen[CurY - 1];
if LineLen[CurY] > 0 then begin
if prevLen + LineLen[CurY] < MAX_COLS then begin
memcpy(uint32(Lines[CurY]), uint32(@Lines[CurY - 1][prevLen]), LineLen[CurY]);
LineLen[CurY - 1] := prevLen + LineLen[CurY];
Lines[CurY - 1][LineLen[CurY - 1]] := char(0);
end;
end;
{ Free merged line }
FreeLine(CurY);
{ Shift remaining lines up }
i := CurY;
while i < NumLines - 1 do begin
Lines[i] := Lines[i + 1];
LineLen[i] := LineLen[i + 1];
i := i + 1;
end;
Lines[NumLines - 1] := nil;
LineLen[NumLines - 1] := 0;
NumLines := NumLines - 1;
CurY := CurY - 1;
CurX := prevLen;
Dirty := true;
EnsureVisible;
end;
end;
{ ------------------------------------------------------------------ }
{ Event handlers }
{ ------------------------------------------------------------------ }
procedure OnKeyPressed(info : TKeyInfo);
var
i : uint32;
savedHandle : uint32;
begin
if not Active then exit;
if Handle = 0 then exit;
{ Ctrl+S: save file }
if info.CTRL_DOWN and (info.key_code = uint8('s')) then begin
SaveFile;
exit;
end;
{ Escape: close editor }
if info.key_code = $1B then begin
Active := false;
ResetBuffer;
if FilePath <> nil then begin
kfree(void(FilePath));
FilePath := nil;
end;
savedHandle := Handle;
Handle := 0;
{ TODO: closeWindow };
exit;
end;
{ Tab: insert spaces }
if info.key_code = $09 then begin
for i := 0 to TAB_SIZE - 1 do begin
InsertChar(' ');
end;
exit;
end;
{ Backspace }
if info.key_code = $08 then begin
DeleteBack;
exit;
end;
{ Enter }
if info.key_code = $0D then begin
InsertNewLine;
exit;
end;
{ Arrow Up }
if info.key_code = $10 then begin
if CurY > 0 then begin
CurY := CurY - 1;
if CurX > LineLen[CurY] then CurX := LineLen[CurY];
EnsureVisible;
end;
exit;
end;
{ Arrow Down }
if info.key_code = $12 then begin
if CurY < NumLines - 1 then begin
CurY := CurY + 1;
if CurX > LineLen[CurY] then CurX := LineLen[CurY];
EnsureVisible;
end;
exit;
end;
{ Arrow Left }
if info.key_code = $13 then begin
if CurX > 0 then begin
CurX := CurX - 1;
end else if CurY > 0 then begin
CurY := CurY - 1;
CurX := LineLen[CurY];
EnsureVisible;
end;
exit;
end;
{ Arrow Right }
if info.key_code = $14 then begin
if CurX < LineLen[CurY] then begin
CurX := CurX + 1;
end else if CurY < NumLines - 1 then begin
CurY := CurY + 1;
CurX := 0;
EnsureVisible;
end;
exit;
end;
{ Printable characters (space through tilde) }
if (info.key_code >= 32) and (info.key_code <= 126) then begin
InsertChar(char(info.key_code));
end;
end;
procedure OnClose;
begin
Active := false;
Handle := 0;
ResetBuffer;
end;
{ ------------------------------------------------------------------ }
{ Command entry point }
{ ------------------------------------------------------------------ }
procedure Run(Params : PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf);
begin
debug.tracer.push_trace('edit.run');
if Handle <> 0 then begin
io.syslog.writestringln('Editor is already open.');
exit;
end;
{ Store file path if provided — convert to absolute path now so it
reflects the working directory at the time the command is issued }
if FilePath <> nil then begin
kfree(void(FilePath));
FilePath := nil;
end;
if paramCount(Params) > 0 then begin
FilePath := driver.storage.vfs.MakeAbsolutePath(getParam(0, Params));
end;
{ Initialise a fresh buffer }
ResetBuffer; //todo also ai is freeing random memory in here
{ If a file path was given, try to load it }
if FilePath <> nil then begin
LoadFile;
end;
{ Open editor window }
Handle := 0 { TODO: newWindow };
{ TODO: registerEventHandler }
{ TODO: registerEventHandler }
{ TODO: registerEventHandler }
Active := true;
io.syslog.writestringln('Editor opened. ^S to save, ESC to close.');
end;
{ ------------------------------------------------------------------ }
{ Initialisation }
{ ------------------------------------------------------------------ }
procedure init;
begin
debug.tracer.push_trace('edit.init');
Colors := $FFFF0000; { white on black }
ColorsHL := $0000FFFF; { black on white - cursor }
ColorsST := $000007E0; { black on green - status bar }
io.stdio.registerCommand('EDIT', @Run, 'Simple text editor');
end;
end.