const std = @import("std"); const Entity = @import("../ecs/entity.zig").Entity; const Action = @import("../actions.zig").Action; const ncurses = @cImport({ @cInclude("ncurses.h"); }); const locale = @cImport({ @cInclude("locale.h"); }); 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); pub const Display = struct { main: ?*ncurses.WINDOW, inst: ?*ncurses.WINDOW, msgs: ?*ncurses.WINDOW, stat: ?*ncurses.WINDOW, allocator: std.mem.Allocator, fn validTermSize() bool { return ncurses.LINES >= MIN_HEIGHT and ncurses.COLS >= MIN_WIDTH; } fn colorSupport() bool { return ncurses.has_colors(); } fn createNewWin(height: i32, width: i32, y: i32, x: i32) ?*ncurses.WINDOW { 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: *Display, 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 { self.displayMessage("Invalid key name: {d}", .{key}); } } fn handleResize(self: *Display) 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 (!validTermSize()) { _ = ncurses.mvprintw(0, 0, "Terminal must be at least %dx%d", @as(i32, @intCast(MIN_WIDTH)), @as(i32, @intCast(MIN_HEIGHT))); } else { self.inst = createInstructionPanel(); self.msgs = createMessagePanel(); self.stat = createStatisticsPanel(); self.main = createMainPanel(); self.displayInstructions(); } _ = ncurses.refresh(); return Action.illegal; } pub fn displayInstructions(self: *Display) 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); } pub fn displayMessage(self: *Display, comptime fmt: []const u8, args: anytype) void { for (1..MESSAGE_PANEL_WIDTH - 1) |i| { const i_32 = @as(i32, @intCast(i)); _ = ncurses.mvwaddch(self.msgs, 1, i_32, ' '); } var buf: [MESSAGE_PANEL_WIDTH:0]u8 = undefined; const data = std.fmt.bufPrint(&buf, fmt, args) catch { self.displayMessage("Message too long", .{}); return; }; buf[data.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); } pub fn displayStatus(self: *Display, entity: *const Entity) 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, ' '); } } _ = entity; _ = ncurses.wrefresh(self.stat); } pub fn processInput(self: *Display) Action { const ch = ncurses.getch(); 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() ?*ncurses.WINDOW { return 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, ); } fn createMessagePanel() ?*ncurses.WINDOW { return 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() ?*ncurses.WINDOW { return 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() ?*ncurses.WINDOW { return 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() !Display { _ = locale.setlocale(locale.LC_ALL, ""); if (ncurses.initscr() == null) { return error.CursesInitFail; } if (!validTermSize()) { _ = ncurses.endwin(); return error.InvalidTermSize; } if (!colorSupport()) { _ = ncurses.endwin(); return error.NoColorSupport; } _ = ncurses.raw(); _ = ncurses.keypad(ncurses.stdscr, true); _ = ncurses.noecho(); _ = ncurses.curs_set(0); _ = ncurses.start_color(); _ = 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(); var d = Display{ .inst = createInstructionPanel(), .msgs = createMessagePanel(), .stat = createStatisticsPanel(), .main = createMainPanel(), .allocator = undefined, }; d.displayInstructions(); return d; } fn deleteWindows(self: *Display) void { _ = ncurses.delwin(self.main); _ = ncurses.delwin(self.inst); _ = ncurses.delwin(self.msgs); _ = ncurses.delwin(self.stat); } pub fn deinit(self: *Display) void { self.deleteWindows(); _ = ncurses.endwin(); } };