//! This module provides an implementation of IO using the ncurses library. const std = @import("std"); const Action = @import("../actions.zig").Action; const ncurses = @cImport({ @cInclude("ncurses.h"); }); const locale = @cImport({ @cInclude("locale.h"); }); const TICK_MS = 33; const MAIN_PANEL_WIDTH = 100; const MAIN_PANEL_HEIGHT = 41; const INSTRUCTION_PANEL_WIDTH = 32; const INSTRUCTION_PANEL_HEIGHT = 39; const MESSAGE_PANEL_WIDTH = 100; const MESSAGE_PANEL_HEIGHT = 3; const STATUS_PANEL_WIDTH = 32; const STATUS_PANEL_HEIGHT = 5; const MIN_WIDTH = MAIN_PANEL_WIDTH + INSTRUCTION_PANEL_WIDTH; const MIN_HEIGHT = MAIN_PANEL_HEIGHT + MESSAGE_PANEL_HEIGHT; const KEY_MOVE_UP: i32 = 'k'; const KEY_MOVE_DOWN: i32 = 'j'; const KEY_MOVE_LEFT: i32 = 'h'; const KEY_MOVE_RIGHT: i32 = 'l'; const KEY_STAIR_UP: i32 = '<'; const KEY_STAIR_DOWN: i32 = '>'; const KEY_QUIT: i32 = ncurses.KEY_F(1); /// Provide an IO struct that manages the state of the display and user input pub const IO = struct { allocator: std.mem.Allocator, main: ?*ncurses.WINDOW, inst: ?*ncurses.WINDOW, msgs: ?*ncurses.WINDOW, stat: ?*ncurses.WINDOW, prev_tick_time: i64, fn validTermSize(self: *IO) bool { _ = self; // should be uncallable without initialization return ncurses.LINES >= MIN_HEIGHT and ncurses.COLS >= MIN_WIDTH; } fn colorSupport(self: *IO) bool { _ = self; // should be uncallable without initialization return ncurses.has_colors(); } fn createNewWin(self: *IO, height: i32, width: i32, y: i32, x: i32) ?*ncurses.WINDOW { _ = self; // should be uncallable without initialization const local_win = ncurses.newwin(height, width, y, x); _ = ncurses.box(local_win, 0, 0); _ = ncurses.wrefresh(local_win); return local_win; } fn formatInstruction(self: *IO, pos: i32, key: i32, description: [:0]const u8) void { const c_description: ?[*:0]const u8 = description.ptr; if (ncurses.KEY_F0 <= key and key <= ncurses.KEY_F(64)) { _ = ncurses.mvwprintw(self.inst, pos, 2, "F%d - %s", key - ncurses.KEY_F0, c_description); } else if (key < 128) { _ = ncurses.mvwprintw(self.inst, pos, 2, "%c - %s", key, c_description); } else { const str = std.fmt.allocPrintZ(self.allocator, "Invalid key name: {d}", .{key}) catch { return; }; self.displayMessage(str); self.allocator.free(str); } } fn createPanels(self: *IO) void { self.createInstructionPanel(); self.createMessagePanel(); self.createStatisticsPanel(); self.createMainPanel(); } fn handleResize(self: *IO) Action { const lines = @as(usize, @intCast(ncurses.LINES)); const cols = @as(usize, @intCast(ncurses.COLS)); for (0..lines) |i| { for (0..cols) |j| { const i_32 = @as(i32, @intCast(i)); const j_32 = @as(i32, @intCast(j)); _ = ncurses.mvaddch(i_32, j_32, ' '); } } _ = ncurses.refresh(); self.deleteWindows(); if (!self.validTermSize()) { _ = ncurses.mvprintw(0, 0, "Terminal must be at least %dx%d", @as(i32, @intCast(MIN_WIDTH)), @as(i32, @intCast(MIN_HEIGHT))); } else { self.createPanels(); } _ = ncurses.refresh(); return Action.illegal; } fn displayInstructions(self: *IO) void { self.formatInstruction(1, KEY_MOVE_LEFT, "move left"); self.formatInstruction(2, KEY_MOVE_DOWN, "move down"); self.formatInstruction(3, KEY_MOVE_UP, "move up"); self.formatInstruction(4, KEY_MOVE_RIGHT, "move right"); self.formatInstruction(5, KEY_STAIR_DOWN, "move down staircase"); self.formatInstruction(6, KEY_STAIR_UP, "move up staircase"); self.formatInstruction(7, KEY_QUIT, "quit"); _ = ncurses.wrefresh(self.inst); } /// Display a message in the message box. /// Takes a pre-formatted null-terminated string /// If the message is too wide for the box, display "Message too long" instead. pub fn displayMessage(self: *IO, str: [:0]const u8) void { if (self.msgs == null) return; for (1..MESSAGE_PANEL_WIDTH - 1) |i| { const i_32 = @as(i32, @intCast(i)); _ = ncurses.mvwaddch(self.msgs, 1, i_32, ' '); } if (str.len > MESSAGE_PANEL_WIDTH - 2) { self.displayMessage("Message too long"); return; } var buf: [MESSAGE_PANEL_WIDTH:0]u8 = undefined; std.mem.copyForwards(u8, &buf, str); buf[str.len] = 0; const msg: [:0]u8 = &buf; const c_msg: ?[*:0]const u8 = msg.ptr; _ = ncurses.mvwprintw(self.msgs, 1, 1, c_msg); _ = ncurses.wrefresh(self.msgs); } fn displayStatus(self: *IO) void { for (1..STATUS_PANEL_HEIGHT - 1) |i| { for (1..STATUS_PANEL_WIDTH - 1) |j| { const i_32 = @as(i32, @intCast(i)); const j_32 = @as(i32, @intCast(j)); _ = ncurses.mvwaddch(self.stat, i_32, j_32, ' '); } } // TODO: Implement _ = ncurses.wrefresh(self.stat); } /// An interface for user input and time processing. /// Waits for the end of a tick and returns a tick action. /// If input is given before the end of the tick, return that instead. pub fn waitForTick(self: *IO) Action { var new = std.time.milliTimestamp(); while (new - self.prev_tick_time <= TICK_MS) { const action = self.processInput(); if (action != Action.illegal) return action; new = std.time.milliTimestamp(); } self.prev_tick_time = new; return Action.tick; } fn processInput(self: *IO) Action { const ch = ncurses.getch(); if (!self.validTermSize()) { if (ch != KEY_QUIT and ch != ncurses.KEY_RESIZE) return Action.illegal; } return switch (ch) { KEY_MOVE_UP => Action.move_up, KEY_MOVE_DOWN => Action.move_down, KEY_MOVE_LEFT => Action.move_left, KEY_MOVE_RIGHT => Action.move_right, KEY_STAIR_DOWN => Action.down_stair, KEY_STAIR_UP => Action.up_stair, KEY_QUIT => Action.exit, ncurses.KEY_RESIZE => self.handleResize(), else => Action.illegal, }; } fn createInstructionPanel(self: *IO) void { self.inst = self.createNewWin( INSTRUCTION_PANEL_HEIGHT, INSTRUCTION_PANEL_WIDTH, @divTrunc(ncurses.LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT, 2) + 1, @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2) + MAIN_PANEL_WIDTH - 1, ); if (self.inst != null) { self.displayInstructions(); } } fn createMessagePanel(self: *IO) void { self.msgs = self.createNewWin( MESSAGE_PANEL_HEIGHT, MESSAGE_PANEL_WIDTH, @divTrunc(ncurses.LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT, 2) + MAIN_PANEL_HEIGHT, @divTrunc(ncurses.COLS - MESSAGE_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2), ); } fn createStatisticsPanel(self: *IO) void { self.stat = self.createNewWin( STATUS_PANEL_HEIGHT, STATUS_PANEL_WIDTH, @divTrunc(ncurses.LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT, 2) + INSTRUCTION_PANEL_HEIGHT, @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2) + MAIN_PANEL_WIDTH - 1, ); } fn createMainPanel(self: *IO) void { self.main = self.createNewWin( MAIN_PANEL_HEIGHT, MAIN_PANEL_WIDTH, @divTrunc(ncurses.LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT, 2) + 1, @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2), ); } pub fn init(allocator: std.mem.Allocator) !IO { _ = locale.setlocale(locale.LC_ALL, ""); var io = IO{ .allocator = allocator, .inst = null, .msgs = null, .stat = null, .main = null, .prev_tick_time = std.time.milliTimestamp(), }; if (ncurses.initscr() == null) { return error.CursesInitFail; } if (!io.colorSupport()) { _ = ncurses.endwin(); return error.NoColorSupport; } _ = ncurses.raw(); _ = ncurses.keypad(ncurses.stdscr, true); _ = ncurses.noecho(); _ = ncurses.curs_set(0); _ = ncurses.start_color(); _ = ncurses.timeout(1); _ = ncurses.init_pair(1, ncurses.COLOR_WHITE, ncurses.COLOR_BLACK); _ = ncurses.init_pair(2, ncurses.COLOR_BLACK, ncurses.COLOR_RED); _ = ncurses.wattron(ncurses.stdscr, ncurses.COLOR_PAIR(1)); _ = ncurses.refresh(); if (!io.validTermSize()) { _ = ncurses.mvprintw(0, 0, "Terminal must be at least %dx%d", @as(i32, @intCast(MIN_WIDTH)), @as(i32, @intCast(MIN_HEIGHT))); } else { io.createPanels(); } return io; } fn deleteWindows(self: *IO) void { _ = ncurses.delwin(self.main); self.main = null; _ = ncurses.delwin(self.inst); self.inst = null; _ = ncurses.delwin(self.msgs); self.msgs = null; _ = ncurses.delwin(self.stat); self.stat = null; } pub fn deinit(self: *IO) void { self.deleteWindows(); _ = ncurses.endwin(); } };