core: functional App structure

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-18 19:02:59 -06:00
parent 891bab73dd
commit 11b7b86913
7 changed files with 87 additions and 70 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
zig-cache/ zig-cache/
zig-out/ zig-out/
*.log

View file

@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void {
}); });
const lib = b.addStaticLibrary(.{ const lib = b.addStaticLibrary(.{
.name = "salmon", .name = "odditui",
// In this case the main source file is merely a path, however, in more // In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file. // complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/root.zig" }, .root_source_file = .{ .path = "src/root.zig" },
@ -39,7 +39,7 @@ pub fn build(b: *std.Build) void {
b.installArtifact(lib); b.installArtifact(lib);
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "salmon", .name = "odditui",
.root_source_file = .{ .path = "src/main.zig" }, .root_source_file = .{ .path = "src/main.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,

View file

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const os = std.os; const os = std.os;
const App = @import("odditui.zig").App;
const log = std.log.scoped(.tty); const log = std.log.scoped(.tty);
@ -44,21 +45,43 @@ pub fn stop(self: *Tty) void {
} }
/// read input from the tty /// read input from the tty
pub fn run(self: *Tty, comptime T: type, comptime _: fn (ev: T) void) !void { pub fn run(
self: *Tty,
comptime EventType: type,
app: *App(EventType),
) !void {
// create a pipe so we can signal to exit the run loop // create a pipe so we can signal to exit the run loop
const pipe = try os.pipe(); const pipe = try os.pipe();
defer os.close(pipe[0]); defer os.close(pipe[0]);
defer os.close(pipe[1]); defer os.close(pipe[1]);
// assign the write end of the pipe to our quit_fd
self.quit_fd = pipe[1]; self.quit_fd = pipe[1];
var parser: Parser = .{}; // the state of the parser
const State = enum {
ground,
escape,
csi,
osc,
dcs,
sos,
pm,
apc,
ss2,
ss3,
};
var buf: [1024]u8 = undefined; const state: State = .ground;
// Set up fds for polling
var pollfds: [2]std.os.pollfd = .{ var pollfds: [2]std.os.pollfd = .{
.{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined }, .{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined },
.{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined }, .{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined },
}; };
// initialize the read buffer
var buf: [1024]u8 = undefined;
while (true) { while (true) {
_ = try std.os.poll(&pollfds, -1); _ = try std.os.poll(&pollfds, -1);
if (pollfds[1].revents & std.os.POLL.IN != 0) { if (pollfds[1].revents & std.os.POLL.IN != 0) {
@ -67,7 +90,16 @@ pub fn run(self: *Tty, comptime T: type, comptime _: fn (ev: T) void) !void {
} }
const n = try os.read(self.fd, &buf); const n = try os.read(self.fd, &buf);
parser.parse(self, buf[0..n]); var i: usize = 0;
while (i < n) : (i += 1) {
const b = buf[i];
switch (state) {
.ground => {
app.postEvent(.{ .key = b });
},
else => {},
}
}
} }
} }
@ -110,38 +142,3 @@ pub fn makeRaw(fd: os.fd_t) !os.termios {
try os.tcsetattr(fd, .FLUSH, raw); try os.tcsetattr(fd, .FLUSH, raw);
return state; return state;
} }
/// parses vt input. Retains some state so we need an object for it
const Parser = struct {
const log = std.log.scoped(.parser);
// the state of the parser
const State = enum {
ground,
escape,
csi,
osc,
dcs,
sos,
pm,
apc,
ss2,
ss3,
};
state: State = .ground,
fn parse(self: *Parser, tty: *Tty, input: []u8) void {
_ = tty; // autofix
var i: usize = 0;
const start: usize = 0;
_ = start; // autofix
while (i < input.len) : (i += 1) {
const b = input[i];
switch (self.state) {
.ground => Parser.log.err("0x{x}\r", .{b}),
else => {},
}
}
}
};

View file

@ -1,31 +1,19 @@
const std = @import("std"); const std = @import("std");
const Tty = @import("tty/Tty.zig"); const Tty = @import("tty/Tty.zig");
const odditui = @import("odditui.zig");
const log = std.log.scoped(.main);
pub fn main() !void { pub fn main() !void {
// Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) var app: odditui.App(Event) = try odditui.App(Event).init(.{});
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
// stdout is for the actual output of your application, for example if you try app.start();
// are implementing gzip, then only the compressed bytes should be sent to
// stdout, not any debugging messages.
const stdout_file = std.io.getStdOut().writer();
var bw = std.io.bufferedWriter(stdout_file);
const stdout = bw.writer();
var tty = try Tty.init(); while (true) {
defer tty.deinit(); const event = app.nextEvent();
log.debug("event: {}", .{event});
}
// run our tty read loop in it's own thread app.stop();
const read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &tty, Event, eventCallback });
try read_thread.setName("tty");
std.time.sleep(100_000_000_00);
tty.stop();
read_thread.join();
try stdout.print("Run `zig build test` to run the tests.\n", .{});
try bw.flush(); // don't forget to flush!
} }
const Event = union(enum) { const Event = union(enum) {

View file

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const queue = @import("queue.zig"); const queue = @import("queue.zig");
const Tty = @import("Tty.zig");
/// App is the entrypoint for an odditui application. The provided type T should /// App is the entrypoint for an odditui application. The provided type T should
/// be a tagged union which contains all of the events the application will /// be a tagged union which contains all of the events the application will
@ -10,17 +11,48 @@ pub fn App(comptime T: type) type {
return struct { return struct {
const Self = @This(); const Self = @This();
const log = std.log.scoped(.app);
pub const EventType = T;
/// the event queue for this App /// the event queue for this App
// //
// TODO: is 512 ok? // TODO: is 512 ok?
queue: queue.Queue(T, 512) = .{}, queue: queue.Queue(T, 512),
tty: ?Tty,
/// Runtime options /// Runtime options
const Options = struct {}; const Options = struct {};
/// Initialize an App with runtime options /// Initialize an App with runtime options
pub fn init(_: Options) Self { pub fn init(_: Options) !Self {
return Self{}; return Self{
.queue = .{},
.tty = null,
};
}
pub fn deinit(self: *Self) void {
if (self.tty) |_| {
var tty = &self.tty.?;
tty.deinit();
}
}
/// spawns the input thread to start listening to the tty for input
pub fn start(self: *Self) !void {
self.tty = try Tty.init();
// run our tty read loop in it's own thread
const read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self });
try read_thread.setName("tty");
}
pub fn stop(self: *Self) void {
if (self.tty) |_| {
var tty = &self.tty.?;
tty.stop();
}
} }
/// returns the next available event, blocking until one is available /// returns the next available event, blocking until one is available
@ -40,7 +72,8 @@ test "App: event queueing" {
const Event = union(enum) { const Event = union(enum) {
key, key,
}; };
var app: App(Event) = App(Event).init(.{}); var app: App(Event) = try App(Event).init(.{});
defer app.deinit();
app.postEvent(.key); app.postEvent(.key);
const event = app.nextEvent(); const event = app.nextEvent();
try std.testing.expect(event == .key); try std.testing.expect(event == .key);

View file

@ -3,6 +3,8 @@ const assert = std.debug.assert;
const atomic = std.atomic; const atomic = std.atomic;
const Futex = std.Thread.Futex; const Futex = std.Thread.Futex;
const log = std.log.scoped(.queue);
pub fn Queue( pub fn Queue(
comptime T: type, comptime T: type,
comptime size: usize, comptime size: usize,

View file

@ -1,4 +0,0 @@
const std = @import("std");
const os = std.os;
pub const Tty = @import("Tty.zig");