From e55291a32a8b3f7b5c105cd79e86f3c19f19b3d4 Mon Sep 17 00:00:00 2001 From: hachem Date: Wed, 7 Oct 2026 21:43:38 +0200 Subject: feat: tty and ttf, you'd think somethn starting with tt would be fun to do... --- Makefile | 1 - boot/boot.zig | 38 +++++++ boot/fonts.zig | 11 ++ boot/heap.zig | 46 ++++++--- boot/limine.conf | 1 + bridge/bridge.zig | 2 + bridge/hosts.zig | 32 ++++++ build.zig | 14 +++ fonts/acorn8.psf | Bin 0 -> 2052 bytes fonts/dejavu.ttf | Bin 0 -> 8488 bytes fonts/pearl8.psf | Bin 0 -> 2052 bytes fonts/sun16.psf | Bin 0 -> 4100 bytes fonts/terminess.ttf | Bin 0 -> 9496 bytes fonts/vga16.psf | Bin 0 -> 4100 bytes fonts/vga8.psf | Bin 0 -> 2052 bytes gils/ed.rv | 2 +- gils/gils.rv | 4 - kernel/console.rv | 79 +++++++++++++-- kernel/fs.rv | 16 +++ kernel/input.rv | 23 ++--- kernel/main.rv | 7 ++ kernel/seed.rv | 8 ++ kernel/shell.rv | 156 ++++++++++++++++++++++++++-- kernel/ttf.rv | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++++ kernel/tty.rv | 151 +++++++++++++++++++++++++++ tools/genfonts.py | 41 ++++++++ tools/subset-ttf.sh | 13 +++ 27 files changed, 883 insertions(+), 48 deletions(-) create mode 100644 boot/fonts.zig create mode 100644 fonts/acorn8.psf create mode 100644 fonts/dejavu.ttf create mode 100644 fonts/pearl8.psf create mode 100644 fonts/sun16.psf create mode 100644 fonts/terminess.ttf create mode 100644 fonts/vga16.psf create mode 100644 fonts/vga8.psf create mode 100644 kernel/ttf.rv create mode 100644 kernel/tty.rv create mode 100644 tools/genfonts.py create mode 100755 tools/subset-ttf.sh diff --git a/Makefile b/Makefile index 5b44c0b..6560bd9 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,6 @@ kernel: limine/limine: $(MAKE) -C limine - iso: kernel limine/limine rm -rf iso_root mkdir -p iso_root/boot/limine iso_root/EFI/BOOT diff --git a/boot/boot.zig b/boot/boot.zig index 4918881..267fc3f 100644 --- a/boot/boot.zig +++ b/boot/boot.zig @@ -9,6 +9,7 @@ const heap = @import("heap.zig"); const vmm = @import("vmm.zig"); const user = @import("user.zig"); const elf = @import("elf.zig"); +const fontdata = @import("fonts.zig"); const keyboard = @import("keyboard.zig"); const timer = @import("timer.zig"); const sched = @import("sched.zig"); @@ -160,6 +161,38 @@ fn kTaskMax() u64 { return sched.taskMax(); } +// i dont actually like implementing fonts in any project +// it be revel or any opengl or vulkan or whatever the fuck +// fonts are always the ugh parts of a projects +// just so much boilerplate +const FontBlob = struct { name: []const u8, phys: u64, size: u64 }; +var g_fonts: [fontdata.fonts.len]FontBlob = undefined; + +fn loadFonts() void { + for (fontdata.fonts, 0..) |f, i| { + const pages = (f.bytes.len + 4095) / 4096; + const phys = pmm.allocContig(pages) orelse @panic("no room for an embedded font"); + @memcpy(@as([*]u8, @ptrFromInt(pmm.physToVirt(phys)))[0..f.bytes.len], f.bytes); + g_fonts[i] = .{ .name = f.name, .phys = phys, .size = f.bytes.len }; + } +} + +fn kFontCount() u64 { + return g_fonts.len; +} + +fn kFontPhys(i: u64) u64 { + return g_fonts[i].phys; +} + +fn kFontName(i: u64) []const u8 { + return g_fonts[i].name; +} + +fn kFontSize(i: u64) u64 { + return g_fonts[i].size; +} + fn kTaskState(i: u64) u64 { return sched.taskState(i); } @@ -206,6 +239,7 @@ export fn _start() callconv(.c) noreturn { ata.init(); loadPrograms(); + loadFonts(); // the event loop below is scheduler task 0 (the kernel), running in the // current cr3. user tasks spawned by `run` get time fucked against it sched.init(vmm.activePml4()); @@ -236,6 +270,10 @@ export fn _start() callconv(.c) noreturn { .disk_read = &kDiskRead, .disk_write = &kDiskWrite, .alloc_contig = &kAllocContig, + .font_count = &kFontCount, + .font_phys = &kFontPhys, + .font_name = &kFontName, + .font_size = &kFontSize, }; // hand the kernel heap + framebuffer + low level ops to revo and bring up the vm diff --git a/boot/fonts.zig b/boot/fonts.zig new file mode 100644 index 0000000..5db8123 --- /dev/null +++ b/boot/fonts.zig @@ -0,0 +1,11 @@ +// js loading fonts from host disk +pub const Font = struct { name: []const u8, bytes: []const u8 }; +pub const fonts = [_]Font{ + .{ .name = "vga16.psf", .bytes = @embedFile("font_vga16") }, + .{ .name = "vga8.psf", .bytes = @embedFile("font_vga8") }, + .{ .name = "sun16.psf", .bytes = @embedFile("font_sun16") }, + .{ .name = "acorn8.psf", .bytes = @embedFile("font_acorn8") }, + .{ .name = "pearl8.psf", .bytes = @embedFile("font_pearl8") }, + .{ .name = "dejavu.ttf", .bytes = @embedFile("font_dejavu") }, + .{ .name = "terminess.ttf", .bytes = @embedFile("font_terminess") }, +}; diff --git a/boot/heap.zig b/boot/heap.zig index badf506..b232585 100644 --- a/boot/heap.zig +++ b/boot/heap.zig @@ -11,6 +11,9 @@ const MIN_BLOCK: usize = 32; // header + a usable scrap, dont split below this var heap_start: usize = 0; var heap_end: usize = 0; +// next fit rover optimization. essentially its resume scanning where hte last +// alloc landed instead of heap_start everytime. it was O(n) before +var rover: usize = 0; inline fn word(b: usize) *usize { return @ptrFromInt(b); @@ -48,25 +51,38 @@ fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) if (alignment.toByteUnits() > ALIGN) return null; const need = alignUp(HEADER + len, ALIGN); - var b = heap_start; - while (b < heap_end) { - if (!blkUsed(b)) { - // lazy coalesce - var size = blkSize(b); - while (b + size < heap_end and !blkUsed(b + size)) size += blkSize(b + size); - setBlk(b, size, false); + if (rover < heap_start or rover >= heap_end) rover = heap_start; - if (size >= need) { - if (size >= need + MIN_BLOCK) { - setBlk(b, need, true); - setBlk(b + need, size - need, false); // the leftover tail - } else { - setBlk(b, size, true); + // next fit bull shit + var b = rover; + var limit = heap_end; + var pass: u8 = 0; + while (pass < 2) : (pass += 1) { + while (b < limit) { + if (!blkUsed(b)) { + // lazy coalesce + var size = blkSize(b); + while (b + size < heap_end and !blkUsed(b + size)) size += blkSize(b + size); + setBlk(b, size, false); + + if (size >= need) { + if (size >= need + MIN_BLOCK) { + setBlk(b, need, true); + setBlk(b + need, size - need, false); // the leftover tail + rover = b + need; + } else { + setBlk(b, size, true); + rover = b + size; + } + if (rover >= heap_end) rover = heap_start; + return @ptrFromInt(b + HEADER); } - return @ptrFromInt(b + HEADER); } + b += blkSize(b); } - b += blkSize(b); + // wrap + b = heap_start; + limit = rover; } return null; } diff --git a/boot/limine.conf b/boot/limine.conf index 7398dca..ffeb3b5 100644 --- a/boot/limine.conf +++ b/boot/limine.conf @@ -2,4 +2,5 @@ timeout: 0 /Revel protocol: limine + resolution: 1920x1080x32 path: boot():/boot/revel diff --git a/bridge/bridge.zig b/bridge/bridge.zig index 87548eb..56cfc7f 100644 --- a/bridge/bridge.zig +++ b/bridge/bridge.zig @@ -29,7 +29,9 @@ const init_program = blk: { for (gils_embedded) |g| s = s ++ @embedFile("k_gils_" ++ g) ++ "\n"; s = s ++ @embedFile("k_proc") ++ "\n" ++ + @embedFile("k_ttf") ++ "\n" ++ @embedFile("k_shell") ++ "\n" ++ + @embedFile("k_tty") ++ "\n" ++ @embedFile("k_input") ++ "\n" ++ @embedFile("k_main"); break :blk s; diff --git a/bridge/hosts.zig b/bridge/hosts.zig index 51362dc..5f401aa 100644 --- a/bridge/hosts.zig +++ b/bridge/hosts.zig @@ -67,6 +67,11 @@ pub const KernelOps = struct { disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail alloc_contig: *const fn (u64) u64, // n contiguous pages -> phys base, 0 on fail + + font_count: *const fn () u64, + font_phys: *const fn (u64) u64, + font_name: *const fn (u64) []const u8, + font_size: *const fn (u64) u64, }; var g_kops: ?KernelOps = null; @@ -280,6 +285,29 @@ fn hostProgSize(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { return HostResult.data(Data.new.num(kops().prog_size(i))); } +// pass shit to revo +fn hostFontCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().font_count())); +} + +// pass shit to revo +fn hostFontPhys(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("font_phys: bad index"); + return HostResult.data(Data.new.num(kops().font_phys(i))); +} + +// pass shit to revo +fn hostFontName(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("font_name: bad index"); + return HostResult.data(try vm.ownValueString(kops().font_name(i))); +} + +// pass shit to revo +fn hostFontSize(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("font_size: bad index"); + return HostResult.data(Data.new.num(kops().font_size(i))); +} + // pass shit to revo fn hostTaskMax(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { return HostResult.data(Data.new.num(kops().task_max())); @@ -436,6 +464,10 @@ pub fn passShitToRevo(vm: *revo.VM) !void { try vm.registerGlobal("prog_phys", try vm.installHost("prog_phys", define(&[_]T{.number}, hostProgPhys))); try vm.registerGlobal("prog_name", try vm.installHost("prog_name", define(&[_]T{.number}, hostProgName))); try vm.registerGlobal("prog_size", try vm.installHost("prog_size", define(&[_]T{.number}, hostProgSize))); + try vm.registerGlobal("font_count", try vm.installHost("font_count", define(&[_]T{}, hostFontCount))); + try vm.registerGlobal("font_phys", try vm.installHost("font_phys", define(&[_]T{.number}, hostFontPhys))); + try vm.registerGlobal("font_name", try vm.installHost("font_name", define(&[_]T{.number}, hostFontName))); + try vm.registerGlobal("font_size", try vm.installHost("font_size", define(&[_]T{.number}, hostFontSize))); try vm.registerGlobal("task_max", try vm.installHost("task_max", define(&[_]T{}, hostTaskMax))); try vm.registerGlobal("task_state", try vm.installHost("task_state", define(&[_]T{.number}, hostTaskState))); try vm.registerGlobal("memmap_count", try vm.installHost("memmap_count", define(&[_]T{}, hostMemmapCount))); diff --git a/build.zig b/build.zig index d55961f..469d513 100644 --- a/build.zig +++ b/build.zig @@ -70,7 +70,9 @@ pub fn build(b: *std.Build) void { .{ "k_vfs", "kernel/vfs.rv" }, .{ "k_seed", "kernel/seed.rv" }, .{ "k_proc", "kernel/proc.rv" }, + .{ "k_ttf", "kernel/ttf.rv" }, .{ "k_shell", "kernel/shell.rv" }, + .{ "k_tty", "kernel/tty.rv" }, .{ "k_input", "kernel/input.rv" }, .{ "k_main", "kernel/main.rv" }, }) |e| { @@ -84,6 +86,18 @@ pub fn build(b: *std.Build) void { kernel.root_module.addImport("bridge", bridge_mod); + inline for (.{ + .{ "font_vga16", "fonts/vga16.psf" }, + .{ "font_vga8", "fonts/vga8.psf" }, + .{ "font_sun16", "fonts/sun16.psf" }, + .{ "font_acorn8", "fonts/acorn8.psf" }, + .{ "font_pearl8", "fonts/pearl8.psf" }, + .{ "font_dejavu", "fonts/dejavu.ttf" }, + .{ "font_terminess", "fonts/terminess.ttf" }, + }) |e| { + kernel.root_module.addAnonymousImport(e[0], .{ .root_source_file = b.path(e[1]) }); + } + inline for (.{ "hexview", "calc", "primes", "save" }) |prog| { const cc = b.addSystemCommand(&.{ "zig", "cc", diff --git a/fonts/acorn8.psf b/fonts/acorn8.psf new file mode 100644 index 0000000..57bc46b Binary files /dev/null and b/fonts/acorn8.psf differ diff --git a/fonts/dejavu.ttf b/fonts/dejavu.ttf new file mode 100644 index 0000000..597b2e0 Binary files /dev/null and b/fonts/dejavu.ttf differ diff --git a/fonts/pearl8.psf b/fonts/pearl8.psf new file mode 100644 index 0000000..acf7939 Binary files /dev/null and b/fonts/pearl8.psf differ diff --git a/fonts/sun16.psf b/fonts/sun16.psf new file mode 100644 index 0000000..8042ba3 Binary files /dev/null and b/fonts/sun16.psf differ diff --git a/fonts/terminess.ttf b/fonts/terminess.ttf new file mode 100644 index 0000000..0055a2c Binary files /dev/null and b/fonts/terminess.ttf differ diff --git a/fonts/vga16.psf b/fonts/vga16.psf new file mode 100644 index 0000000..652b07b Binary files /dev/null and b/fonts/vga16.psf differ diff --git a/fonts/vga8.psf b/fonts/vga8.psf new file mode 100644 index 0000000..3fcce09 Binary files /dev/null and b/fonts/vga8.psf differ diff --git a/gils/ed.rv b/gils/ed.rv index 00ff50a..2572ba3 100644 --- a/gils/ed.rv +++ b/gils/ed.rv @@ -124,7 +124,7 @@ end fn ed_quit(state) do state.active = :false - puts("% ") + puts(prompt()) end global ed_state = { active = :false, input = :false, path = "", lines = {}, dot = 0, insert_at = 0, dirty = :false } diff --git a/gils/gils.rv b/gils/gils.rv index 1a61cc5..afd0bbd 100644 --- a/gils/gils.rv +++ b/gils/gils.rv @@ -24,10 +24,6 @@ end # scripts read their args off env.ctx, stashed here before eval global env = { ctx = :nil } -# working dir + path resolver, shared by every gils tool and the shell. lives -# here so its declared before anything that uses it gets concatenated -global wd = { path = "/" } - global abspath = fn(arg) do if arg:len() == 0 return wd.path let path = arg diff --git a/kernel/console.rv b/kernel/console.rv index 7948a74..3bff6de 100644 --- a/kernel/console.rv +++ b/kernel/console.rv @@ -4,15 +4,78 @@ const FG_COLOR = 0xd0d0d0 const BG_COLOR = 0x101018 const CURSOR_COLOR = 0x50fa7b +global wd = { path = "/" } +global prompt = fn() "revel:#{wd.path}$ " + +# runtime font bs +fn embolden(byte, row) do return (byte bor (byte shl 1)) band 0xFF end +fn slant(byte, row) do return (byte shl ((FONT_HEIGHT - 1 - row) shr 2)) band 0xFF end + +fn remap_glyphs(transform) do + let out = {} + for glyph_index in 0..FONT:len() do + const glyph = FONT[glyph_index] + let rows = {} + for row_index in 0..FONT_HEIGHT do rows:push(transform(glyph[row_index], row_index)) end + out:push(rows) + end + return out +end + +global reverse8 = fn(byte) do + let out = 0 + for bit in 0..8 do + if (byte shr bit) band 1 == 1 do out = out bor (1 shl (7 - bit)) end + end + return out +end + +global fonts = {} +global add_font = fn(name, glyphs, width, height, kind, path, size) do + for index in 0..fonts:len() do + if fonts[index].name == name do + fonts[index].glyphs = glyphs + fonts[index].width = width + fonts[index].height = height + fonts[index].kind = kind + fonts[index].path = path + fonts[index].size = size + return :nil + end + end + fonts:push({ name = name, glyphs = glyphs, width = width, height = height, kind = kind, path = path, size = size }) +end +global find_font = fn(name) do + for index in 0..fonts:len() do + if fonts[index].name == name return fonts[index] + end + return :nil +end + +add_font("vga", FONT, FONT_WIDTH, FONT_HEIGHT, :bitmap, :nil, 0) +add_font("bold", remap_glyphs(embolden), FONT_WIDTH, FONT_HEIGHT, :bitmap, :nil, 0) +add_font("italic", remap_glyphs(slant), FONT_WIDTH, FONT_HEIGHT, :bitmap, :nil, 0) +add_font("bolditalic", remap_glyphs(fn(byte, row) do return slant(embolden(byte, row), row) end), FONT_WIDTH, FONT_HEIGHT, :bitmap, :nil, 0) + global console = { - column = 0, row = 0, scale = 1, shown = :false, cursor_x = 0, cursor_y = 0, blink = 0, + column = 0, row = 0, scale = 1, shown = :false, cursor_x = 0, cursor_y = 0, blink = 0, font = FONT, font_width = FONT_WIDTH, font_height = FONT_HEIGHT, font_entry = find_font("vga"), - # cell size in pixels at the current zoom - cell_width = fn(self) FONT_WIDTH * self.scale, - cell_height = fn(self) FONT_HEIGHT * self.scale, + # this is now font dependent + cell_width = fn(self) self.font_width * self.scale, + cell_height = fn(self) self.font_height * self.scale, columns = fn(self) fb_width() / self:cell_width(), rows = fn(self) fb_height() / self:cell_height(), + set_font = fn(self, entry) do + self.font = entry.glyphs + self.font_width = entry.width + self.font_height = entry.height + self.font_entry = entry + # a ttf renders at its true pixel size so we js bitmap zoom scale + if entry.kind == :ttf do self.scale = 1 end + self:clear() + end, + draw_glyph = fn(self, column, row, character) do const origin_x = column * self:cell_width() const origin_y = row * self:cell_height() @@ -20,10 +83,10 @@ global console = { const code = character:ascii() if code < 32 return :nil if code > 127 return :nil - const glyph = FONT[code - 32] - for glyph_row in 0..FONT_HEIGHT do + const glyph = self.font[code - 32] + for glyph_row in 0..self.font_height do const bits = glyph[glyph_row] - for glyph_col in 0..FONT_WIDTH do + for glyph_col in 0..self.font_width do if (bits shr glyph_col) band 1 == 1 fill_rect(origin_x + glyph_col * self.scale, origin_y + glyph_row * self.scale, self.scale, self.scale, FG_COLOR) end end @@ -102,7 +165,7 @@ global on_tick = fn(ticks) do end if proc_exited() do console:cursor_clear() - puts("% ") + puts(prompt()) console:drain() console.blink = 0 console:cursor_show() diff --git a/kernel/fs.rv b/kernel/fs.rv index 6229fd5..255797f 100644 --- a/kernel/fs.rv +++ b/kernel/fs.rv @@ -330,6 +330,22 @@ global fs = { return result end, + read_into = fn(self, path, dest) do + const entry = self:resolve(path) + if entry == :nil or entry.type == T_DIR return 0 + 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 const entry = self:resolve(path) diff --git a/kernel/input.rv b/kernel/input.rv index 26ce545..ed0f4f7 100644 --- a/kernel/input.rv +++ b/kernel/input.rv @@ -25,11 +25,13 @@ end # she stroke my keys till i handle # no fr this is just a key stroke handler -global shell = { line = "", shift = :false, extended = :false } +global shell = { shift = :false, extended = :false, ctrl = :false } fn decode_key(code) do if code == 0x2a or code == 0x36 do shell.shift = :true return :nil end # shift down if code == 0xaa or code == 0xb6 do shell.shift = :false return :nil end # shift up + if code == 0x1d do shell.ctrl = :true return :nil end # ctrl down + 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 @@ -37,7 +39,8 @@ fn decode_key(code) do if code >= KEYMAP:len() return :nil const character = if shell.shift KEYMAP_SHIFT[code] else KEYMAP[code] if character == " " and code != 0x39 return :nil # blank cells = modifiers/unmapped - character + if shell.ctrl return string.of_ascii(character:ascii() band 0x1f) + return character end global on_key = fn(code) do @@ -49,21 +52,15 @@ global on_key = fn(code) do else if code == 0x50 do final = 66 end # down else if code == 0x4d do final = 67 end # right else if code == 0x4b do final = 68 end # left + else if code == 0x47 do final = 72 end # home + else if code == 0x4f do final = 70 end # end if final != 0 do - stdin_deliver(27) - stdin_deliver(91) - stdin_deliver(final) + tty:feed(27) tty:feed(91) tty:feed(final) end return :nil end const character = decode_key(code) if character == :nil return :nil - if stdin_deliver(character:ascii()) return :nil - if character == "\n" do puts("\n") run(shell.line) shell.line = "" return :nil end - if character == "\x08" do - if shell.line:len() > 0 do shell.line = shell.line:sub(0, shell.line:len() - 1) puts("\x08 \x08") end - return :nil - end - shell.line ~= character - puts(character) + tty:feed(character:ascii()) end + diff --git a/kernel/main.rv b/kernel/main.rv index 568d695..3363bde 100644 --- a/kernel/main.rv +++ b/kernel/main.rv @@ -15,5 +15,12 @@ if (disk_sectors() != 0 and fs.ready == :false) do seed_fs() end +# lung will be happy +const default_face = load_ttf("/fonts/terminess.ttf", 32) +if default_face != :nil do + add_font("terminess", default_face.glyphs, default_face.width, default_face.height, :ttf, "/fonts/terminess.ttf", 32) + console:set_font(find_font("terminess")) +end + welcome() flush_console() diff --git a/kernel/seed.rv b/kernel/seed.rv index ad3f4e9..058b57a 100644 --- a/kernel/seed.rv +++ b/kernel/seed.rv @@ -10,6 +10,13 @@ fn seed_bin() do end end +fn seed_fonts() do + fs:mkdir("/fonts") + for index in 0..font_count() do + fs:store("/fonts/#{font_name(index)}", phys_to_str(font_phys(index), font_size(index))) + end +end + fn seed_fs() do fs:mkdir("/usr") fs:mkdir("/etc") @@ -20,4 +27,5 @@ fn seed_fs() do fs:store("/home/i have big balls.txt", "dude my balls are actually massive") seed_bin() + seed_fonts() end diff --git a/kernel/shell.rv b/kernel/shell.rv index 5d0a1c2..3e24767 100644 --- a/kernel/shell.rv +++ b/kernel/shell.rv @@ -6,7 +6,11 @@ fn pad(text, width) do return result end -fn help_row(label, description) puts(" #{pad(label, 20)}#{description}\n") +fn help_row(label, description) do + let col = pad(label, 22) + if label:len() >= 22 do col = label ~ " " end + puts(" #{col}#{description}\n") +end fn help_group(title, group_name) do puts("\n #{title}\n") @@ -32,6 +36,14 @@ fn cmd_help(context) do 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 + 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 + end puts("\n") end @@ -41,7 +53,8 @@ fn welcome() do puts("\n") puts(" kernel written in revo - by an alcoholic\n") puts(" #{(mem_total() shr 20)} MiB ram | #{fs_status} | #{prog_count()} programs\n") - puts(" type help to see what it does\n\n% ") + puts(" type help to see what it does\n\n") + puts(prompt()) end # global so /bin scripts can call it from a nested eval fiber @@ -84,6 +97,21 @@ fn wipe() console:clear() 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 + 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 + const face = load_ttf(entry.path, size) + 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 @@ -99,6 +127,119 @@ fn cmd_zoom(context) do else do puts("usage: zoom in | zoom out\n") end end +fn font_basename(path) do + let start = 0 + let index = 0 + 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 + return name +end + +fn load_psf(path) do + const size = fs:size(path) + if size < 4 return :nil + const data = fs:read_bytes(path, 0, size) + 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 + 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 + 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 + return :false +end + +fn ends_with(text, suffix) do + if text:len() < suffix:len() return :false + return text[(text:len() - suffix:len())..] == suffix +end + +fn str_to_int(text) 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 + end + return value +end + +fn font_file(name) do + if has_slash(name) do + const p = abspath(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 } +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 + 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 + 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 + 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 size == 0 do + const existing = find_font(name) + 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 + 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 + const parsed = load_psf(info.path) + 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 + const face = load_ttf(info.path, size) + 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 outb(0x70, cmos_reg) @@ -151,8 +292,9 @@ end fn run(line) 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("% ") 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]) @@ -160,13 +302,14 @@ fn run(line) 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("% ") end + if result != :hold do puts(prompt()) end return :nil end - if run_script(argv[0], context) == :true do puts("% ") 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("#{argv[0]}: command not found\n") + puts(prompt()) end register("help", "shell", "show this", cmd_help) @@ -176,6 +319,7 @@ register("mem", "shell", "free / total ram", cmd_mem) register("uptime", "shell", "seconds since boot", cmd_uptime) register("ps", "shell", "running processes", cmd_ps) register("zoom in|out", "shell", "scale the font", cmd_zoom) +register("font [name] [size]", "shell", "switch/load a console font (psf or ttf)", cmd_font) register("echo ", "shell", "print it back", cmd_echo) register("mkfs", "shell", "format the disk", cmd_mkfs) register("cd ", "gils", "change directory", cmd_cd) diff --git a/kernel/ttf.rv b/kernel/ttf.rv new file mode 100644 index 0000000..17d1a19 --- /dev/null +++ b/kernel/ttf.rv @@ -0,0 +1,286 @@ +# so many fucking hours wasted man i hate fonts so much +# fuck bezier +# fuck revo +# fuck revel +# fuck ttf +# fuck lung +# fuck me +# im like 90% confident this works, but im like 10% confident its fucked +fn ttf_mod(a, b) a - math.floor(a / b) * b + +fn ttf_u8(d, o) mem_read(d + o, 1) +fn ttf_u16(d, o) mem_read(d + o, 1) * 256 + mem_read(d + o + 1, 1) +fn ttf_i16(d, o) do + const v = ttf_u16(d, o) + if v >= 32768 return v - 65536 + return v +end +fn ttf_u32(d, o) ((mem_read(d + o, 1) * 256 + mem_read(d + o + 1, 1)) * 256 + mem_read(d + o + 2, 1)) * 256 + mem_read(d + o + 3, 1) + +fn ttf_tag_eq(d, o, tag) do + for i in 0..4 do if mem_read(d + o + i, 1) != tag[i]:ascii() return :false end + return :true +end + +fn ttf_table(d, tag) do + const n = ttf_u16(d, 4) + for i in 0..n do + const rec = 12 + i * 16 + if ttf_tag_eq(d, rec, tag) return ttf_u32(d, rec + 8) + end + return -1 +end + +fn ttf_cmap_sub(d, cmap_off) do + const n = ttf_u16(d, cmap_off + 2) + let best = -1 + for i in 0..n do + const rec = cmap_off + 4 + i * 8 + const plat = ttf_u16(d, rec) + const enc = ttf_u16(d, rec + 2) + if (plat == 3 and (enc == 1 or enc == 0)) or plat == 0 do best = cmap_off + ttf_u32(d, rec + 4) end + end + return best +end + +fn ttf_glyph_of(d, sub, c) do + if ttf_u16(d, sub) != 4 return 0 + const segCount = ttf_u16(d, sub + 6) / 2 + const endBase = sub + 14 + const startBase = endBase + segCount * 2 + 2 + const deltaBase = startBase + segCount * 2 + const rangeBase = deltaBase + segCount * 2 + for i in 0..segCount do + if c <= ttf_u16(d, endBase + i * 2) do + const startC = ttf_u16(d, startBase + i * 2) + if c < startC return 0 + const idro = ttf_u16(d, rangeBase + i * 2) + if idro == 0 return (c + ttf_i16(d, deltaBase + i * 2)) band 0xFFFF + const g = ttf_u16(d, rangeBase + i * 2 + idro + (c - startC) * 2) + if g == 0 return 0 + return (g + ttf_i16(d, deltaBase + i * 2)) band 0xFFFF + end + end + return 0 +end + +fn ttf_glyph_offset(d, loca, long, g) do + if long return ttf_u32(d, loca + g * 4) + return ttf_u16(d, loca + g * 2) * 2 +end + +fn ttf_advance(d, hmtx, numH, g) do + if g < numH return ttf_u16(d, hmtx + g * 4) + return ttf_u16(d, hmtx + (numH - 1) * 4) +end + +fn ttf_add_line(edges, x0, y0, x1, y1) edges:push({ ax = x0, ay = y0, bx = x1, by = y1 }) + +fn ttf_add_quad(edges, x0, y0, cx, cy, x1, y1, n) do + let px = x0 + let py = y0 + for i in 1..(n + 1) do + const t = i / n + const mt = 1 - t + const qx = mt * mt * x0 + 2 * mt * t * cx + t * t * x1 + const qy = mt * mt * y0 + 2 * mt * t * cy + t * t * y1 + ttf_add_line(edges, px, py, qx, qy) + px = qx + py = qy + end +end + +fn ttf_contour(pts, edges, nsub) do + const m = pts:len() + if m < 2 return :nil + let exp = {} + for i in 0..m do + exp:push(pts[i]) + const j = ttf_mod(i + 1, m) + if (pts[i].on == :false and pts[j].on == :false) do + exp:push({ x = (pts[i].x + pts[j].x) / 2, y = (pts[i].y + pts[j].y) / 2, on = :true }) + end + end + const n = exp:len() + let s = -1 + for i in 0..n do if (s < 0 and exp[i].on) do s = i end end + if s < 0 return :nil + let cx = exp[s].x + let cy = exp[s].y + let k = 1 + while k <= n do + const a = exp[ttf_mod(s + k, n)] + if a.on do + ttf_add_line(edges, cx, cy, a.x, a.y) + cx = a.x + cy = a.y + k += 1 + end + else do + const b = exp[ttf_mod(s + k + 1, n)] + ttf_add_quad(edges, cx, cy, a.x, a.y, b.x, b.y, nsub) + cx = b.x + cy = b.y + k += 2 + end + end +end + +# how about you contour my fucking asshole +fn ttf_glyph_edges(d, glyf, loca, long, g, scale, ox, baseline, edges, nsub) do + const off = ttf_glyph_offset(d, loca, long, g) + const next = ttf_glyph_offset(d, loca, long, g + 1) + if next <= off return :nil # empty glyph + const base = glyf + off + const nc = ttf_i16(d, base) + if nc < 0 return :nil # skip + let ends = {} + for i in 0..nc do ends:push(ttf_u16(d, base + 10 + i * 2)) end + const npts = ends[nc - 1] + 1 + let p = base + 10 + nc * 2 + p += 2 + ttf_u16(d, p) # skip instructions + let flags = {} + while flags:len() < npts do + const f = ttf_u8(d, p) + p += 1 + flags:push(f) + if (f band 0x08) != 0 do + let rep = ttf_u8(d, p) + p += 1 + while rep > 0 do flags:push(f) rep -= 1 end + end + end + let xs = {} + let x = 0 + for i in 0..npts do + const f = flags[i] + if (f band 0x02) != 0 do + const dx = ttf_u8(d, p) + p += 1 + if (f band 0x10) != 0 do x += dx end else do x -= dx end + end + else do + if (f band 0x10) == 0 do x += ttf_i16(d, p) p += 2 end + end + xs:push(x) + end + let ys = {} + let y = 0 + for i in 0..npts do + const f = flags[i] + if (f band 0x04) != 0 do + const dy = ttf_u8(d, p) + p += 1 + if (f band 0x20) != 0 do y += dy end else do y -= dy end + end + else do + if (f band 0x20) == 0 do y += ttf_i16(d, p) p += 2 end + end + ys:push(y) + end + # split into contours + let start = 0 + for ci in 0..nc do + const stop = ends[ci] + let pts = {} + for i in start..(stop + 1) do + pts:push({ x = xs[i] * scale + ox, y = baseline - ys[i] * scale, on = (flags[i] band 0x01) != 0 }) + end + ttf_contour(pts, edges, nsub) + start = stop + 1 + end +end + +fn ttf_fill(edges, w, h) do + let rows = {} + for py in 0..h do + const yc = py + 0.5 + let cross = {} + for e in 0..edges:len() do + const ed = edges[e] + const y0 = ed.ay + const y1 = ed.by + if ((y0 <= yc and yc < y1) or (y1 <= yc and yc < y0)) do + const t = (yc - y0) / (y1 - y0) + let dir = 1 + if y1 < y0 do dir = -1 end + cross:push({ x = ed.ax + t * (ed.bx - ed.ax), dir = dir }) + end + end + # sort crossings left to right + # insertion sort + for a in 1..cross:len() do + const key = cross[a] + let b = a - 1 + while (b >= 0 and cross[b].x > key.x) do + cross[b + 1] = cross[b] + b -= 1 + end + cross[b + 1] = key + end + let bits = 0 + let wind = 0 + for k in 0..cross:len() do + wind += cross[k].dir + if (wind != 0 and k + 1 < cross:len()) do + const xa = cross[k].x + const xb = cross[k + 1].x + if xb > xa do + let any = :false + for px in 0..w do + if (px + 0.5 >= xa and px + 0.5 < xb) do bits = bits bor (1 shl px) any = :true end + end + if any == :false do + const mid = math.floor((xa + xb) / 2) + if (mid >= 0 and mid < w) do bits = bits bor (1 shl mid) end + end + end + end + end + rows:push(bits) + end + return rows +end + +global ttf_io = { buf = 0, pages = 512 } + +global load_ttf = fn(path, size) do + const total = fs:size(path) + if total < 12 return :nil + if ttf_io.buf == 0 do ttf_io.buf = alloc_contig(ttf_io.pages) end + if ttf_io.buf == 0 return :nil + if total > ttf_io.pages * 4096 return :nil + fs:read_into(path, ttf_io.buf) + const d = ttf_io.buf + const head = ttf_table(d, "head") + const maxp = ttf_table(d, "maxp") + const cmap = ttf_table(d, "cmap") + const loca = ttf_table(d, "loca") + const glyf = ttf_table(d, "glyf") + const hhea = ttf_table(d, "hhea") + const hmtx = ttf_table(d, "hmtx") + if (head < 0 or cmap < 0 or loca < 0 or glyf < 0 or hhea < 0) return :nil + const upm = ttf_u16(d, head + 18) + if upm == 0 return :nil + const long = ttf_i16(d, head + 50) == 1 + const ascent = ttf_i16(d, hhea + 4) + const descent = ttf_i16(d, hhea + 6) + const numH = ttf_u16(d, hhea + 34) + const scale = size / upm + const baseline = math.floor(ascent * scale) + let height = math.floor((ascent - descent) * scale) + 1 + const sub = ttf_cmap_sub(d, cmap) + if sub < 0 return :nil + const mgate = ttf_glyph_of(d, sub, 77) # 'M' + let width = math.floor(ttf_advance(d, hmtx, numH, mgate) * scale) + 1 + if width < 2 do width = math.floor(size * 0.6) + 1 end + const nsub = 5 + let glyphs = {} + for code in 32..128 do + const g = ttf_glyph_of(d, sub, code) + let edges = {} + ttf_glyph_edges(d, glyf, loca, long, g, scale, 1, baseline, edges, nsub) + glyphs:push(ttf_fill(edges, width, height)) + end + return { glyphs = glyphs, width = width, height = height } +end diff --git a/kernel/tty.rv b/kernel/tty.rv new file mode 100644 index 0000000..5fcd224 --- /dev/null +++ b/kernel/tty.rv @@ -0,0 +1,151 @@ +# tty layer as opposed to a future window management server bullshit thingy +# this is just tty0 it adds nice shit like ^B and stuff +# also has history and shit + +global tty = { + line = "", cursor = 0, esc = 0, + history = {}, hist_index = 0, + + back = fn(self, n) do + let i = 0 + while i < n do puts("\x08") i += 1 end + end, + blanks = fn(self, n) do + let i = 0 + while i < n do puts(" ") i += 1 end + end, + + insert = fn(self, ch) do + const pre = self.line[0..self.cursor] + const post = self.line[self.cursor..] + self.line = pre ~ ch ~ post + puts(ch ~ post) + self:back(post:len()) + self.cursor += 1 + end, + + backspace = fn(self) do + if self.cursor == 0 return :nil + const pre = self.line[0..(self.cursor - 1)] + const post = self.line[self.cursor..] + self.line = pre ~ post + self.cursor -= 1 + puts("\x08") + puts(post ~ " ") + self:back(post:len() + 1) + end, + + move_left = fn(self) do + if self.cursor > 0 do puts("\x08") self.cursor -= 1 end + end, + move_right = fn(self) do + if self.cursor < self.line:len() do puts(self.line[self.cursor]) self.cursor += 1 end + end, + home = fn(self) do + self:back(self.cursor) + self.cursor = 0 + end, + to_end = fn(self) do + puts(self.line[self.cursor..]) + self.cursor = self.line:len() + end, + + kill = fn(self) do + self:back(self.cursor) + self:blanks(self.line:len()) + self:back(self.line:len()) + self.line = "" + self.cursor = 0 + end, + + erase_word = fn(self) do + if self.cursor == 0 return :nil + let at = self.cursor + while (at > 0 and self.line[at - 1] == " ") do at -= 1 end + while (at > 0 and self.line[at - 1] != " ") do at -= 1 end + let count = self.cursor - at + while count > 0 do self:backspace() count -= 1 end + end, + + replace_line = fn(self, text) do + self:kill() + self.line = text + puts(text) + self.cursor = text:len() + end, + + history_prev = fn(self) do + if self.history:len() == 0 return :nil + if self.hist_index > 0 do self.hist_index -= 1 end + self:replace_line(self.history[self.hist_index]) + end, + history_next = fn(self) do + if self.history:len() == 0 return :nil + if self.hist_index < self.history:len() - 1 do + self.hist_index += 1 + self:replace_line(self.history[self.hist_index]) + end + else do + self.hist_index = self.history:len() + self:replace_line("") + end + end, + + submit = fn(self) do + puts("\n") + const entered = self.line + if entered:len() > 0 do self.history:push(entered) end + self.hist_index = self.history:len() + self.line = "" + self.cursor = 0 + run(entered) + end, + + cancel = fn(self) do + puts("^C\n") + self.line = "" + self.cursor = 0 + self.hist_index = self.history:len() + puts(prompt()) + end, + + clear_screen = fn(self) do + console:clear() + puts(prompt()) + puts(self.line) + self:back(self.line:len() - self.cursor) + end, + + feed = fn(self, byte) do + if stdin_deliver(byte) return :nil + + if self.esc == 1 do + if byte == 91 do self.esc = 2 end + else do self.esc = 0 end + return :nil + end + if self.esc == 2 do + self.esc = 0 + if byte == 65 do self:history_prev() end # up + else if byte == 66 do self:history_next() end # down + else if byte == 67 do self:move_right() end # right + else if byte == 68 do self:move_left() end # left + else if byte == 72 do self:home() end # home + else if byte == 70 do self:to_end() end # end + return :nil + end + 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 == 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 + if byte == 12 do self:clear_screen() return :nil end # ctrl-l + if byte == 21 do self:kill() return :nil end # ctrl-u + if byte == 23 do self:erase_word() return :nil end # ctrl-w + if byte < 32 return :nil # ctrl-d and other ctrls: ignore + if byte > 126 return :nil + self:insert(string.of_ascii(byte)) + end, +} diff --git a/tools/genfonts.py b/tools/genfonts.py new file mode 100644 index 0000000..e5afb7e --- /dev/null +++ b/tools/genfonts.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# this just turns the linxu console fonts which are C arrays into psf1 fonts +# that i can load on disk since half of my psf1 fonts are stolen from there. +# +# usage: genfonts.py [height] + +import re +import sys + + +def parse_c(path): + text = open(path).read() + cut = text.index("}, {") + body = text[cut + 4 :] + body = re.sub(r"/\*.*?\*/", "", body, flags=re.S) + bytes_ = [int(m, 16) for m in re.findall(r"0x([0-9a-fA-F]{2})", body)] + return bytes_ + +def write_psf1(rows, height, out): + count = len(rows) // height + if count < 256: + rows = rows + [0] * (height * (256 - count)) + count = 256 + if count not in (256, 512): + rows = rows[: height * 256] + count = 256 + mode = 0x00 if count == 256 else 0x01 # bit0 set => 512 glyphs + header = bytes([0x36, 0x04, mode, height]) + with open(out, "wb") as f: + f.write(header) + f.write(bytes(rows[: height * count])) + +def main(): + src, out = sys.argv[1], sys.argv[2] + height = int(sys.argv[3]) if len(sys.argv) > 3 else int(re.search(r"8x(\d+)", src).group(1)) + rows = parse_c(src) + write_psf1(rows, height, out) + print(f"{out}: psf1 8x{height}, {len(rows) // height} glyphs in") + +if __name__ == "__main__": + main() diff --git a/tools/subset-ttf.sh b/tools/subset-ttf.sh new file mode 100755 index 0000000..70cffa2 --- /dev/null +++ b/tools/subset-ttf.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# trim the ttf into a subset (U+0020..007E), its just to embed fonts that are actually +# rendered, since we have NERD fonts and shit, they can be quite large + +set -e +IN="$1" +OUT="$2" +python3 -m fontTools.subset "$IN" \ + --unicodes=U+0020-007E \ + --no-hinting --glyph-names \ + --output-file="$OUT" +echo "wrote $OUT" -- cgit v1.3