const std = @import("std"); const builtin = @import("builtin"); const vaxis = @import("vaxis"); const xev = @import("xev"); const Cell = vaxis.Cell; const networking = @import("./networking.zig"); const rotctl = @import("./rotctl.zig"); const log = std.log.scoped(.rotint); pub const panic = vaxis.panic_handler; pub const std_options: std.Options = .{ .log_level = if (builtin.mode == .Debug) .debug else .err, }; // these are all in degrees pub const AzEl = struct { az: f64, el: f64 }; pub const RotInt = struct { allocator: std.mem.Allocator, // TODO: associate timestamps with this somehow offsets: AzEl = .{ .az = 0, .el = 0 }, last_request: AzEl = .{ .az = 0, .el = 0 }, current_posture: AzEl = .{ .az = 0, .el = 0 }, // printbuffer: [128]u8 = undefined, termbuffer: std.io.BufferedWriter(4096, std.io.AnyWriter), vx: *vaxis.Vaxis, loop: *xev.Loop, fake: FakeRotator = .{}, parser: rotctl.RotCtl = .{}, server: networking.Server = .{}, pub fn initFixed(self: *RotInt) void { self.server.rotint = self; } pub fn warn(_: *RotInt, comptime fmt: []const u8, args: anytype) void { log.warn(fmt, args); } pub fn forwardRequest(self: *RotInt, req: []const u8) void { var temp: [128]u8 = undefined; const command = self.parser.parseCommand(req) catch |err| switch (err) { error.Incomplete => return, error.NotSupported => { const response = (rotctl.RotReply{ .status = .not_supported }).write(temp[0..]) catch { self.warn("serialization failure", .{}); return; }; self.server.writeResponse(self.loop, response); return; }, error.InvalidParameter => { const response = (rotctl.RotReply{ .status = .invalid_parameter }).write(temp[0..]) catch { self.warn("serialization failure", .{}); return; }; self.server.writeResponse(self.loop, response); return; }, }; // if command is `quit`, we should disconnect the client after our reply has been sent. const reply = self.fake.request(command); const response = reply.write(temp[0..]) catch { self.warn("serialization failure", .{}); return; }; self.server.writeResponse(self.loop, response); } fn draw(self: *RotInt) !void { const Static = struct { const lower_limit: u8 = 30; const next_ms: u64 = 8; var color_idx: u8 = lower_limit; var dir: enum { up, down } = .up; }; const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ Static.color_idx, Static.color_idx, Static.color_idx } }, }; const segment: vaxis.Segment = .{ .text = "yeah ok", .style = style, }; const win = self.vx.window(); win.clear(); const y_off = (win.height / 2) -| (6 / 2); const x_off = (win.width / 2) -| (30 / 2); const center = win.child(.{ .x_off = x_off + win.x_off, .y_off = y_off + win.y_off, .width = .{ .limit = 30 }, .height = .{ .limit = 6 }, .border = .{ .where = .all, .style = style }, }); _ = try center.printSegment(segment, .{ .wrap = .grapheme }); switch (Static.dir) { .up => { Static.color_idx += 1; if (Static.color_idx == 255) Static.dir = .down; }, .down => { Static.color_idx -= 1; if (Static.color_idx == Static.lower_limit) Static.dir = .up; }, } try self.vx.render(self.termbuffer.writer().any()); try self.termbuffer.flush(); } }; const FakeRotator = struct { const park_position: AzEl = .{ .az = 180, .el = 90 }; current: AzEl = park_position, fn request(self: *FakeRotator, command: rotctl.RotCommand) rotctl.RotReply { return switch (command) { .get_position => .{ .get_position = self.current }, .set_position => |pos| blk: { self.current = pos; break :blk .{ .status = .okay }; }, .stop => .{ .status = .okay }, .park => blk: { self.current = park_position; break :blk .{ .status = .okay }; }, .quit => .{ .status = .okay }, }; } }; // const App = struct { // const lower_limit: u8 = 30; // const next_ms: u64 = 8; // allocator: std.mem.Allocator, // vx: *vaxis.Vaxis, // buffered_writer: std.io.BufferedWriter(4096, std.io.AnyWriter), // color_idx: u8, // dir: enum { // up, // down, // }, // }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const alloc = gpa.allocator(); var tty = try vaxis.Tty.init(); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); defer vx.deinit(alloc, tty.anyWriter()); var pool = xev.ThreadPool.init(.{}); var loop = try xev.Loop.init(.{ .thread_pool = &pool, }); defer loop.deinit(); var app: RotInt = .{ .allocator = alloc, .termbuffer = tty.bufferedWriter(), .vx = &vx, .loop = &loop, }; app.initFixed(); var vx_loop: vaxis.xev.TtyWatcher(RotInt) = undefined; try vx_loop.init(&tty, &vx, &loop, &app, eventCallback); // try vx.enterAltScreen(tty.anyWriter()); try vx.queryTerminalSend(tty.anyWriter()); // Window size appears to be left uninitialized unless we manually set it here. This // seems sketchy to me (tty fd should be nonblocking for the event loop) const size = try vaxis.Tty.getWinsize(tty.fd); vx.resize(alloc, tty.anyWriter(), size) catch @panic("TODO"); try app.server.listen(app.loop); try loop.run(.until_done); } fn eventCallback( ud: ?*RotInt, loop: *xev.Loop, watcher: *vaxis.xev.TtyWatcher(RotInt), event: vaxis.xev.Event, ) xev.CallbackAction { const app = ud orelse unreachable; switch (event) { .key_press => |key| keyp: { var mods = key.mods; mods.caps_lock = false; mods.num_lock = false; const scale: f64 = if (std.meta.eql(mods, .{ .shift = true })) 1 else 10; const delta: AzEl = switch (key.codepoint) { vaxis.Key.left, vaxis.Key.kp_left => .{ .az = -0.1 * scale, .el = 0 }, vaxis.Key.right, vaxis.Key.kp_right => .{ .az = 0.1 * scale, .el = 0 }, vaxis.Key.up, vaxis.Key.kp_up => .{ .az = 0, .el = 0.1 * scale }, vaxis.Key.down, vaxis.Key.kp_down => .{ .az = 0 * scale, .el = -0.1 * scale }, 'l' => { if (std.meta.eql(mods, .{ .ctrl = true })) app.vx.queueRefresh(); break :keyp; }, 'c' => { if (std.meta.eql(mods, .{ .ctrl = true })) { loop.stop(); return .disarm; } break :keyp; }, 'q' => { if (std.meta.eql(mods, .{})) { loop.stop(); return .disarm; } break :keyp; }, else => break :keyp, }; _ = delta; // state.offsets.az += delta.az; // state.offsets.el += delta.el; }, .winsize => |ws| { watcher.vx.resize(app.allocator, watcher.tty.anyWriter(), ws) catch return .disarm; }, else => {}, } return .rearm; } fn timerCallback( ud: ?*RotInt, loop: *xev.Loop, completion: *xev.Completion, err: xev.Timer.RunError!void, ) xev.CallbackAction { _ = err catch @panic("timer error"); var app = ud orelse return .disarm; app.draw() catch @panic("couldn't draw"); const timer = xev.Timer.init() catch unreachable; timer.run(loop, completion, 8, RotInt, app, timerCallback); return .disarm; }