From 6cf6c1cca475c6064089d56f6b58969d36e256fa Mon Sep 17 00:00:00 2001 From: lung Date: Thu, 8 Oct 2026 01:27:24 +0300 Subject: reformat shell.rv, bump zig version --- build.zig.zon | 2 +- kernel/shell.rv | 527 +++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 407 insertions(+), 122 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 114983a..e0dd3e9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .revel, .version = "0.1.0", .fingerprint = 0x453ae5f0bb5a7c9f, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .dependencies = .{ .revo = .{ .path = "revo" }, }, diff --git a/kernel/shell.rv b/kernel/shell.rv index f112889..4940b34 100644 --- a/kernel/shell.rv +++ b/kernel/shell.rv @@ -1,55 +1,89 @@ # i eventually want the shell to be a standalone program and shit -fn pad(text, width) do +fn pad(text, width) -> string do let result = text - while result:len() < width do result = result ~ " " end + + while (result:len() < width) + result = result ~ " " + return result end fn help_row(label, description) do let col = pad(label, 22) - if label:len() >= 22 do col = label ~ " " end + + if label:len() >= 22 do + col = label ~ " " + end + puts(" #{col}#{description}\n") end fn help_group(title, group_name) do puts("\n #{title}\n") + for index in 0..commands:len() do - if commands[index].group == group_name do help_row(commands[index].usage, commands[index].help) end + if commands[index].group == group_name do + help_row(commands[index].usage, commands[index].help) + end end end fn cmd_help(context) do help_group("shell", "shell") help_group("gils (builtin)", "gils") + # the rest of gils, and the programs, are real files in /bin now const items = fs:entries("/bin") + if items != :nil do puts("\n gils (/bin scripts)\n") + for index in 0..items:len() do const name = items[index].name - if (name:len() > 3 and name[(name:len() - 3)..] == ".rv") do help_row(name[0..(name:len() - 3)], "") end + + if name:len() > 3 and name[(name:len() - 3)..] == ".rv" do + help_row(name[0..(name:len() - 3)], "") + end end + puts("\n programs (/bin)\n") + for index in 0..items:len() do const name = items[index].name - if (name:len() <= 3 or name[(name:len() - 3)..] != ".rv") do help_row(name, "") end + + if name:len() <= 3 or name[(name:len() - 3)..] != ".rv" do + help_row(name, "") + end end end puts("\n fonts (font )\n") - for index in 0..fonts:len() do help_row(fonts[index].name, "8x#{fonts[index].height}") end + + for index in 0..fonts:len() do + help_row(fonts[index].name, "8x#{fonts[index].height}") + end + const font_files = fs:entries("/fonts") + if font_files != :nil do puts("\n fonts on disk (font load /fonts/x.psf)\n") - for index in 0..font_files:len() do help_row(font_files[index].name, "") end + + for index in 0..font_files:len() do + help_row(font_files[index].name, "") + end end + puts("\n") end fn welcome() do let fs_status = "no filesystem" - if fs.ready do fs_status = "revelfs mounted" end + + if fs.ready do + fs_status = "revelfs mounted" + end + puts("\n") puts(" kernel written in revo - by an alcoholic\n") puts(" #{(mem_total() shr 20)} MiB ram | #{fs_status} | #{prog_count()} programs\n") @@ -58,287 +92,538 @@ fn welcome() do end # global so /bin scripts can call it from a nested eval fiber -global arg_name = fn(line, start) do +global arg_name = fn(line, start) -> { name: string, rest: num } do let quoted = :false let begin = start - if (begin < line:len() and line[begin] == "\"") do begin += 1 quoted = :true end + + if begin < line:len() and line[begin] == "\"" do + begin += 1 + quoted = :true + end + let index = begin + if quoted do - while (index < line:len() and line[index] != "\"") do index += 1 end - end - else do - while (index < line:len() and line[index] != " ") do index += 1 end + while (index < line:len() and line[index] != "\"") + index += 1 + end else do + while (index < line:len() and line[index] != " ") + index += 1 end + const name = line[begin..index] let rest = index - if (quoted and rest < line:len()) do rest += 1 end - if (rest < line:len() and line[rest] == " ") do rest += 1 end - { name = name, rest = rest } + + if quoted and rest < line:len() do + rest += 1 + end + + if rest < line:len() and line[rest] == " " do + rest += 1 + end + + return { name = name, rest = rest } end -fn parent_of(path) do - if path == "/" return "/" +fn parent_of(path) -> string do + if path == "/" do + return "/" + end + let index = path:len() - 1 - while (index > 0 and path[index] != "/") do index -= 1 end - if index == 0 return "/" + + while (index > 0 and path[index] != "/") + index -= 1 + + if index == 0 do + return "/" + end + return path[0..index] end fn cmd_cd(context) do - const arg = if context.argv:len() > 1 context.argv[1] else "/" - const target = if arg == ".." parent_of(wd.path) else expand_path(arg) - if target == "/proc" or target == "/bin" or fs:is_dir(target) do wd.path = target end - else do puts("cd: #{arg}: not a directory\n") end + const arg = if (context.argv:len() > 1) context.argv[1] else "/" + + const target = if (arg == "..") parent_of(wd.path) else expand_path(arg) + + if target == "/proc" + or target == "/bin" + or fs:is_dir(target) do + wd.path = target + end else do + puts("cd: #{arg}: not a directory\n") + end end -fn cmd_pwd(context) puts("#{wd.path}\n") +fn cmd_pwd(context) + puts("#{wd.path}\n") + +fn wipe() + console:clear() -fn wipe() console:clear() -fn cmd_clear(context) wipe() +fn cmd_clear(context) + wipe() fn zoom_by(delta) do const entry = console.font_entry + # ttf is vector, stepping the size interactively is fine - if (entry != :nil and entry.kind == :ttf) do + if entry != :nil and entry.kind == :ttf do let size = entry.size + delta * 4 - if size < 8 do size = 8 end - if size > 72 do size = 72 end - if size == entry.size do puts("zoom #{size}px\n") return :nil end + + if (size < 8) size = 8 + if (size > 72) size = 72 + + if size == entry.size do + puts("zoom #{size}px\n") + return :nil + end + const face = load_ttf(entry.path, size) - if face == :nil do puts("zoom: rasterize failed\n") return :nil end + + if face == :nil do + puts("zoom: rasterize failed\n") + return :nil + end + add_font(entry.name, face.glyphs, face.width, face.height, :ttf, entry.path, size) console:set_font(find_font(entry.name)) puts("zoom #{size}px\n") + return :nil end + # bitmap fonts js use integer pixel scale let scale = console.scale + delta - if scale < 1 do scale = 1 end - if scale > 6 do scale = 6 end + + if (scale < 1) scale = 1 + if (scale > 6) scale = 6 + console.scale = scale wipe() + puts("zoom #{scale}x\n") end fn cmd_zoom(context) do - const direction = if context.argv:len() > 1 context.argv[1] else "" - if direction == "in" do zoom_by(1) end - else if direction == "out" do zoom_by(-1) end - else do puts("usage: zoom in | zoom out\n") end + const direction = + if (context.argv:len() > 1) + context.argv[1] + else "" + + match direction + | "in" => zoom_by(1) + | "out" => zoom_by(-1) + | _ => puts("usage: zoom in | zoom out\n") end -fn font_basename(path) do +fn font_basename(path) -> string do let start = 0 let index = 0 - while index < path:len() do - if path[index] == "/" do start = index + 1 end + + while (index < path:len()) do + if path[index] == "/" do + start = index + 1 + end + index += 1 end + let name = path[start..] let dot = -1 - for i in 0..name:len() do if name[i] == "." do dot = i end end - if dot > 0 do name = name[0..dot] end + + for i in 0..name:len() do + if (name[i] == ".") + dot = i + end + + if (dot > 0) + name = name[0..dot] + return name end -fn load_psf(path) do +fn load_psf(path) -> { glyphs: table, width: num, height: num } | :nil do const size = fs:size(path) - if size < 4 return :nil + + if size < 4 do + return :nil + end + const data = fs:read_bytes(path, 0, size) - if (data[0]:ascii() != 0x36 or data[1]:ascii() != 0x04) do return :nil end + + if data[0]:ascii() != 0x36 or data[1]:ascii() != 0x04 do + return :nil + end + const height = data[3]:ascii() - if (height == 0 or size < 4 + 128 * height) do return :nil end + + if height == 0 or size < 4 + 128 * height do + return :nil + end + let glyphs = {} + for code in 32..128 do const base = 4 + code * height let rows = {} - for row in 0..height do rows:push(reverse8(data[base + row]:ascii())) end + + for row in 0..height do + rows:push(reverse8(data[base + row]:ascii())) + end + glyphs:push(rows) end + return { glyphs = glyphs, width = 8, height = height } end -fn has_slash(text) do - for index in 0..text:len() do if text[index] == "/" return :true end +fn has_slash(text) -> bool do + for index in 0..text:len() do + if (text[index] == "/") + return :true + end + return :false end -fn ends_with(text, suffix) do - if text:len() < suffix:len() return :false +fn ends_with(text, suffix) -> bool do + if text:len() < suffix:len() do + return :false + end + return text[(text:len() - suffix:len())..] == suffix end -fn str_to_int(text) do +fn str_to_int(text) -> num do let value = 0 + for index in 0..text:len() do const code = text[index]:ascii() - if (code >= 48 and code <= 57) do value = value * 10 + (code - 48) end + + if code >= 48 and code <= 57 do + value = value * 10 + (code - 48) + end end + return value end -fn font_file(name) do - if has_slash(name) do - const p = expand_path(name) - if (ends_with(p, ".ttf") and fs:size(p) >= 0) return { path = p, kind = :ttf } - if (ends_with(p, ".psf") and fs:size(p) >= 0) return { path = p, kind = :psf } - if fs:size(p ~ ".ttf") >= 0 return { path = p ~ ".ttf", kind = :ttf } - if fs:size(p ~ ".psf") >= 0 return { path = p ~ ".psf", kind = :psf } - return { path = p, kind = :none } - end - if fs:size("/fonts/#{name}.psf") >= 0 return { path = "/fonts/#{name}.psf", kind = :psf } - if fs:size("/fonts/#{name}.ttf") >= 0 return { path = "/fonts/#{name}.ttf", kind = :ttf } - return { path = name, kind = :none } +#* resolve a font name to a path and a kind *# +fn font_file(name) -> { path: string, kind: ( :ttf | :psf | :none ) } do + return match + | has_slash(name) => do + const p = expand_path(name) + + if ends_with(p, ".ttf") and fs:size(p) >= 0 do + return { path = p, kind = :ttf } + end + + if ends_with(p, ".psf") and fs:size(p) >= 0 do + return { path = p, kind = :psf } + end + + if fs:size(p ~ ".ttf") >= 0 do + return { path = p ~ ".ttf", kind = :ttf } + end + + if fs:size(p ~ ".psf") >= 0 do + return { path = p ~ ".psf", kind = :psf } + end + + return { path = p, kind = :none } + end + | fs:size("/fonts/#{name}.psf") >= 0 => { path = "/fonts/#{name}.psf", kind = :psf } + | fs:size("/fonts/#{name}.ttf") >= 0 => { path = "/fonts/#{name}.ttf", kind = :ttf } + | _ => { path = name, kind = :none } end fn cmd_font(context) do if context.argv:len() < 2 do puts("fonts:") - for index in 0..fonts:len() do puts(" #{fonts[index].name}(#{fonts[index].width}x#{fonts[index].height})") end + + for index in 0..fonts:len() do + puts(" #{fonts[index].name}(#{fonts[index].width}x#{fonts[index].height})") + end + puts("\n") + const font_files = fs:entries("/fonts") + if font_files != :nil do puts("on disk:") - for index in 0..font_files:len() do puts(" #{font_files[index].name}") end + + for index in 0..font_files:len() do + puts(" #{font_files[index].name}") + end + puts("\n") end + return :nil end + # `font load [size]` still works, but a bare name/path is the same thing let name = context.argv[1] let arg_at = 1 + if name == "load" do - if context.argv:len() < 3 do puts("usage: font load [size]\n") return :nil end + if context.argv:len() < 3 do + puts("usage: font load [size]\n") + return :nil + end + name = context.argv[2] arg_at = 2 end + # an explicit size (ttf only) forces a fresh rasterize. without one we just # switch to whatever is already in the registry under that name let size = 0 - if context.argv:len() > arg_at + 1 do size = str_to_int(context.argv[arg_at + 1]) end + + if context.argv:len() > arg_at + 1 do + size = str_to_int(context.argv[arg_at + 1]) + end + if size == 0 do const existing = find_font(name) - if existing != :nil do console:set_font(existing) return :nil end + + if existing != :nil do + console:set_font(existing) + return :nil + end end + const info = font_file(name) - if info.kind == :none do puts("font: #{name}: no such font\n") return :nil end + + if info.kind == :none do + puts("font: #{name}: no such font\n") + return :nil + end + const reg_name = font_basename(info.path) + if info.kind == :psf do const prior = find_font(reg_name) - if prior != :nil do console:set_font(prior) return :nil end + + if prior != :nil do + console:set_font(prior) + return :nil + end + const parsed = load_psf(info.path) - if parsed == :nil do puts("font: #{name}: not a psf font\n") return :nil end + + if parsed == :nil do + puts("font: #{name}: not a psf font\n") + return :nil + end + add_font(reg_name, parsed.glyphs, parsed.width, parsed.height, :bitmap, :nil, 0) console:set_font(find_font(reg_name)) + return :nil end + # ttf: rasterize ascii at the requested size (default 18) - if size == 0 do size = 18 end + if size == 0 do + size = 18 + end + const face = load_ttf(info.path, size) - if face == :nil do puts("font: #{name}: bad ttf\n") return :nil end + + if face == :nil do + puts("font: #{name}: bad ttf\n") + return :nil + end + add_font(reg_name, face.glyphs, face.width, face.height, :ttf, info.path, size) console:set_font(find_font(reg_name)) end -fn bcd(byte) (byte shr 4) * 10 + (byte band 15) -fn rtc(cmos_reg) do +fn bcd(byte) -> num do + return (byte shr 4) * 10 + (byte band 15) +end + +fn rtc(cmos_reg) -> num do outb(0x70, cmos_reg) - inb(0x71) + return inb(0x71) +end + +fn cmd_time(context) do + const hour = 4 |> rtc |> bcd + const min = 2 |> rtc |> bcd + const sec = 0 |> rtc |> bcd + + puts("utc #{hour}:#{min}:#{sec}\n") end -fn cmd_time(context) puts("utc #{bcd(rtc(4))}:#{bcd(rtc(2))}:#{bcd(rtc(0))}\n") -# 0x prefixed 16 digit hex -fn hex(value) do +#* 0x prefixed 16 digit hex *# +fn hex(value) -> string do const digits = "0123456789abcdef" let result = "" - for index in 0..16 do result = result ~ digits[(value shr ((15 - index) * 4)) band 15] end - "0x#{result}" + + for index in 0..16 do + result = result ~ digits[(value shr ((15 - index) * 4)) band 15] + end + + return "0x#{result}" end -fn mem_type_name(kind) do - if kind == 0 return "usable" - if kind == 1 return "reserved" - if kind == 2 return "acpi-reclaimable" - if kind == 3 return "acpi-nvs" - if kind == 4 return "bad" - if kind == 5 return "bootloader-reclaimable" - if kind == 6 return "kernel+modules" - if kind == 7 return "framebuffer" - "unknown" +fn mem_type_name(kind) -> string do + match kind + | 0 => "usable" + | 1 => "reserved" + | 2 => "acpi-reclaimable" + | 3 => "acpi-nvs" + | 4 => "bad" + | 5 => "bootloader-reclaimable" + | 6 => "kernel+modules" + | 7 => "framebuffer" + | _ => "unknown" end -# the boot memory map dump +#* the boot memory map dump *# fn dump_memmap() do puts("memory map:\n") + for index in 0..memmap_count() do puts(" #{hex(memmap_base(index))} len=#{hex(memmap_len(index))} type=#{mem_type_name(memmap_kind(index))}\n") end + puts("usable: #{(mem_total() shr 20)} MiB, free: #{(mem_free() shr 20)} MiB\n") end -fn cmd_mem(context) puts("#{(mem_free() shr 20)} MiB free / #{(mem_total() shr 20)} MiB usable\n") -fn cmd_uptime(context) puts("#{math.floor(uptime() / 100)}s (#{uptime()} ticks)\n") -fn cmd_ps(context) puts("#{proc_count()} process(es) running\n") -fn cmd_echo(context) do if context.line:len() > 5 puts("#{context.line[5..]}\n") else puts("\n") end -fn cmd_mkfs(context) fs:format() +fn cmd_mem(context) + puts("#{(mem_free() shr 20)} MiB free / #{(mem_total() shr 20)} MiB usable\n") + +fn cmd_uptime(context) + puts("#{math.floor(uptime() / 100)}s (#{uptime()} ticks)\n") + +fn cmd_ps(context) + puts("#{proc_count()} process(es) running\n") + +fn cmd_echo(context) do + if context.line:len() > 5 do + puts("#{context.line[5..]}\n") + end else do + puts("\n") + end +end + +fn cmd_mkfs(context) + fs:format() -fn exec_file(target) do +#* resolve a command name to a path and a kind *# +fn exec_file(target) -> { path: string, kind: ( :elf | :script | :none ) } do let candidates = {} + if has_slash(target) do - candidates:push(expand_path(target)) - candidates:push(expand_path(target) ~ ".rv") - end - else do - candidates:push(expand_path(target)) - candidates:push(expand_path(target) ~ ".rv") - candidates:push("/bin/#{target}") - candidates:push("/bin/#{target}.rv") + candidates:push( + expand_path(target), + expand_path(target) ~ ".rv", + ) + end else do + candidates:push( + expand_path(target), + expand_path(target) ~ ".rv", + "/bin/#{target}", + "/bin/#{target}.rv", + ) end + for index in 0..candidates:len() do const path = candidates[index] + if fs:size(path) >= 0 do const head = fs:read_bytes(path, 0, 4) - if (head:len() >= 4 and head[0]:ascii() == 127 and head[1]:ascii() == 69 and head[2]:ascii() == 76 and head[3]:ascii() == 70) return { path = path, kind = :elf } + + if head:len() >= 4 + and head[0]:ascii() == 127 + and head[1]:ascii() == 69 + and head[2]:ascii() == 76 + and head[3]:ascii() == 70 do + return { path = path, kind = :elf } + end + return { path = path, kind = :script } end end + return { path = target, kind = :none } end fn cmd_run(context) do - if context.argv:len() <= 1 do list_progs() return :nil end + if context.argv:len() <= 1 do + list_progs() + return :nil + end + const target = context.argv[1] const info = exec_file(target) - if info.kind == :none do puts("run: #{target}: not found\n") return :nil end + + if info.kind == :none do + puts("run: #{target}: not found\n") + return :nil + end + const sub_argv = context.rest:split(" ") const sub_ctx = make_ctx(context.rest, sub_argv) + if info.kind == :script do run_script_path(info.path, target, sub_ctx) + return :nil end - if load_file_elf(info.path, sub_ctx.rest) == :true return :hold + + if load_file_elf(info.path, sub_ctx.rest) == :true do + return :hold + end + puts("run: #{target}: not executable\n") + return :nil end -fn run(line) do +#* run one shell line *# +fn run(line) -> :nil do # the fact that the shell needs to interface directly with ed isnt, very nice # this is a TODO btw - if gils_ed_active() do gils_ed_feed(line) return :nil end - if line:trim():len() == 0 do puts(prompt()) return :nil end + if gils_ed_active() do + gils_ed_feed(line) + return :nil + end + + if line:trim():len() == 0 do + puts(prompt()) + return :nil + end + const argv = line:split(" ") const context = make_ctx(line, argv) const command = lookup(argv[0]) + if command != :nil do # :hold means the command took over (modal editor / launched a program) so # we dont draw a fresh prompt, otherwise we do const result = command.handler(context) - if result != :hold do puts(prompt()) end + + if result != :hold do + puts(prompt()) + end + + return :nil + end + + if run_script(argv[0], context) == :true do + puts(prompt()) + return :nil + end + + if run_prog(argv[0], context.rest) == :true do return :nil end - if run_script(argv[0], context) == :true do puts(prompt()) return :nil end - if run_prog(argv[0], context.rest) == :true return :nil puts("#{argv[0]}: command not found\n") puts(prompt()) end -- cgit v1.3 From 0d090f616a5ca7932ddc2e8dd5e4273a1189380b Mon Sep 17 00:00:00 2001 From: lung Date: Thu, 8 Oct 2026 02:00:35 +0300 Subject: reformat fs.rv --- kernel/fs.rv | 504 +++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 389 insertions(+), 115 deletions(-) diff --git a/kernel/fs.rv b/kernel/fs.rv index 255797f..b23be7f 100644 --- a/kernel/fs.rv +++ b/kernel/fs.rv @@ -19,220 +19,329 @@ const END = 0xffff # end of chain marker in the fat const T_FILE = 1 const T_DIR = 2 -fn split_path(path) do +#* into non empty parts *# +fn split_path(path) -> table do const raw_parts = path:split("/") let parts = {} + for index in 0..raw_parts:len() do - if raw_parts[index]:len() > 0 parts:push(raw_parts[index]) + if (raw_parts[index]:len() > 0) parts:push(raw_parts[index]) end + return parts end -# scratch is a spare sector for superblocks/dirs/data, fat is a separate scratch -# so fat lookups dont trample a dir block were mid edit. global fs = { - scratch = 0, fat = 0, ready = :false, total = 0, root = 0, - - fat_get = fn(self, block) do + # scratch is a spare sector for superblocks/dirs/data + scratch = 0, + # fat is a separate scratch + # so fat lookups dont trample a dir block were mid edit. + fat = 0, + ready = :false, total = 0, root = 0, + + fn fat_get(self, block) -> num do disk_read(FAT_LBA + (block shr 8), self.fat) - mem_read(self.fat + (block band 255) * 2, 2) + + return mem_read(self.fat + (block band 255) * 2, 2) end, - fat_set = fn(self, block, value) do + fn fat_set(self, block, value) do const sector = FAT_LBA + (block shr 8) disk_read(sector, self.fat) mem_write(self.fat + (block band 255) * 2, value, 2) disk_write(sector, self.fat) end, - fat_alloc = fn(self) do + fn fat_alloc(self) -> num do let block = DATA_LBA - while block < self.total do + + while (block < self.total) do disk_read(FAT_LBA + (block shr 8), self.fat) + let offset = block band 255 + while (offset < 256 and block < self.total) do - if mem_read(self.fat + offset * 2, 2) == 0 return block + if mem_read(self.fat + offset * 2, 2) == 0 do + return block + end + offset += 1 block += 1 end end - 0 + + return 0 end, - free_chain = fn(self, first) do + fn free_chain(self, first) do let block = first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + self:fat_set(block, 0) block = next_block end end, - # entry is 32 bytes: - # +0 type (0 free/1 file/2 dir) - # +1 name len - # +2 first block - # +4 size - # +8 name bytes - ent_name = fn(self, entry_offset) do + #* entry is 32 bytes: + - +0 type (0 free/1 file/2 dir) + - +1 name len + - +2 first block + - +4 size + - +8 name bytes *# + fn ent_name(self, entry_offset) -> string do const name_length = mem_read(self.scratch + entry_offset + 1, 1) let result = "" - for index in 0..name_length do result = result ~ string.of_ascii(mem_read(self.scratch + entry_offset + 8 + index, 1)) end - result + + for index in 0..name_length do + result = result ~ string.of_ascii(mem_read(self.scratch + entry_offset + 8 + index, 1)) + end + + return result end, - ent_write = fn(self, entry_offset, type, name, first, size) do + fn ent_write(self, entry_offset, type, name, first, size) do mem_write(self.scratch + entry_offset, type, 1) mem_write(self.scratch + entry_offset + 1, name:len(), 1) mem_write(self.scratch + entry_offset + 2, first, 2) mem_write(self.scratch + entry_offset + 4, size, 4) - for index in 0..name:len() do mem_write(self.scratch + entry_offset + 8 + index, name[index]:ascii(), 1) end + + for index in 0..name:len() do + mem_write(self.scratch + entry_offset + 8 + index, name[index]:ascii(), 1) + end end, - dir_lookup = fn(self, dir_first, name) do + fn dir_lookup(self, dir_first, name) -> { type: num, first: num, size: num } | :nil do let block = dir_first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do const entry_offset = slot * 32 + if mem_read(self.scratch + entry_offset, 1) != 0 and self:ent_name(entry_offset) == name do return { type = mem_read(self.scratch + entry_offset, 1), first = mem_read(self.scratch + entry_offset + 2, 2), size = mem_read(self.scratch + entry_offset + 4, 4) } end end + block = next_block end + return :nil end, - # add or overwrite `name` in the dir - # grows a block if every slot is taken - dir_set = fn(self, dir_first, name, type, first, size) do + #* add or overwrite `name` in the dir + grows a block if every slot is taken *# + fn dir_set(self, dir_first, name, type, first, size) -> bool do let block = dir_first let found_block = 0 let found_offset = 0 let free_block = 0 let free_offset = -1 let last_block = dir_first + while (block != 0 and block != END) do last_block = block + const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do const entry_offset = slot * 32 + if mem_read(self.scratch + entry_offset, 1) == 0 do - if free_offset < 0 do free_block = block free_offset = entry_offset end - end - else do - if self:ent_name(entry_offset) == name do found_block = block found_offset = entry_offset end + if free_offset < 0 do + free_block = block + free_offset = entry_offset + end + end else do + if self:ent_name(entry_offset) == name do + found_block = block + found_offset = entry_offset + end end end + block = next_block end let target_block = 0 let target_offset = 0 - if found_block != 0 do target_block = found_block target_offset = found_offset end - else if free_offset >= 0 do target_block = free_block target_offset = free_offset end - else do - const new_block = self:fat_alloc() - if new_block == 0 return :false - self:fat_set(new_block, END) - self:fat_set(last_block, new_block) - mem_zero(self.scratch, BLOCK) - disk_write(new_block, self.scratch) - target_block = new_block - target_offset = 0 - end + + match + | found_block != 0 => do + target_block = found_block + target_offset = found_offset + end + | free_offset >= 0 => do + target_block = free_block + target_offset = free_offset + end + | _ => do + const new_block = self:fat_alloc() + + if new_block == 0 do + return :false + end + + self:fat_set(new_block, END) + self:fat_set(last_block, new_block) + mem_zero(self.scratch, BLOCK) + disk_write(new_block, self.scratch) + target_block = new_block + target_offset = 0 + end disk_read(target_block, self.scratch) self:ent_write(target_offset, type, name, first, size) disk_write(target_block, self.scratch) + return :true end, - dir_clear = fn(self, dir_first, name) do + fn dir_clear(self, dir_first, name) -> { type: num, first: num } | :nil do let block = dir_first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do const entry_offset = slot * 32 + if mem_read(self.scratch + entry_offset, 1) != 0 and self:ent_name(entry_offset) == name do const info = { type = mem_read(self.scratch + entry_offset, 1), first = mem_read(self.scratch + entry_offset + 2, 2) } + mem_write(self.scratch + entry_offset, 0, 1) disk_write(block, self.scratch) + return info end end + block = next_block end + return :nil end, - dir_empty = fn(self, dir_first) do + fn dir_empty(self, dir_first) -> bool do let block = dir_first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do - if mem_read(self.scratch + slot * 32, 1) != 0 return :false + if mem_read(self.scratch + slot * 32, 1) != 0 do + return :false + end end + block = next_block end + return :true end, - resolve = fn(self, path) do + # resolve a path to its entry + fn resolve(self, path) -> { type: num, first: num, size: num } | :nil do const parts = split_path(path) let current = self.root let entry = { type = T_DIR, first = self.root, size = 0 } + for index in 0..parts:len() do - if entry.type != T_DIR return :nil + if entry.type != T_DIR do + return :nil + end + const found = self:dir_lookup(current, parts[index]) - if found == :nil return :nil + + if found == :nil do + return :nil + end + entry = found current = found.first end + return entry end, - resolve_parent = fn(self, path) do + # resolve a path to its parent dir and name + fn resolve_parent(self, path) -> { dir: num, name: string } | :nil do const parts = split_path(path) - if parts:len() == 0 return :nil + + if parts:len() == 0 do + return :nil + end + let parent_dir = self.root + for index in 0..(parts:len() - 1) do const found = self:dir_lookup(parent_dir, parts[index]) - if found == :nil or found.type != T_DIR return :nil + + if found == :nil or found.type != T_DIR do + return :nil + end + parent_dir = found.first end + return { dir = parent_dir, name = parts[parts:len() - 1] } end, - mount = fn(self) do + fn mount(self) do self.scratch = frame_alloc() self.fat = frame_alloc() self.total = disk_sectors() - if self.total == 0 do self.ready = :false return :nil end + + if self.total == 0 do + self.ready = :false + return :nil + end + disk_read(SB_LBA, self.scratch) + if mem_read(self.scratch, 4) == MAGIC do self.root = mem_read(self.scratch + 8, 2) self.ready = :true + end else do + self.ready = :false end - else do self.ready = :false end end, - ensure = fn(self) do - if self.ready return :true + fn ensure(self) -> bool do + if self.ready do + return :true + end + puts("no filesystem, run mkfs first\n") - :false + + return :false end, - format = fn(self) do - if disk_sectors() == 0 do puts("mkfs: no disk\n") return :nil end + fn format(self) do + if disk_sectors() == 0 do + puts("mkfs: no disk\n") + return :nil + end + mem_zero(self.scratch, BLOCK) - for sector in 0..FAT_SECTORS do disk_write(FAT_LBA + sector, self.scratch) end + + for sector in 0..FAT_SECTORS do + disk_write(FAT_LBA + sector, self.scratch) + end + const root = self:fat_alloc() + self:fat_set(root, END) mem_zero(self.scratch, BLOCK) disk_write(root, self.scratch) @@ -243,31 +352,70 @@ global fs = { mem_write(self.scratch + 8, root, 2) disk_write(SB_LBA, self.scratch) self.ready = :true + puts("formatted #{self.total} sectors, #{(self.total shr 1)} KiB\n") end, - mkdir = fn(self, path) do - if self:ensure() == :false return :nil + fn mkdir(self, path) do + if self:ensure() == :false do + return :nil + end + const parent = self:resolve_parent(path) - if parent == :nil do puts("mkdir: #{path}: bad path\n") return :nil end - if parent.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return :nil end - if self:dir_lookup(parent.dir, parent.name) != :nil do puts("mkdir: #{parent.name}: already exists\n") return :nil end + + if parent == :nil do + puts("mkdir: #{path}: bad path\n") + return :nil + end + + if parent.name:len() > NAME_MAX do + puts("name too long (max #{NAME_MAX})\n") + return :nil + end + + if self:dir_lookup(parent.dir, parent.name) != :nil do + puts("mkdir: #{parent.name}: already exists\n") + return :nil + end + const new_block = self:fat_alloc() - if new_block == 0 do puts("disk full\n") return :nil end + + if new_block == 0 do + puts("disk full\n") + return :nil + end + self:fat_set(new_block, END) mem_zero(self.scratch, BLOCK) disk_write(new_block, self.scratch) self:dir_set(parent.dir, parent.name, T_DIR, new_block, 0) end, - store = fn(self, path, text) do - if self:ensure() == :false return 0 + fn store(self, path, text) -> num do + if self:ensure() == :false do + return 0 + end + const parent = self:resolve_parent(path) - if parent == :nil do puts("#{path}: bad path\n") return 0 end - if parent.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return 0 end + + if parent == :nil do + puts("#{path}: bad path\n") + return 0 + end + + if parent.name:len() > NAME_MAX do + puts("name too long (max #{NAME_MAX})\n") + return 0 + end + const existing = self:dir_lookup(parent.dir, parent.name) + if existing != :nil do - if existing.type == T_DIR do puts("#{parent.name}: is a directory\n") return 0 end + if existing.type == T_DIR do + puts("#{parent.name}: is a directory\n") + return 0 + end + self:free_chain(existing.first) end @@ -275,168 +423,294 @@ global fs = { let first = 0 let previous = 0 let written = 0 - while written < size do + + while (written < size) do const block = self:fat_alloc() - if block == 0 do puts("disk full\n") return 0 end + + if block == 0 do + puts("disk full\n") + return 0 + end + self:fat_set(block, END) - if first == 0 do first = block end - else do self:fat_set(previous, block) end + + if first == 0 do + first = block + end else do + self:fat_set(previous, block) + end + mem_zero(self.scratch, BLOCK) # blit a whole block of the string at once, per byte was O(n^2) and choked # the second files got into the kilobytes let chunk = size - written - if chunk > BLOCK do chunk = BLOCK end + + if (chunk > BLOCK) chunk = BLOCK + blit_str(self.scratch, text[written..(written + chunk)]) disk_write(block, self.scratch) written += chunk previous = block end + self:dir_set(parent.dir, parent.name, T_FILE, first, size) + return 1 end, - write = fn(self, path, text) do - if self:store(path, text) == 1 puts("wrote #{path} (#{text:len()} bytes)\n") + fn write(self, path, text) do + if (self:store(path, text) == 1) puts("wrote #{path} (#{text:len()} bytes)\n") end, - size = fn(self, path) do + fn size(self, path) -> num do const entry = self:resolve(path) - if entry == :nil or entry.type == T_DIR return -1 + + if entry == :nil or entry.type == T_DIR do + return -1 + end + return entry.size end, - read_bytes = fn(self, path, offset, length) do + fn read_bytes(self, path, offset, length) -> string do const entry = self:resolve(path) - if entry == :nil or entry.type == T_DIR return "" + + if entry == :nil or entry.type == T_DIR do + return "" + end + let block = entry.first let stop = offset + length - if stop > entry.size do stop = entry.size end + + if (stop > entry.size) stop = entry.size + let position = 0 let result = "" + while (block != 0 and block != END and position < stop) do const next_block = self:fat_get(block) - if (position + BLOCK > offset and position < stop) do + + if position + BLOCK > offset and position < stop do disk_read(block, self.scratch) # pull this blocks slice in one go, per byte concat was O(n^2) let from = 0 - if offset > position do from = offset - position end + + if (offset > position) from = offset - position + let to = BLOCK - if stop - position < BLOCK do to = stop - position end + + if (stop - position < BLOCK) to = stop - position + result = result ~ phys_to_str(self.scratch + from, to - from) end + position += BLOCK block = next_block end + return result end, - read_into = fn(self, path, dest) do + fn read_into(self, path, dest) -> num do const entry = self:resolve(path) - if entry == :nil or entry.type == T_DIR return 0 + + if entry == :nil or entry.type == T_DIR do + return 0 + end + let block = entry.first let offset = 0 let remaining = entry.size + while (block != 0 and block != END and remaining > 0) do const next_block = self:fat_get(block) + disk_read(block, dest + offset) offset += BLOCK remaining -= BLOCK block = next_block end + return entry.size end, - cat = fn(self, path) do - if self:ensure() == :false return :nil + fn cat(self, path) do + if self:ensure() == :false do + return :nil + end + const entry = self:resolve(path) - if entry == :nil do puts("cat: #{path}: no such file\n") return :nil end - if entry.type == T_DIR do puts("cat: #{path}: is a directory\n") return :nil end + + if entry == :nil do + puts("cat: #{path}: no such file\n") + return :nil + end + + if entry.type == T_DIR do + puts("cat: #{path}: is a directory\n") + return :nil + end + let block = entry.first let remaining = entry.size let result = "" + while (block != 0 and block != END and remaining > 0) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) - const chunk = if remaining > BLOCK BLOCK else remaining - for index in 0..chunk do result = result ~ string.of_ascii(mem_read(self.scratch + index, 1)) end + + const chunk = if (remaining > BLOCK) BLOCK else remaining + + for index in 0..chunk do + result = result ~ string.of_ascii(mem_read(self.scratch + index, 1)) + end + remaining -= chunk block = next_block end + puts(result) puts("\n") end, - ls = fn(self, path) do - if self:ensure() == :false return -1 + fn ls(self, path) -> num do + if self:ensure() == :false do + return -1 + end + const entry = self:resolve(path) - if entry == :nil do puts("ls: #{path}: no such file\n") return -1 end - if entry.type != T_DIR do puts(" #{path} #{entry.size} bytes\n") return -1 end + + if entry == :nil do + puts("ls: #{path}: no such file\n") + return -1 + end + + if entry.type != T_DIR do + puts(" #{path} #{entry.size} bytes\n") + return -1 + end + let count = 0 let block = entry.first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do const entry_offset = slot * 32 const entry_type = mem_read(self.scratch + entry_offset, 1) + if entry_type != 0 do const name = self:ent_name(entry_offset) - if entry_type == T_DIR do puts(" #{name}/\n") end - else do puts(" #{name} #{mem_read(self.scratch + entry_offset + 4, 4)} bytes\n") end + + if entry_type == T_DIR do + puts(" #{name}/\n") + end else do + puts(" #{name} #{mem_read(self.scratch + entry_offset + 4, 4)} bytes\n") + end + count += 1 end end + block = next_block end + return count end, - rm = fn(self, path) do - if self:ensure() == :false return :nil + fn rm(self, path) do + if self:ensure() == :false do + return :nil + end + const parent = self:resolve_parent(path) - if parent == :nil do puts("rm: #{path}: no such file\n") return :nil end + + if parent == :nil do + puts("rm: #{path}: no such file\n") + return :nil + end + const entry = self:dir_lookup(parent.dir, parent.name) - if entry == :nil do puts("rm: #{path}: no such file\n") return :nil end + + if entry == :nil do + puts("rm: #{path}: no such file\n") + return :nil + end + if entry.type == T_DIR and self:dir_empty(entry.first) == :false do puts("rm: #{parent.name}: directory not empty\n") return :nil end + self:free_chain(entry.first) self:dir_clear(parent.dir, parent.name) puts("removed #{parent.name}\n") end, - is_dir = fn(self, path) do + fn is_dir(self, path) -> bool do const entry = self:resolve(path) + return entry != :nil and entry.type == T_DIR end, - entries = fn(self, path) do + fn entries(self, path) -> table | :nil do const entry = self:resolve(path) - if entry == :nil or entry.type != T_DIR return :nil + + if entry == :nil or entry.type != T_DIR do + return :nil + end + let result = {} let block = entry.first + while (block != 0 and block != END) do const next_block = self:fat_get(block) + disk_read(block, self.scratch) + for slot in 0..ENTS do const entry_offset = slot * 32 const entry_type = mem_read(self.scratch + entry_offset, 1) - if entry_type != 0 result:push({ name = self:ent_name(entry_offset), type = entry_type, size = mem_read(self.scratch + entry_offset + 4, 4) }) + + if entry_type != 0 do + result:push({ + name = self:ent_name(entry_offset), + type = entry_type, + size = mem_read(self.scratch + entry_offset + 4, 4), + }) + end end + block = next_block end + return result end, - unlink = fn(self, path) do + fn unlink(self, path) -> bool do const parent = self:resolve_parent(path) - if parent == :nil return :false + + if parent == :nil do + return :false + end + const entry = self:dir_lookup(parent.dir, parent.name) - if entry == :nil return :false - if (entry.type == T_DIR and self:dir_empty(entry.first) == :false) return :false + + if entry == :nil do + return :false + end + + if entry.type == T_DIR and self:dir_empty(entry.first) == :false do + return :false + end + self:free_chain(entry.first) self:dir_clear(parent.dir, parent.name) + return :true end, } -- cgit v1.3 From 96aa6d3f0bfcc437d840cd338c92257ae58942f2 Mon Sep 17 00:00:00 2001 From: lung Date: Thu, 8 Oct 2026 02:25:49 +0300 Subject: enable regex feat for revo --- build.zig | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 469d513..2a4466e 100644 --- a/build.zig +++ b/build.zig @@ -21,9 +21,9 @@ pub fn build(b: *std.Build) void { }; const Feature = std.Target.x86.Feature; - query.cpu_features_sub.addFeature(@intFromEnum(Feature.mmx)); - query.cpu_features_sub.addFeature(@intFromEnum(Feature.avx)); - query.cpu_features_sub.addFeature(@intFromEnum(Feature.avx2)); + query.cpu_features_sub.addFeature(@backingInt(Feature.mmx)); + query.cpu_features_sub.addFeature(@backingInt(Feature.avx)); + query.cpu_features_sub.addFeature(@backingInt(Feature.avx2)); const target = b.resolveTargetQuery(query); @@ -44,7 +44,11 @@ pub fn build(b: *std.Build) void { kernel.setLinkerScript(b.path("boot/linker.ld")); // the big guns - const revo_dep = b.dependency("revo", .{ .target = target, .optimize = optimize }); + const revo_dep = b.dependency("revo", .{ + .target = target, + .optimize = optimize, + .features = "regex", + }); const revo_mod = revo_dep.module("revo"); var visited: std.AutoHashMap(*std.Build.Module, void) = .init(b.allocator); -- cgit v1.3 From 2219f9b60fb3cd42991bece1fa55a503862d26a8 Mon Sep 17 00:00:00 2001 From: lung Date: Thu, 8 Oct 2026 02:58:13 +0300 Subject: /etc/profile and C-BS, start a little README.md handbook --- README.md | 14 +++++++++++++- kernel/input.rv | 8 +++++++- kernel/main.rv | 4 ++++ kernel/seed.rv | 12 ++++++++++-- kernel/tty.rv | 3 ++- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1c859ff..7d75caf 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,17 @@ god so needy $ make run ``` -## License +# handbook +once you boot in, you wanna see `help` + +your motd lies in `/etc/motd` and the profile script in `/etc/profile` + +## working with files +`cat`, `ls`, `rm`, `mkdir`, whatever, work as you'd expect them to + +`ed` is your only text editor\ +it's cooler than gnu ed. type `h` to see help\ +you also get a `!` command for free: it will run your file, saved or unsaved, as a revo script + +# License MIT, see [LICENSE](LICENSE). diff --git a/kernel/input.rv b/kernel/input.rv index ed0f4f7..139edf0 100644 --- a/kernel/input.rv +++ b/kernel/input.rv @@ -34,7 +34,13 @@ fn decode_key(code) do if code == 0x9d do shell.ctrl = :false return :nil end # ctrl up if code >= 128 return :nil # other releases if code == 28 return "\n" # enter - if code == 14 return "\x08" # backspace + if code == 14 do + if shell.ctrl do + return "\x7f" + end + + return "\x08" + end # backspace (ctrl: word erase) if code < 2 return :nil if code >= KEYMAP:len() return :nil const character = if shell.shift KEYMAP_SHIFT[code] else KEYMAP[code] diff --git a/kernel/main.rv b/kernel/main.rv index 3363bde..67850d5 100644 --- a/kernel/main.rv +++ b/kernel/main.rv @@ -15,6 +15,10 @@ if (disk_sectors() != 0 and fs.ready == :false) do seed_fs() end +if fs:size("/etc/profile") >= 0 do + run_script_path("/etc/profile", "profile", make_ctx("", { "" })) +end + # lung will be happy const default_face = load_ttf("/fonts/terminess.ttf", 32) if default_face != :nil do diff --git a/kernel/seed.rv b/kernel/seed.rv index 058b57a..cbb96e0 100644 --- a/kernel/seed.rv +++ b/kernel/seed.rv @@ -23,8 +23,16 @@ fn seed_fs() do fs:mkdir("/home") fs:store("/etc/motd", "welcome to revel") - fs:store("/home/balls.txt", "its lowk insane how big my balls are") - fs:store("/home/i have big balls.txt", "dude my balls are actually massive") + fs:store("/etc/profile", """ + # /etc/profile runs once at shell startup, keep it quick + + if fs:size("/etc/motd") > 0 do + puts(fs:read_bytes("/etc/motd", 0, motd_size)) + puts("\n") + end + """) + fs:store("/home/balls.txt", "its lowk insane how big my balls are lol jk\n\t- hachem") + fs:store("/home/i have big balls.txt", "dude my balls are actually massive lol jk\n\t- hachem") seed_bin() seed_fonts() diff --git a/kernel/tty.rv b/kernel/tty.rv index 5fcd224..680289f 100644 --- a/kernel/tty.rv +++ b/kernel/tty.rv @@ -137,7 +137,8 @@ global tty = { if byte == 27 do self.esc = 1 return :nil end # ESC, start a sequence if byte == 10 do self:submit() return :nil end # enter - if byte == 8 or byte == 127 do self:backspace() return :nil end # bs / del + if byte == 8 do self:backspace() return :nil end # bs + if byte == 127 do self:erase_word() return :nil end # del (ctrl-bs): erase word if byte == 1 do self:home() return :nil end # ctrl-a if byte == 5 do self:to_end() return :nil end # ctrl-e if byte == 3 do self:cancel() return :nil end # ctrl-c -- cgit v1.3 From ae21ae98f2945a6a6786939e0d77e9e5a535405c Mon Sep 17 00:00:00 2001 From: lung Date: Thu, 8 Oct 2026 21:36:31 +0300 Subject: cooler ed.rv --- README.md | 6 +- boot/heap.zig | 2 +- gils/cat.rv | 6 +- gils/ed.rv | 363 +++++++++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 307 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 7d75caf..ce90856 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ your motd lies in `/etc/motd` and the profile script in `/etc/profile` `ed` is your only text editor\ it's cooler than gnu ed. type `h` to see help\ -you also get a `!` command for free: it will run your file, saved or unsaved, as a revo script +you also get some revo-specific commands, for free!\ +- `x` to execute and show the result +- `!` to execute +- `v` to check whether a file is valid +- `o` to get an outline # License MIT, see [LICENSE](LICENSE). diff --git a/boot/heap.zig b/boot/heap.zig index b232585..ff8bfad 100644 --- a/boot/heap.zig +++ b/boot/heap.zig @@ -1,4 +1,4 @@ -// freelist allocator type shit +//! freelist allocator type shit const std = @import("std"); const pmm = @import("pmm.zig"); diff --git a/gils/cat.rv b/gils/cat.rv index 220ce93..05ab418 100644 --- a/gils/cat.rv +++ b/gils/cat.rv @@ -2,8 +2,10 @@ const context = env.ctx const parsed = arg_name(context.rest, 0) -if parsed.name:len() == 0 do puts("usage: cat \n") end -else do + +if parsed.name:len() == 0 do + puts("usage: cat \n") +end else do const path = expand_path(parsed.name) if vfs_owns(path) do vfs_cat(path) end else do fs:cat(path) end diff --git a/gils/ed.rv b/gils/ed.rv index 7f1d849..023ce15 100644 --- a/gils/ed.rv +++ b/gils/ed.rv @@ -1,89 +1,157 @@ # simple ass modal ass line ass editor -fn list_insert(items, index, value) do +#* printed by the h command *# +const ed_usage = """ + ed, the only good and standard text editor on earth + + addresses: . current | $ last | , or % all | n nth line + p print | n numbered | a append | i insert | c change | d delete + w [file] write | q quit | = number | h help | o outline | empty prints next + ! [file] run buffer or file | low,high! run range | v check | x eval and show + a/i/c take lines until a lone . + """ + +fn list_insert(items, index, value) -> table do let result = {} - for position in 0..index do result:push(items[position]) end + + for position in 0..index do + result:push(items[position]) + end + result:push(value) - for position in index..items:len() do result:push(items[position]) end + + for position in index..items:len() do + result:push(items[position]) + end + return result end -fn list_delete(items, low, high) do +fn list_delete(items, low, high) -> table do let result = {} - for position in 0..items:len() do if (position < low or position > high) result:push(items[position]) end + + for position in 0..items:len() do + if (position < low or position > high) result:push(items[position]) + end + return result end -fn ed_is_digit(character) do +fn ed_is_digit(character) -> bool do const code = character:ascii() + return code >= 48 and code <= 57 end -# parse one address at index -fn ed_addr(state, line, index) do - if index >= line:len() return { value = -1, next = index } - if line[index] == "." return { value = state.dot, next = index + 1 } - if line[index] == "$" return { value = state.lines:len() - 1, next = index + 1 } +#* parse one address at index *# +fn ed_addr(state, line, index) -> { value: num, next: num } do + if index >= line:len() do + return { value = -1, next = index } + end + + if line[index] == "." do + return { value = state.dot, next = index + 1 } + end + + if line[index] == "$" do + return { value = state.lines:len() - 1, next = index + 1 } + end + if ed_is_digit(line[index]) do let number = 0 let scan = index - while (scan < line:len() and ed_is_digit(line[scan])) do number = number * 10 + (line[scan]:ascii() - 48) scan += 1 end + + while (scan < line:len() and ed_is_digit(line[scan])) do + number = number * 10 + (line[scan]:ascii() - 48) + scan += 1 + end + return { value = number - 1, next = scan } end + return { value = -1, next = index } end -# split a command line into low, high and command -fn ed_parse(state, line) do +#* split a command line into low, high and command *# +fn ed_parse(state, line) -> { low: num, high: num, command: string } do let index = 0 let low = -1 let high = -1 - if (line:len() > 0 and (line[0] == "," or line[0] == "%")) do + + if line:len() > 0 and (line[0] == "," or line[0] == "%") do low = 0 high = state.lines:len() - 1 index = 1 - end - else do + end else do const first_addr = ed_addr(state, line, 0) - if first_addr.value >= 0 do low = first_addr.value index = first_addr.next end - if (index < line:len() and line[index] == ",") do + + if first_addr.value >= 0 do + low = first_addr.value + index = first_addr.next + end + + if index < line:len() and line[index] == "," do index += 1 + const second_addr = ed_addr(state, line, index) - if second_addr.value >= 0 do high = second_addr.value index = second_addr.next end - else do high = state.lines:len() - 1 end + + if second_addr.value >= 0 do + high = second_addr.value + index = second_addr.next + end else do + high = state.lines:len() - 1 + end end end - if low < 0 do low = state.dot end - if high < 0 do high = low end - const command = if index < line:len() line[index..] else "" + + if (low < 0) low = state.dot + if (high < 0) high = low + + const command = if (index < line:len()) line[index..] else "" + return { low = low, high = high, command = command } end fn ed_print(state, low, high, numbered) do let index = low - while index <= high do - if (index >= 0 and index < state.lines:len()) do - if numbered do puts("#{index + 1}\t#{state.lines[index]}\n") end - else do puts("#{state.lines[index]}\n") end + + while (index <= high) do + if index >= 0 and index < state.lines:len() do + if numbered do + # todo make the a \t once it renders right + puts("#{index + 1} #{state.lines[index]}\n") + end else do + puts("#{state.lines[index]}\n") + end end + index += 1 end - if (high >= 0 and high < state.lines:len()) do state.dot = high end + + if (high >= 0 and high < state.lines:len()) state.dot = high end fn ed_goto(state, low) do - if state.lines:len() == 0 do puts("?\n") return :nil end + if state.lines:len() == 0 do + puts("?\n") + return :nil + end + let target = low - if target < 0 do target = 0 end - if target >= state.lines:len() do target = state.lines:len() - 1 end + + if (target < 0) target = 0 + if (target >= state.lines:len()) target = state.lines:len() - 1 + state.dot = target puts("#{state.lines[target]}\n") end fn ed_begin_input(state, position) do let clamped = position - if clamped < 0 do clamped = 0 end - if clamped > state.lines:len() do clamped = state.lines:len() end + + if (clamped < 0) clamped = 0 + if (clamped > state.lines:len()) clamped = state.lines:len() + state.input = :true state.insert_at = clamped end @@ -91,9 +159,12 @@ end fn ed_delete(state, low, high) do state.lines = list_delete(state.lines, low, high) state.dirty = :true + let new_dot = low - if new_dot >= state.lines:len() do new_dot = state.lines:len() - 1 end - if new_dot < 0 do new_dot = 0 end + + if (new_dot >= state.lines:len()) new_dot = state.lines:len() - 1 + if (new_dot < 0) new_dot = 0 + state.dot = new_dot end @@ -102,20 +173,44 @@ fn ed_change(state, low, high) do ed_begin_input(state, low) end -fn ed_join(items) do +fn ed_join(items) -> string do let result = "" - for index in 0..items:len() do result = result ~ items[index] ~ "\n" end + + for index in 0..items:len() do + result = result ~ items[index] ~ "\n" + end + return result end +fn ed_range_text(state, low, high) -> string do + let range = {} + + for index in low..(high + 1) do + if index >= 0 and index < state.lines:len() do + range:push(state.lines[index]) + end + end + + return ed_join(range) +end + fn ed_write(state, command) do let path = state.path + if command:len() > 1 do const filename = command[1..]:trim() - if filename:len() > 0 do path = expand_path(filename) end + + if (filename:len() > 0) path = expand_path(filename) end - if path:len() == 0 do puts("?\n") return :nil end + + if path:len() == 0 do + puts("?\n") + return :nil + end + const contents = ed_join(state.lines) + fs:store(path, contents) state.path = path state.dirty = :false @@ -127,64 +222,200 @@ fn ed_quit(state) do puts(prompt()) end +fn ed_run_file(state, line, parsed) do + if line:len() > 0 and line[0] != "!" do + const text = ed_range_text(state, parsed.low, parsed.high) + + if text:len() == 0 do + puts("?\n") + return :nil + end + + env.ctx = make_ctx("", {""}) + + const error_msg = eval_script(text) + + if error_msg != :nil do + puts("!: #{error_msg}\n") + end + + return :nil + end + + let rest = "" + + if parsed.command:len() > 1 do + rest = parsed.command[1..]:trim() + end + + if rest:len() == 0 do + if state.lines:len() == 0 do + puts("?\n") + return :nil + end + + env.ctx = make_ctx("", {""}) + + const error_msg = eval_script(ed_join(state.lines)) + + if error_msg != :nil do + puts("!: #{error_msg}\n") + end + + return :nil + end + + const path = expand_path(rest:split(" ")[0]) + + if fs:size(path) < 0 do + puts("?\n") + return :nil + end + + run_script_path(path, path, make_ctx(rest, rest:split(" "))) +end + +#* check the buffer (or range) without running it *# +fn ed_check(state, line, parsed) do + let text = ed_join(state.lines) + + if line:len() > 0 and line[0] != "v" do + text = ed_range_text(state, parsed.low, parsed.high) + end + + if text:len() == 0 do + puts("?\n") + return :nil + end + + match revo.compile(text) + | {:ok, _} => puts("ok\n") + | {:err, e} => puts("#{e}\n") +end + +#* eval and show the value, like the repl does *# +fn ed_eval_show(state, line, parsed) do + let text = ed_join(state.lines) + + if line:len() > 0 and line[0] != "x" do + text = ed_range_text(state, parsed.low, parsed.high) + end + + if text:len() == 0 do + puts("?\n") + return :nil + end + + env.ctx = make_ctx("", {""}) + + match revo.eval(text) + | {:ok, v} => puts("#{v}\n") + | {:err, e} => puts("#{e}\n") +end + +#* list fn and global definitions with line numbers *# +fn ed_outline(state) do + for index in 0..state.lines:len() do + const text = state.lines[index]:trim() + + if text:starts_with?("fn ") or text:starts_with?("global ") do + puts("#{index + 1}\t#{text}\n") + end + end +end + global ed_state = { active = :false, input = :false, path = "", lines = {}, dot = 0, insert_at = 0, dirty = :false } -fn gils_ed(context) do +#* open the editor, takes over the shell until quit *# +fn gils_ed(context) -> :hold do ed_state.active = :true ed_state.input = :false ed_state.dirty = :false ed_state.lines = {} - ed_state.path = if context.argv:len() > 1 expand_path(context.argv[1]) else "" + ed_state.path = if (context.argv:len() > 1) + expand_path(context.argv[1]) + else "" + if ed_state.path:len() > 0 do const size = fs:size(ed_state.path) + if size > 0 do const contents = fs:read_bytes(ed_state.path, 0, size) const raw_lines = contents:split("\n") + for index in 0..raw_lines:len() do - if (index < raw_lines:len() - 1 or raw_lines[index]:len() > 0) do ed_state.lines:push(raw_lines[index]) end + if index < raw_lines:len() - 1 or raw_lines[index]:len() > 0 do + ed_state.lines:push(raw_lines[index]) + end end + puts("#{size}\n") + end else do + puts("#{ed_state.path}: new file\n") end - else do puts("#{ed_state.path}: new file\n") end end - ed_state.dot = if ed_state.lines:len() > 0 ed_state.lines:len() - 1 else 0 + + ed_state.dot = + if (ed_state.lines:len() > 0) + ed_state.lines:len() - 1 + else 0 + return :hold end fn gils_ed_feed(line) do const state = ed_state + if state.input do - if line == "." do state.input = :false end - else do + if line == "." do + state.input = :false + end else do state.lines = list_insert(state.lines, state.insert_at, line) state.insert_at += 1 state.dot = state.insert_at - 1 state.dirty = :true end + return :nil end + if line:len() == 0 do - if state.lines:len() == 0 do puts("?\n") return :nil end - if state.dot < state.lines:len() - 1 do state.dot += 1 end + if state.lines:len() == 0 do + puts("?\n") + return :nil + end + + if (state.dot < state.lines:len() - 1) state.dot += 1 + puts("#{state.lines[state.dot]}\n") + return :nil end + const parsed = ed_parse(state, line) - const command_char = if parsed.command:len() > 0 parsed.command[0] else "" + const command_char = if (parsed.command:len() > 0) parsed.command[0] else "" + match command_char - | "p" => ed_print(state, parsed.low, parsed.high, :false) - | "n" => ed_print(state, parsed.low, parsed.high, :true) - | "a" => ed_begin_input(state, parsed.low + 1) - | "i" => ed_begin_input(state, parsed.low) - | "c" => ed_change(state, parsed.low, parsed.high) - | "d" => ed_delete(state, parsed.low, parsed.high) - | "w" => ed_write(state, parsed.command) - | "q" => ed_quit(state) - | "=" => puts("#{state.dot + 1}\n") - | "" => ed_goto(state, parsed.low) - | _ => puts("?\n") -end - -fn gils_ed_active() ed_state.active - -register("ed [path]", "gils", "the standard editor (a i d p w q)", gils_ed) + | "p" => ed_print(state, parsed.low, parsed.high, numbered = :false) + | "n" => ed_print(state, parsed.low, parsed.high, numbered = :true) + | "a" => ed_begin_input(state, parsed.low + 1) + | "i" => ed_begin_input(state, parsed.low) + | "c" => ed_change(state, parsed.low, parsed.high) + | "d" => ed_delete(state, parsed.low, parsed.high) + | "w" => ed_write(state, parsed.command) + | "!" => ed_run_file(state, line, parsed) + | "v" => ed_check(state, line, parsed) + | "x" => ed_eval_show(state, line, parsed) + | "q" => ed_quit(state) + | "h" => puts(ed_usage ~ "\n") + | "o" => ed_outline(state) + | "=" => puts("#{state.dot + 1}\n") + | "" => ed_goto(state, parsed.low) + | _ => puts("?\n") +end + +fn gils_ed_active() -> bool do + return ed_state.active +end + +register("ed [path]", "gils", "the editor (type h for help)", gils_ed) -- cgit v1.3