Microkernel thing OS experiment (Zig ⚡)

reintroduce console and add double buffering

pci.express 765e551a 3ba56657

verified
Changed files
+456 -76
components
+2 -2
components/root_server/src/main.zig
···
const os = @import("os.zig");
export fn _start() callconv(.c) noreturn {
-
// _ = os.syscall1(SYS_poke, 0xB16B00B5BADBABE);
-
// _ = os.syscall1(SYS_exit, 0x69696969);
+
_ = os.syscall1(SYS_poke, 0xB16B00B5BADBABE);
+
_ = os.syscall1(SYS_exit, 0x69696969);
asm volatile ("int3");
asm volatile (
\\ mov $0x69696969, %%rdi
+10 -17
components/ukernel/arch/amd64/boot.zig
···
const std = @import("std");
const arch = @import("root.zig");
const common = @import("common");
-
const flanterm = @import("flanterm");
+
const console = @import("console");
const log = std.log.scoped(.amd64_init);
const StandardGdt = arch.structures.gdt.StandardGdt;
const Tss = arch.structures.tss.Tss;
···
const hhdm_offset = limine_requests.hhdm.response.?.offset;
common.init_data.hhdm_slide = hhdm_offset;
+
// Set up the temporary Physical Memory Allocator
+
common.mm.bootmem.init();
+
// Add in a framebuffer if found
initConsole();
+
// Get basic information through CPUID
arch.instructions.cpuid.init();
// Add in ACPI/dtb if found, prefer ACPI
initHwDesc();
-
-
// Set up the temporary Physical Memory Allocator
-
common.mm.bootmem.init();
// Attach the root task
if (limine_requests.modules.response) |module_response| {
···
fn initConsole() void {
if (limine_requests.framebuffer.response) |fb_response| {
if (fb_response.framebuffer_count > 0) {
-
const fb = common.aux.Framebuffer.from_limine(fb_response.getFramebuffers()[0]);
+
const fb = common.aux.fb_from_limine(fb_response.getFramebuffers()[0]);
common.init_data.framebuffer = fb;
+
// Create a canvas for the console to render to
+
const canvas: [*]u8 = @ptrFromInt(common.init_data.bootmem.allocMem(fb.width * fb.height * fb.bypp) catch @panic("Couldn't allocate a canvas"));
+
@memset(canvas[0 .. fb.width * fb.height * fb.bypp], 0);
-
common.init_data.console = flanterm.Context.init(.{
-
.fb = fb.address,
-
.width = fb.width,
-
.height = fb.height,
-
.pitch = fb.pitch,
-
.red_mask_size = fb.red_mask_size,
-
.red_mask_shift = fb.red_mask_shift,
-
.green_mask_size = fb.green_mask_size,
-
.green_mask_shift = fb.green_mask_shift,
-
.blue_mask_size = fb.blue_mask_size,
-
.blue_mask_shift = fb.blue_mask_shift,
-
});
+
common.init_data.console = console.Console.init(fb, canvas);
}
}
}
+6 -3
components/ukernel/arch/amd64/instructions/root.zig
···
pub const cpuid = @import("cpuid.zig");
pub inline fn die() noreturn {
-
while (true) {
-
asm volatile ("hlt");
-
}
+
asm volatile (
+
\\ mov $0xDEADDEADDEADDEAD, %%rax
+
\\ 1: hlt
+
\\ jmp 1b
+
);
+
unreachable;
}
+4 -4
components/ukernel/build.zig
···
.api_revision = 3,
});
const spinlock_dep = b.dependency("spinlock", .{});
-
const flanterm_dep = b.dependency("flanterm", .{});
+
const console_dep = b.dependency("console", .{});
const limine_mod = limine_dep.module("limine");
const spinlock_mod = spinlock_dep.module("spinlock");
-
const flanterm_mod = flanterm_dep.module("flanterm");
+
const console_mod = console_dep.module("console");
const common_mod = b.createModule(.{
.root_source_file = b.path("common/root.zig"),
});
arch_module.addImport("limine", limine_mod);
-
arch_module.addImport("flanterm", flanterm_mod);
+
arch_module.addImport("console", console_mod);
arch_module.addImport("common", common_mod);
common_mod.addImport("arch", arch_module);
common_mod.addImport("spinlock", spinlock_mod);
-
common_mod.addImport("flanterm", flanterm_mod);
+
common_mod.addImport("console", console_mod);
common_mod.addImport("limine", limine_mod);
const kernel = b.addExecutable(.{
+1 -4
components/ukernel/build.zig.zon
···
.limine = .{ .path = "deps/limine-zig" },
.spinlock = .{ .path = "deps/spinlock" },
.build_helpers = .{ .path = "../build_helpers" },
-
.flanterm = .{
-
.url = "git+https://tangled.sh/@sydney.blue/flanterm.zig?ref=trunk#8071c825750c415b9e5502cdff34efc9c6dfeab7",
-
.hash = "flanterm-2.0.0-QnufngHlAQCGtHyca0PrvRvoOldHdJG4DdSz437r9fRr",
-
},
+
.console = .{ .path = "deps/console" },
},
.paths = .{
"build.zig",
+26 -45
components/ukernel/common/aux/root.zig
···
const console = @import("console");
-
const flanterm = @import("flanterm");
const common = @import("../root.zig");
const mm = common.mm;
const std = @import("std");
···
pub const InitState = struct {
bootmem: mm.bootmem.BootPmm = .{},
-
console: ?flanterm.Context = null,
-
framebuffer: ?common.aux.Framebuffer = null,
+
console: ?console.Console = null,
+
framebuffer: ?console.Framebuffer = null,
hardware_description: HardwareDescription = .none,
root_task: []align(4096) u8 = undefined,
hhdm_slide: usize = 0,
};
-
pub const Framebuffer = struct {
-
const Self = @This();
-
address: [*]u32,
-
width: u64,
-
height: u64,
-
pitch: u64,
-
bypp: u16,
-
red_mask_size: u8,
-
red_mask_shift: u8,
-
green_mask_size: u8,
-
green_mask_shift: u8,
-
blue_mask_size: u8,
-
blue_mask_shift: u8,
-
-
pub fn from_limine(fb: *const limine.Framebuffer) Self {
-
return .{
-
.address = @ptrCast(@alignCast(fb.address)),
-
.width = fb.width,
-
.height = fb.height,
-
.pitch = fb.pitch,
-
.red_mask_size = fb.red_mask_size,
-
.red_mask_shift = fb.red_mask_shift,
-
.green_mask_size = fb.green_mask_size,
-
.green_mask_shift = fb.green_mask_shift,
-
.blue_mask_size = fb.blue_mask_size,
-
.blue_mask_shift = fb.blue_mask_shift,
-
.bypp = fb.bpp / 8,
-
};
-
}
-
};
+
pub fn fb_from_limine(fb: *const limine.Framebuffer) console.Framebuffer {
+
return .{
+
.address = @ptrCast(@alignCast(fb.address)),
+
.width = fb.width,
+
.height = fb.height,
+
.pitch = fb.pitch,
+
.red_mask_size = fb.red_mask_size,
+
.red_mask_shift = fb.red_mask_shift,
+
.green_mask_size = fb.green_mask_size,
+
.green_mask_shift = fb.green_mask_shift,
+
.blue_mask_size = fb.blue_mask_size,
+
.blue_mask_shift = fb.blue_mask_shift,
+
.bypp = fb.bpp / 8,
+
};
+
}
var stdout_lock: spinlock.Spinlock = .{};
···
if (common.init_data.console == null) return;
// Use the same naming as the default logger
-
const level, const color: flanterm.Colors.Color = switch (message_level) {
-
.debug => .{ "D", .green },
-
.err => .{ "E", .red },
-
.info => .{ "I", .cyan },
-
.warn => .{ "W", .yellow },
+
const level, const color: u32 = switch (message_level) {
+
.debug => .{ "D", 0x3bcf1d },
+
.err => .{ "E", 0xff0000 },
+
.info => .{ "I", 0x00bbbb },
+
.warn => .{ "W", 0xfee409 },
};
// Use same format as default once again
const scope_text = switch (scope) {
···
const prefix = std.fmt.comptimePrint("{s}{s}: ", .{ level, scope_text });
{
-
const color_default: flanterm.Colors.Color = .default;
stdout_lock.lock();
defer stdout_lock.unlock();
-
var backing_buf = std.mem.zeroes([512]u8);
-
const buf = std.fmt.bufPrint(backing_buf[0..], color.esc_seq() ++ prefix ++ format ++ color_default.esc_seq() ++ "\n", args) catch return;
-
-
common.init_data.console.?.write_slice(buf);
-
// cons.setColor(color, 0);
-
// cons.writer().print(prefix ++ format ++ "\n", args) catch return;
+
common.init_data.console.?.setColor(color, 0);
+
// No buffering for now
+
var writer = console.Console.Writer.init(&common.init_data.console.?, &.{});
+
writer.interface.print(prefix ++ format ++ "\n", args) catch return;
}
}
-1
components/ukernel/common/mm/bootmem.zig
···
// Finally, initialize the global bootmem
common.init_data.bootmem.initialize(bootmem_struct);
-
common.init_data.bootmem.debugInfo();
}
+6
components/ukernel/deps/console/build.zig
···
+
const std = @import("std");
+
pub fn build(b: *std.Build) void {
+
_ = b.addModule("console", .{
+
.root_source_file = b.path("src/root.zig"),
+
});
+
}
+13
components/ukernel/deps/console/build.zig.zon
···
+
.{
+
.name = .console,
+
.version = "0.0.0",
+
.fingerprint = 0x3603cfb621692996, // Changing this has security and trust implications.
+
.minimum_zig_version = "0.15.1",
+
.dependencies = .{},
+
.paths = .{
+
"build.zig",
+
"build.zig.zon",
+
"src",
+
"fonts",
+
},
+
}
+24
components/ukernel/deps/console/flake.lock
···
+
{
+
"nodes": {
+
"nixpkgs": {
+
"locked": {
+
"lastModified": 315532800,
+
"narHash": "sha256-t4zrLJk1EZWk1lUnvNEVjPBVNBHVzS3A0RsxkRSwwSE=",
+
"rev": "6d7ec06d6868ac6d94c371458fc2391ded9ff13d",
+
"type": "tarball",
+
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre861040.6d7ec06d6868/nixexprs.tar.xz?rev=6d7ec06d6868ac6d94c371458fc2391ded9ff13d"
+
},
+
"original": {
+
"type": "tarball",
+
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
+
}
+
},
+
"root": {
+
"inputs": {
+
"nixpkgs": "nixpkgs"
+
}
+
}
+
},
+
"root": "root",
+
"version": 7
+
}
+23
components/ukernel/deps/console/flake.nix
···
+
{
+
inputs = {
+
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
+
};
+
outputs =
+
{ nixpkgs, ... }@inputs:
+
let
+
inherit (inputs.nixpkgs) lib;
+
forAllSystems =
+
body: lib.genAttrs lib.systems.flakeExposed (system: body nixpkgs.legacyPackages.${system});
+
in
+
{
+
devShells = forAllSystems (pkgs: {
+
default = pkgs.mkShell {
+
packages = with pkgs; [
+
zig_0_15
+
];
+
};
+
});
+
+
formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
+
};
+
}
+24
components/ukernel/deps/console/src/fonts/LICENSE.spleen
···
+
Copyright (c) 2018-2024, Frederic Cambus
+
All rights reserved.
+
+
Redistribution and use in source and binary forms, with or without
+
modification, are permitted provided that the following conditions are met:
+
+
* Redistributions of source code must retain the above copyright
+
notice, this list of conditions and the following disclaimer.
+
+
* Redistributions in binary form must reproduce the above copyright
+
notice, this list of conditions and the following disclaimer in the
+
documentation and/or other materials provided with the distribution.
+
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+
POSSIBILITY OF SUCH DAMAGE.
components/ukernel/deps/console/src/fonts/spleen-12x24.psf

This is a binary file and will not be displayed.

+39
components/ukernel/deps/console/src/psf2.zig
···
+
const std = @import("std");
+
+
pub const Psf2Header = extern struct {
+
magic: u32 = 0x864ab572,
+
version: u32,
+
header_size: u32,
+
flags: u32,
+
numglyph: u32,
+
bytes_per_glyph: u32,
+
height: u32,
+
width: u32,
+
+
pub fn bytesPerLine(self: *const Psf2Header) u32 {
+
return (self.width + 7) / 8;
+
}
+
};
+
+
pub const Font = struct {
+
const Self = @This();
+
fontdata: []const u8 align(4),
+
pub fn new(fontdata: []const u8) Self {
+
return .{
+
.fontdata = fontdata,
+
};
+
}
+
+
pub fn getHdr(self: *const Self) *const Psf2Header {
+
return @ptrCast(@alignCast(self.fontdata));
+
}
+
+
pub fn getGlyph(self: *const Self, ch: u8) ![]const u8 {
+
const hdr = self.getHdr();
+
const startpos: u64 = @as(u64, hdr.header_size) + @as(u64, ch) * @as(u64, hdr.bytes_per_glyph);
+
+
if (self.fontdata.len < startpos + @as(u64, hdr.bytes_per_glyph)) return error.InvalidCharacter;
+
+
return self.fontdata[startpos..][0..hdr.bytes_per_glyph];
+
}
+
};
+278
components/ukernel/deps/console/src/root.zig
···
+
const std = @import("std");
+
const builtin = @import("builtin");
+
pub const psf2 = @import("psf2.zig");
+
const are_we_le = builtin.cpu.arch.endian() == .little;
+
+
const fontdata_embed = @embedFile("fonts/spleen-12x24.psf");
+
const fontdata: [fontdata_embed.len]u8 align(@alignOf(u32)) = fontdata_embed.*;
+
+
/// Basic framebuffer container
+
pub const Framebuffer = struct {
+
const Self = @This();
+
address: [*]u32,
+
width: u64,
+
height: u64,
+
pitch: u64,
+
bypp: u16,
+
red_mask_size: u8,
+
red_mask_shift: u8,
+
green_mask_size: u8,
+
green_mask_shift: u8,
+
blue_mask_size: u8,
+
blue_mask_shift: u8,
+
};
+
+
/// Framebuffer based console
+
pub const Console = struct {
+
fb: Framebuffer,
+
canvas: [*]u8,
+
font: psf2.Font,
+
x_pos: usize = 0,
+
y_pos: usize = 0,
+
x_chrs_max: usize,
+
y_chrs_max: usize,
+
fg_color: u32 = 0xFFFFFFFF,
+
bg_color: u32 = 0,
+
+
/// Create an instance given a framebuffer
+
/// Canvas must be exactly fb.width * fb.height * fb.bypp bytes
+
pub fn init(fb: Framebuffer, canvas: [*]u8) Console {
+
const font = psf2.Font.new(&fontdata);
+
return init_with_font(fb, canvas, font);
+
}
+
+
/// Create an instance given a framebuffer and font
+
pub fn init_with_font(fb: Framebuffer, canvas: [*]u8, font: psf2.Font) Console {
+
const font_hdr = font.getHdr();
+
return .{
+
.fb = fb,
+
.font = font,
+
.canvas = canvas,
+
// TODO: implement spacing between chars?
+
.x_chrs_max = fb.width / font_hdr.width,
+
.y_chrs_max = fb.height / font_hdr.height,
+
};
+
}
+
+
/// Write a string to the console
+
pub fn puts(self: *Console, msg: []const u8) usize {
+
var written: usize = 0;
+
+
const start_line, const num_lines = blk: {
+
const start_line = self.y_pos;
+
var scrolled: bool = false;
+
for (msg) |ch| {
+
// TODO: handle characters that failed to print
+
scrolled |= self.putc(ch) catch false;
+
written += 1;
+
}
+
if (scrolled) break :blk .{ 0, self.y_chrs_max };
+
break :blk .{ start_line, self.y_pos - start_line + 1 };
+
};
+
self.renderCanvas(start_line, num_lines);
+
return written;
+
}
+
+
// Copy in the given lines from the canvas to the framebuffer
+
fn renderCanvas(
+
self: *Console,
+
start_line: usize,
+
num_lines: usize,
+
) void {
+
const glyph_height: usize = @intCast(self.font.getHdr().height);
+
// Not necessarily fb pitch!
+
const canvas_pitch = self.fb.width * self.fb.bypp;
+
const byte_fb: [*]u8 = @ptrCast(self.fb.address);
+
+
const src_buf = self.canvas[canvas_pitch * glyph_height * start_line ..][0 .. canvas_pitch * glyph_height * num_lines];
+
const dst_buf = byte_fb[self.fb.pitch * glyph_height * start_line ..][0 .. self.fb.pitch * glyph_height * num_lines];
+
@memcpy(dst_buf, src_buf);
+
}
+
+
/// Write a character to the console, return true if scrolled
+
/// If putchar failed we did not scroll for sure
+
fn putc(self: *Console, ch: u8) !bool {
+
var scrolled: bool = false;
+
// Handle newlines
+
if (ch == '\r') return scrolled;
+
if (ch == '\n') {
+
// Reset to the beginning of the next line
+
self.x_pos = 0;
+
self.y_pos += 1;
+
// If we've overrun, scroll the entire framebuffer up one
+
// and then reset to the last line
+
if (self.y_pos >= self.y_chrs_max) {
+
self.scrollUp();
+
scrolled = true;
+
}
+
return scrolled;
+
}
+
// TODO: color palette and escape codes?
+
try self.putchar(ch, self.x_pos, self.y_pos, self.fg_color, self.bg_color);
+
self.x_pos += 1;
+
+
// If our x is too far, go down a line
+
if (self.x_pos < self.x_chrs_max) return scrolled;
+
self.x_pos = 0;
+
self.y_pos += 1;
+
if (self.y_pos >= self.y_chrs_max) {
+
self.scrollUp();
+
scrolled = true;
+
}
+
return scrolled;
+
}
+
+
fn putchar(self: *const Console, ch: u8, x_pos: usize, y_pos: usize, fg_val: u32, bg_val: u32) !void {
+
const raw_color_choice: [2]u32 = [2]u32{
+
if (are_we_le) bg_val else @byteSwap(bg_val),
+
if (are_we_le) fg_val else @byteSwap(fg_val),
+
};
+
+
const font = self.font;
+
const hdr = font.getHdr();
+
+
const bytes_per_line = hdr.bytesPerLine();
+
const mask_shamt: u5 = @truncate(bytes_per_line * 8 - 1);
+
const mask_initial: u32 = @as(u32, 1) << mask_shamt;
+
const glyph = try font.getGlyph(ch);
+
+
// Offset into framebuffer of the beginning of the character
+
const canvas_pitch: usize = self.fb.width * self.fb.bypp;
+
var offset: usize = (y_pos * @as(usize, hdr.height) * canvas_pitch) + (x_pos * @as(usize, hdr.width) * self.fb.bypp);
+
// run for every line
+
var glyph_y: u32 = 0;
+
var mask: u32 = 0;
+
while (glyph_y < hdr.height) : (glyph_y += 1) {
+
// initialize the mask and current line
+
mask = mask_initial;
+
// TODO: endian
+
const line_value: u32 = std.mem.readVarInt(u32, glyph[glyph_y * bytes_per_line ..][0..bytes_per_line], .big);
+
// offset into the fb of the current line
+
var line_offset: usize = offset;
+
var glyph_x: u32 = 0;
+
while (glyph_x < hdr.width) : (glyph_x += 1) {
+
// Write the fb or bg color
+
const color: [4]u8 = @bitCast(raw_color_choice[@intFromBool(line_value & mask != 0)]);
+
@memcpy(self.canvas[line_offset..][0..self.fb.bypp], color[0..]);
+
// Move right a pixel
+
line_offset += self.fb.bypp;
+
mask >>= 1;
+
}
+
// Move down a line
+
offset += canvas_pitch;
+
}
+
}
+
+
// Set the fg and bg color
+
pub fn setColor(self: *Console, new_fg: u32, new_bg: u32) void {
+
self.fg_color = self.convertColor(new_fg);
+
self.bg_color = self.convertColor(new_bg);
+
}
+
+
// Convert a normal _RGB u32 to the actual framebuffer format
+
fn convertColor(self: *const Console, color: u32) u32 {
+
// The color value also needs to be scaled. For example,
+
// if it's 10 bits per color and we're starting from 8 bits,
+
// Full bright will only be 0xFF/0x3FF or about 25% brightness.
+
// To fix this hypothetical, shift left by (10 - 8). This isn't
+
// perfectly accurate but close enough.
+
const red_left_shift: u5 = @truncate(self.fb.red_mask_size);
+
const green_left_shift: u5 = @truncate(self.fb.red_mask_size);
+
const blue_left_shift: u5 = @truncate(self.fb.red_mask_size);
+
+
// Get our source RGB 888
+
const right_shift = 8;
+
const red_src: u32 = (color >> 16) & 0xFF;
+
const green_src: u32 = (color >> 8) & 0xFF;
+
const blue_src: u32 = color & 0xFF;
+
+
// These shifts are the offsets to place each color.
+
const red_dest_shift: u5 = @truncate(self.fb.red_mask_shift);
+
const green_dest_shift: u5 = @truncate(self.fb.green_mask_shift);
+
const blue_dest_shift: u5 = @truncate(self.fb.blue_mask_shift);
+
+
// Do the calculations
+
const red_dst = switch (std.math.order(red_left_shift, right_shift)) {
+
.gt => red_src << (red_left_shift - right_shift),
+
.lt => red_src >> (right_shift - red_left_shift),
+
.eq => red_src,
+
} << red_dest_shift;
+
+
const green_dst = switch (std.math.order(green_left_shift, right_shift)) {
+
.gt => green_src << (green_left_shift - right_shift),
+
.lt => green_src >> (right_shift - green_left_shift),
+
.eq => green_src,
+
} << green_dest_shift;
+
+
const blue_dst = switch (std.math.order(blue_left_shift, right_shift)) {
+
.gt => blue_src << (blue_left_shift - right_shift),
+
.lt => blue_src >> (right_shift - blue_left_shift),
+
.eq => blue_src,
+
} << blue_dest_shift;
+
+
return red_dst | green_dst | blue_dst;
+
}
+
+
// Make sure to set damage to the entire screen!
+
fn scrollUp(self: *Console) void {
+
const glyph_height: usize = @intCast(self.font.getHdr().height);
+
const canvas_pitch: usize = self.fb.width * self.fb.bypp;
+
+
// Copy 1:n line up to 0:n-1 with memmove
+
const dst_buf = self.canvas[0 .. canvas_pitch * glyph_height * (self.y_chrs_max - 1)];
+
const src_buf = self.canvas[canvas_pitch * glyph_height ..][0 .. canvas_pitch * glyph_height * (self.y_chrs_max - 1)];
+
@memmove(dst_buf, src_buf);
+
+
// Clear last line
+
const last_line = self.canvas[canvas_pitch * glyph_height * (self.y_chrs_max - 1) ..][0 .. canvas_pitch * glyph_height];
+
@memset(last_line, 0);
+
self.x_pos = 0;
+
self.y_pos = self.y_chrs_max - 1;
+
}
+
+
// Get a writer with optional buffering
+
pub fn writer(self: *Console, buffer: []u8) Writer {
+
return Writer.init(self, buffer);
+
}
+
+
// Writer with the new std.Io.Writer interface
+
pub const Writer = struct {
+
console: *Console,
+
interface: std.Io.Writer,
+
+
pub fn init(console: *Console, buffer: []u8) Writer {
+
return .{
+
.console = console,
+
.interface = .{
+
.buffer = buffer,
+
.vtable = &.{ .drain = Writer.drain },
+
},
+
};
+
}
+
+
pub fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize {
+
// fieldParentPtr is so cool
+
const self: *Writer = @fieldParentPtr("interface", w);
+
var written: usize = 0;
+
// First, consume the buffer
+
if (w.end < w.buffer.len) {
+
const n = self.console.puts(w.buffer[w.end..]);
+
written += n;
+
w.end += n;
+
}
+
+
// Iterate over all the provided slices
+
for (data, 0..) |slice, i| {
+
// If we are the last slice, splat by the amount
+
if (i == data.len - 1) {
+
for (0..splat) |_| {
+
written += self.console.puts(slice);
+
}
+
break;
+
}
+
written += self.console.puts(slice);
+
}
+
return written;
+
}
+
};
+
};