| git.druid.rocks | index | druid520 | kaboom | src/ | user/ | ed.nsc |
src/user/ed.nsc
include "syscalls.nsh";
include "lib.nsh";
include "string.nsh";
include "mem.nsh";
include "stdio.nsh";
/*
* ed: tape-kernel's own line editor (src/usr/editor.c there), ported
* -- not reinvented -- to kaboom/nsc. the real logic is unchanged:
* a fixed 32-line x 80-column buffer, load a file by splitting on
* '\n', a command loop where 'e' re-enters every line fresh (empty
* line stops early) and 'w' joins the buffer back with '\n' and
* saves it, 'q' quits. same MAX_LINES/MAX_LINE_LEN as the original
* (32/80). two small, real additions past the original: 'a' appends
* lines after whatever's already there instead of always starting
* over, and the display now shows each line's number -- both asked
* for directly, not part of the original port.
*
* one honest adaptation, not a logic change: tape-kernel's version
* repaints the whole screen at fixed (row, col) positions every
* loop iteration (editor_display's prt/cob calls) and reads single
* keys with a raw, no-echo gtchr(). kaboom's vga driver only exposes
* sequential, scrolling text output plus a hardware clear (vga_clear,
* syscall 16) -- there is no absolute-position write or a real
* hardware cursor api yet, so this prints the same information
* (filename, hotkeys, current lines) sequentially after a clear
* instead of redrawing it in place. sys_read(fd, buf, 1) already
* blocks for exactly one keystroke (kaboom's keyboard driver echoes
* at the driver level, same as every other read here), which is
* exactly what gtchr() gave the original.
*
* kaboom has no arrays-of-arrays (array elements must be a scalar
* type), so the 32x80 line buffer tape-kernel declared as
* char[MAX_LINES][MAX_LINE_LEN+1] is one flat i8[MAX_LINES*81] here,
* indexed as lines[line*81 + col] -- same data, same layout, just
* addressed by hand the way every other flat table in this kernel
* already is (kfs_dir_list's names_out, for one).
*
* every working buffer of any real size (the line table, the load
* buffer, the save buffer) is sys_alloc'd now, not a stack local or
* a fixed global array: kaboom built a real arena allocator (see
* alloc.nsc) specifically so nothing would need to hand-declare
* fixed-size buffers for "give me some memory, once, at runtime" --
* an earlier version of this file did exactly that anyway, before
* the allocator was actually wired up to userspace.
*/
/* loads filename into lines, returning 1 if ed can go on to edit it
* or 0 if it must refuse (the caller exits without ever reaching a
* 'w'). only a file that genuinely doesn't exist starts a blank
* buffer. anything else that can't be loaded COMPLETELY is refused
* with a message: a permission problem, something that isn't a plain
* file, or content that doesn't fit ed's fixed 32 lines x 79 columns
* (or its 4096-byte load buffer, or contains a nul byte, which the
* nul-terminated line table can't hold). this used to treat every
* open failure as a new file, and silently cut an oversized file down
* to what fit -- after which 'w' overwrote the real, full file with
* that fragment (`cp /bin/ls lscopy; ed lscopy`, 'w', left 3 bytes
* of a 16952-byte binary). */
i32
editor_load(ptr filename, u64 filename_len, ptr lines, ptr line_count_out)
{
ptr content;
i64 clen;
i64 size;
i64 type;
i32 fd;
u64 ci;
i32 line;
u64 pos;
u8 ch;
line = 0;
pos = 0;
*line_count_out = (i64)0;
type = sys_stat(filename, filename_len);
if(type < (i64)0)
{
if(sys_errno() == (i64)1)
{
print_err(2, "ed", "cannot open");
return 0; /* a directory on the way can't be traversed */
}
return 1; /* new file: genuinely nothing yet */
}
if(type != (i64)1)
{
fputs(2, "ed: not a plain file\n");
return 0;
}
fd = sys_open(filename, filename_len);
if(fd < 0)
{
print_err(2, "ed", "cannot open");
return 0;
}
size = sys_fsize(fd);
if(size < (i64)0 || size > (i64)4096)
{
sys_close(fd);
fputs(2, "ed: file too big for ed, not opened\n");
return 0;
}
/* sys_alloc'd, not a stack array: see the file header note on why
* ed's own working buffers moved off the stack/off fixed globals. */
content = sys_alloc((u64)4096);
if(content == (ptr)0)
{
sys_close(fd);
fputs(2, "ed: out of memory\n");
return 0;
}
memset(content, (u8)0, (u64)4096);
clen = 0;
if(size > (i64)0)
{
clen = sys_read(fd, content, (u64)size);
}
sys_close(fd);
if(clen != size)
{
fputs(2, "ed: read failed\n");
return 0;
}
ci = 0;
while(ci < (u64)clen)
{
ch = ptr_byte_at(content, ci);
if(ch == (u8)0)
{
fputs(2, "ed: file contains a nul byte, not opened\n");
return 0;
}
if(line >= 32)
{
fputs(2, "ed: file has more than 32 lines, not opened\n");
return 0;
}
if(ch == (u8)10)
{
mem_byte_set(lines, (u64)line * (u64)81 + pos, (u8)0);
line = line + 1;
pos = 0;
}
else if(pos < (u64)79)
{
mem_byte_set(lines, (u64)line * (u64)81 + pos, ch);
pos = pos + (u64)1;
}
else
{
fputs(2, "ed: a line is longer than 79 columns, not opened\n");
return 0;
}
ci = ci + (u64)1;
}
if(pos > (u64)0)
{
mem_byte_set(lines, (u64)line * (u64)81 + pos, (u8)0);
line = line + 1;
}
*line_count_out = (i64)line;
return 1;
}
void
editor_save(ptr filename, u64 filename_len, ptr lines, i32 line_count)
{
ptr buf;
u64 pos;
i32 i;
u64 j;
u8 ch;
buf = sys_alloc((u64)2688);
if(buf == (ptr)0)
{
fputs(2, "ed: out of memory, not saved\n");
return;
}
pos = 0;
i = 0;
while(i < line_count)
{
j = 0;
ch = ptr_byte_at(lines, (u64)i * (u64)81 + j);
while(ch != (u8)0 && pos < (u64)2686)
{
mem_byte_set(buf, pos, ch);
pos = pos + (u64)1;
j = j + (u64)1;
ch = ptr_byte_at(lines, (u64)i * (u64)81 + j);
}
/* every line gets a trailing \n, including the last -- real
* text files end in one (editor_load can't actually tell
* whether the file it read already had a final newline or
* not: splitting "a\nb\n" and "a\nb" on '\n' produces the
* identical in-memory ["a","b"], so always writing one back
* out is the only consistent choice, and matches what every
* other real editor/tool assumes a text file looks like). */
if(pos < (u64)2686)
{
mem_byte_set(buf, pos, (u8)10);
pos = pos + (u64)1;
}
i = i + 1;
}
if(pos > (u64)0)
{
if(sys_writefile(filename, filename_len, buf, pos) == 1)
{
fputs(1, "saved!\n");
}
else
{
print_err(1, "ed", "save failed");
}
}
else
{
fputs(1, "empty, not saved\n");
}
}
/* displays each occupied line prefixed with its 1-based number
* ("1: text", ...) -- a small, real, explicitly requested addition
* past tape-kernel's own original, which showed lines bare. */
void
editor_display(ptr filename, u64 filename_len, ptr lines, i32 line_count)
{
i32 i;
ptr lineptr;
sys_vga_clear();
sys_write(1, filename, filename_len); /* argv string, not a literal -- stays sys_write */
puts("\n[e] edit [a] append [w] save [q] quit\n");
i = 0;
while(i < line_count)
{
lineptr = lines + (u64)i * (u64)81;
if(ptr_byte_at(lineptr, (u64)0) != (u8)0)
{
put_udec((u64)(i + 1));
puts(": ");
puts(lineptr);
putchar((u8)10);
}
i = i + 1;
}
stdio_flush();
}
/* reads lines from stdin starting at index start (0 for a fresh 'e',
* the current line_count for an 'a' append), stopping at an empty
* line or MAX_LINES (32), writing through mem_byte_set since lines is
* a bare ptr here, not the real array main() itself declares it as.
* returns the new line count.
*
* done is a separate flag from i deliberately: an earlier version
* used i==32 itself as the "stop" signal (breaking the while(i<32)
* condition), then returned i as the new count -- so stopping via an
* empty line (the normal case, not filling all 32 slots) always
* reported a line count of 32 instead of how many lines were
* actually entered. editor_save's own loop trusts line_count to know
* where the real content ends, so every save after a normal e/a
* session was appending one extra newline per phantom "line" 32
* minus the real count -- found by actually cat'ing a saved file and
* seeing ~29 trailing blank lines, not by reading the code. */
i32
editor_enter_lines(ptr lines, i32 start)
{
i8 linebuf[88];
i64 linelen;
i32 i;
i32 done;
u64 k;
i = start;
done = 0;
while(i < 32 && done == 0)
{
fputs(1, "> "); /* unbuffered -- must appear before the blocking read below */
linelen = sys_read(0, &linebuf[0], (u64)82);
if(linelen <= (i64)0)
{
done = 1;
}
else
{
if(ptr_byte_at(&linebuf[0], (u64)(linelen - (i64)1)) == (u8)10)
{
linebuf[linelen - (i64)1] = 0;
linelen = linelen - (i64)1;
}
if(linelen == (i64)0)
{
done = 1; /* empty line: stop */
}
else
{
if(linelen > (i64)79)
{
linelen = (i64)79;
}
k = 0;
while((i64)k < linelen)
{
mem_byte_set(lines, (u64)i * (u64)81 + k, ptr_byte_at(&linebuf[0], k));
k = k + (u64)1;
}
mem_byte_set(lines, (u64)i * (u64)81 + k, (u8)0);
i = i + 1;
}
}
}
return i;
}
global i32
main(i32 argc, ptr argv)
{
ptr lines;
i64 line_count;
ptr filename;
u64 filename_len;
i8 key[8];
if(argc < 2)
{
fputs(2, "usage: ed file\n");
return 1;
}
filename = argv_get(argv, 1);
filename_len = strlen(filename);
/* sys_alloc'd, not a stack array -- see the file header note. */
lines = sys_alloc((u64)2592);
if(lines == (ptr)0)
{
fputs(2, "ed: out of memory\n");
return 1;
}
memset(lines, (u8)0, (u64)2592);
if(editor_load(filename, filename_len, lines, &line_count) == 0)
{
return 1; /* refused -- see editor_load; nothing was touched */
}
while(1)
{
editor_display(filename, filename_len, lines, (i32)line_count);
key[0] = 0;
sys_read(0, &key[0], (u64)1);
if(ptr_byte_at(&key[0], (u64)0) == (u8)101) /* 'e' */
{
sys_vga_clear();
puts("enter each line, empty line to stop\n");
line_count = (i64)editor_enter_lines(lines, 0);
if(line_count == (i64)0)
{
mem_byte_set(lines, (u64)0, (u8)0);
line_count = 1;
}
}
else if(ptr_byte_at(&key[0], (u64)0) == (u8)97) /* 'a' */
{
sys_vga_clear();
puts("append lines, empty line to stop\n");
line_count = (i64)editor_enter_lines(lines, (i32)line_count);
}
else if(ptr_byte_at(&key[0], (u64)0) == (u8)119) /* 'w' */
{
editor_save(filename, filename_len, lines, (i32)line_count);
}
else if(ptr_byte_at(&key[0], (u64)0) == (u8)113) /* 'q' */
{
sys_vga_clear();
return 0;
}
}
return 0;
}