├── .gitignore ├── src ├── scroll.zig ├── main.zig ├── winapiGlue.zig ├── clear.zig ├── utils.zig ├── style.zig ├── cursor.zig ├── term.zig ├── color.zig └── event.zig ├── examples ├── color.zig ├── alternate_screen.zig └── event.zig ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | /zig-out 3 | -------------------------------------------------------------------------------- /src/scroll.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const lib = @import("main.zig"); 4 | const utils = lib.utils; 5 | 6 | /// Scrolls the terminal up by `n` lines. 7 | /// Lines that scroll off the top are discarded, and new lines appear at the bottom. 8 | pub fn up(writer: *std.io.Writer, n: anytype) !void { 9 | return writer.print(utils.csi ++ "{d}S", .{n}); 10 | } 11 | 12 | /// Scrolls the terminal down by `n` lines. 13 | /// Lines that scroll off the bottom are discarded, and new lines appear at the top. 14 | pub fn down(writer: *std.io.Writer, n: anytype) !void { 15 | return writer.print(utils.csi ++ "{d}T", .{n}); 16 | } 17 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const clear = @import("clear.zig"); 4 | pub const color = @import("color.zig"); 5 | pub const cursor = @import("cursor.zig"); 6 | pub const style = @import("style.zig"); 7 | pub const utils = @import("utils.zig"); 8 | pub const term = @import("term.zig"); 9 | pub const events = @import("event.zig"); 10 | pub const scroll = @import("scroll.zig"); 11 | 12 | pub const enableWindowsVTS = switch (@import("builtin").os.tag) { 13 | .windows => @import("utils.zig").enableWindowsVTS, 14 | else => @compileError("enableWindowsVTS is supported only on Windows"), 15 | }; 16 | 17 | test { 18 | _ = clear; 19 | _ = color; 20 | _ = cursor; 21 | _ = style; 22 | _ = utils; 23 | _ = term; 24 | _ = events; 25 | _ = scroll; 26 | } 27 | -------------------------------------------------------------------------------- /examples/color.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Io = std.Io; 5 | 6 | const mibu = @import("mibu"); 7 | const color = mibu.color; 8 | const cursor = mibu.cursor; 9 | 10 | pub fn main() !void { 11 | var stdout_buffer: [1]u8 = undefined; 12 | 13 | var stdout_file = std.fs.File.stdout(); 14 | var stdout_writer = stdout_file.writer(&stdout_buffer); 15 | const stdout = &stdout_writer.interface; 16 | 17 | if (builtin.os.tag == .windows) { 18 | try mibu.enableWindowsVTS(stdout.handle); 19 | } 20 | 21 | try stdout.print("{s}Warning text\n", .{color.print.fg(.red)}); 22 | 23 | try color.fg256(stdout, .blue); 24 | try stdout.print("Blue text\n", .{}); 25 | 26 | try color.fgRGB(stdout, 97, 37, 160); 27 | try stdout.print("Purple text\n", .{}); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Diego Barria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/alternate_screen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Io = std.Io; 5 | 6 | const mibu = @import("mibu"); 7 | const term = mibu.term; 8 | const color = mibu.color; 9 | const cursor = mibu.cursor; 10 | 11 | pub fn main() !void { 12 | var stdout_buffer: [1]u8 = undefined; 13 | 14 | var stdout_file = std.fs.File.stdout(); 15 | var stdout_writer = stdout_file.writer(&stdout_buffer); 16 | 17 | const stdout = &stdout_writer.interface; 18 | 19 | // we have to make sure that exitAlternateScreen 20 | // and cursor.show are flushed when the program exits. 21 | defer stdout.flush() catch {}; 22 | 23 | if (builtin.os.tag == .windows) { 24 | try mibu.enableWindowsVTS(stdout.handle); 25 | } 26 | 27 | try term.enterAlternateScreen(stdout); 28 | defer term.exitAlternateScreen(stdout) catch {}; 29 | 30 | try cursor.hide(stdout); 31 | defer cursor.show(stdout) catch {}; 32 | 33 | try cursor.goTo(stdout, 1, 1); 34 | try mibu.style.italic(stdout, true); 35 | try stdout.print("This is being shown in the alternate screen...", .{}); 36 | try stdout.flush(); 37 | 38 | std.Thread.sleep(std.time.ns_per_s * 2); 39 | } 40 | -------------------------------------------------------------------------------- /src/winapiGlue.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const windows = std.os.windows; 4 | const kernel32 = windows.kernel32; 5 | 6 | pub const ENABLE_PROCESSED_OUTPUT: windows.DWORD = 0x0001; 7 | pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING: windows.DWORD = 0x0004; 8 | pub const ENABLE_WINDOW_INPUT: windows.DWORD = 0x0008; 9 | pub const ENABLE_MOUSE_INPUT: windows.DWORD = 0x0010; 10 | pub const ENABLE_VIRTUAL_TERMINAL_INPUT: windows.DWORD = 0x0200; 11 | 12 | pub const DISABLE_NEWLINE_AUTO_RETURN: windows.DWORD = 0x0008; 13 | 14 | // https://learn.microsoft.com/en-us/windows/console/getconsolemode 15 | pub fn getConsoleMode(handle: windows.HANDLE) !windows.DWORD { 16 | var mode: windows.DWORD = 0; 17 | 18 | // nonzero value means success 19 | if (kernel32.GetConsoleMode(handle, &mode) == 0) { 20 | const err = kernel32.GetLastError(); 21 | return windows.unexpectedError(err); 22 | } 23 | 24 | return mode; 25 | } 26 | 27 | pub fn setConsoleMode(handle: windows.HANDLE, mode: windows.DWORD) !void { 28 | // nonzero value means success 29 | if (kernel32.SetConsoleMode(handle, mode) == 0) { 30 | const err = kernel32.GetLastError(); 31 | return windows.unexpectedError(err); 32 | } 33 | } 34 | 35 | pub fn getConsoleScreenBufferInfo(handle: windows.HANDLE) !windows.CONSOLE_SCREEN_BUFFER_INFO { 36 | var csbi: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; 37 | if (kernel32.GetConsoleScreenBufferInfo(handle, &csbi) == 0) { 38 | const err = kernel32.GetLastError(); 39 | return windows.unexpectedError(err); 40 | } 41 | return csbi; 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mibu 2 | 3 | **mibu** is a pure Zig library for low-level terminal manipulation. 4 | 5 | **Status:** This library is in beta. Breaking changes may occur. 6 | 7 | > Tested with zig version `0.15.1` (release) 8 | 9 | ## Features 10 | 11 | - Zero heap allocations. 12 | - UTF-8 character support. 13 | - Terminal raw mode support. 14 | - Text styling: bold, italic, underline. 15 | - Color output: supports 8, 16, and true color (24-bit). 16 | - Cursor movement and positioning functions. 17 | - Screen clearing and erasing utilities. 18 | - Key event handling: codepoints, modifiers, and special keys. 19 | - Mouse event handling: click, scroll, and release actions. 20 | 21 | ## How to use 22 | 23 | Add the library as a dependency in your `build.zig.zon` file: 24 | 25 | ```bash 26 | zig fetch --save git+https://github.com/xyaman/mibu 27 | ``` 28 | 29 | Import the dependency in your `build.zig` file: 30 | 31 | ```zig 32 | const mibu_dep = b.dependency("mibu", .{}); 33 | exe.root_module.addImport("mibu", mibu_dep.module("mibu")); 34 | ``` 35 | 36 | Use the library in your Zig code: 37 | 38 | ```zig 39 | const std = @import("std"); 40 | const mibu = @import("mibu"); 41 | const color = mibu.color; 42 | 43 | pub fn main() void { 44 | std.debug.print("{s}Hello World in purple!\n", .{color.print.bgRGB(97, 37, 160)}); 45 | } 46 | ``` 47 | 48 | ## Getting Started 49 | 50 | See the [examples directory](examples/). 51 | 52 | You can run the examples with the following command: 53 | 54 | ```bash 55 | # Prints text with different colors 56 | zig build color 57 | 58 | # Prints what key you pressed, until you press `q` or `ctrl+c` 59 | zig build event 60 | 61 | zig build alternate_screen 62 | ``` 63 | 64 | ## TODO 65 | 66 | - [ ] Mouse: Click and move (drag) 67 | 68 | ## Projects that use `mibu` 69 | 70 | - [zigtris](https://github.com/ringtailsoftware/zigtris) 71 | - [chip8 emulator (wip)](https://github.com/xyaman/chip8) 72 | - [2048 in zig](https://codeberg.org/Vulwsztyn/2048_zig) 73 | -------------------------------------------------------------------------------- /src/clear.zig: -------------------------------------------------------------------------------- 1 | //! Clear screen. 2 | //! Note: Clear doesn't move the cursor, so the cursor will stay at the same position, 3 | //! to move cursor check `Cursor`. 4 | 5 | const std = @import("std"); 6 | 7 | const lib = @import("main.zig"); 8 | const utils = lib.utils; 9 | 10 | pub const print = struct { 11 | /// Clear from cursor until end of screen 12 | pub const screen_from_cursor = utils.comptimeCsi("0J", .{}); 13 | 14 | /// Clear from cursor to beginning of screen 15 | pub const screen_to_cursor = utils.comptimeCsi("1J", .{}); 16 | 17 | /// Clear all screen 18 | pub const all = utils.comptimeCsi("2J", .{}); 19 | 20 | /// Clear from cursor to end of line 21 | pub const line_from_cursor = utils.comptimeCsi("0K", .{}); 22 | 23 | /// Clear start of line to the cursor 24 | pub const line_to_cursor = utils.comptimeCsi("1K", .{}); 25 | 26 | /// Clear entire line 27 | pub const line = utils.comptimeCsi("2K", .{}); 28 | }; 29 | 30 | /// Clear from cursor until end of screen 31 | pub fn screenFromCursor(writer: *std.Io.Writer) !void { 32 | return writer.print(utils.csi ++ utils.clear_screen_from_cursor, .{}); 33 | } 34 | 35 | /// Clear from cursor to beginning of screen 36 | pub fn screenToCursor(writer: *std.Io.Writer) !void { 37 | return writer.print(utils.csi ++ utils.clear_screen_to_cursor, .{}); 38 | } 39 | 40 | /// Clear all screen 41 | pub fn all(writer: *std.Io.Writer) !void { 42 | return writer.print(utils.csi ++ utils.clear_all, .{}); 43 | } 44 | 45 | /// Clear from cursor to end of line 46 | pub fn lineFromCursor(writer: *std.Io.Writer) !void { 47 | return writer.print(utils.csi ++ utils.clear_line_from_cursor, .{}); 48 | } 49 | 50 | /// Clear start of line to the cursor 51 | pub fn lineToCursor(writer: *std.Io.Writer) !void { 52 | return writer.print(utils.csi ++ utils.clear_line_to_cursor, .{}); 53 | } 54 | 55 | /// Clear entire line 56 | pub fn entireLine(writer: *std.Io.Writer) !void { 57 | return writer.print(utils.csi ++ utils.clear_line, .{}); 58 | } 59 | -------------------------------------------------------------------------------- /examples/event.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Io = std.Io; 5 | 6 | const mibu = @import("mibu"); 7 | const events = mibu.events; 8 | const term = mibu.term; 9 | 10 | pub fn main() !void { 11 | var stdout_buffer: [1024]u8 = undefined; 12 | 13 | const stdin = std.fs.File.stdin(); 14 | var stdout_file = std.fs.File.stdout(); 15 | var stdout_writer = stdout_file.writer(&stdout_buffer); 16 | const stdout = &stdout_writer.interface; 17 | 18 | if (!std.posix.isatty(stdin.handle)) { 19 | try stdout.print("The current file descriptor is not a referring to a terminal.\n", .{}); 20 | return; 21 | } 22 | 23 | if (builtin.os.tag == .windows) { 24 | try mibu.enableWindowsVTS(stdout.handle); 25 | } 26 | 27 | // Enable terminal raw mode, its very recommended when listening for events 28 | var raw_term = try term.enableRawMode(stdin.handle); 29 | defer raw_term.disableRawMode() catch {}; 30 | 31 | // To listen mouse events, we need to enable mouse tracking 32 | try stdout.print("{s}", .{mibu.utils.enable_mouse_tracking}); 33 | defer stdout.print("{s}", .{mibu.utils.disable_mouse_tracking}) catch {}; 34 | 35 | try stdout.print("Press q or Ctrl-C to exit...\n\r", .{}); 36 | 37 | while (true) { 38 | const next = try events.nextWithTimeout(stdin, 1000); 39 | switch (next) { 40 | .key => |k| switch (k.code) { 41 | .char => |char| { 42 | if (k.mods.ctrl and char == 'c') { 43 | break; 44 | } 45 | try stdout.print("Pressed: {f}\n\r", .{k}); 46 | }, 47 | else => {}, 48 | }, 49 | .mouse => |m| try stdout.print("Mouse: {f}\n\r", .{m}), 50 | .timeout => try stdout.print("Timeout.\n\r", .{}), 51 | 52 | // ex. mouse events not supported yet 53 | else => try stdout.print("Event: {any}\n\r", .{next}), 54 | } 55 | 56 | try stdout.flush(); 57 | } 58 | 59 | try stdout.print("Bye bye\n\r", .{}); 60 | } 61 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const windows = std.os.windows; 3 | 4 | const winapiGlue = @import("winapiGlue.zig"); 5 | 6 | pub const csi = "\x1b["; 7 | 8 | /// Sequence to set foreground color using 256 colors table 9 | pub const fg_256 = "38;5;"; 10 | 11 | /// Sequence to set foreground color using 256 colors table 12 | pub const bg_256 = "48;5;"; 13 | 14 | /// Sequence to set foreground color using 256 colors table 15 | pub const fg_rgb = "38;2;"; 16 | 17 | /// Sequence to set foreground color using 256 colors table 18 | pub const bg_rgb = "48;2;"; 19 | 20 | /// Sequence to reset color and style 21 | pub const reset_all = "0m"; 22 | 23 | /// Sequence to clear from cursor until end of screen 24 | pub const clear_screen_from_cursor = "0J"; 25 | 26 | /// Sequence to clear from beginning to cursor. 27 | pub const clear_screen_to_cursor = "1J"; 28 | 29 | /// Sequence to clear all screen 30 | pub const clear_all = "2J"; 31 | 32 | /// Clear from cursor to end of line 33 | pub const clear_line_from_cursor = "0K"; 34 | 35 | /// Clear start of line to the cursor 36 | pub const clear_line_to_cursor = "1K"; 37 | 38 | /// Clear entire line 39 | pub const clear_line = "2K"; 40 | 41 | /// Returns the ANSI sequence to set bold mode 42 | pub const style_bold = "1m"; 43 | pub const style_no_bold = "22m"; 44 | 45 | /// Returns the ANSI sequence to set dim mode 46 | pub const style_dim = "2m"; 47 | pub const style_no_dim = "22m"; 48 | 49 | /// Returnstyle_s the ANSI sequence to set italic mode 50 | pub const style_italic = "3m"; 51 | pub const style_no_italic = "23m"; 52 | 53 | /// Returnstyle_s the ANSI sequence to set underline mode 54 | pub const style_underline = "4m"; 55 | pub const style_no_underline = "24m"; 56 | 57 | /// Returnstyle_s the ANSI sequence to set blinking mode 58 | pub const style_blinking = "5m"; 59 | pub const style_no_blinking = "25m"; 60 | 61 | /// Returnstyle_s the ANSI sequence to set reverse mode 62 | pub const style_reverse = "7m"; 63 | pub const style_no_reverse = "27m"; 64 | 65 | /// Returnstyle_s the ANSI sequence to set hidden/invisible mode 66 | pub const style_invisible = "8m"; 67 | pub const style_no_invisible = "28m"; 68 | 69 | /// Returnstyle_s the ANSI sequence to set strikethrough mode 70 | pub const style_strikethrough = "9m"; 71 | pub const style_no_strikethrough = "29m"; 72 | 73 | /// When enable_mouse_tracking is sent to the terminal 74 | /// mouse events will be received. 75 | /// Don't forget to call disable_mouse_tracking afteruse. 76 | pub const enable_mouse_tracking = "\x1b[?1003h"; 77 | 78 | /// When disable_mouse_tracking is sent to the terminal 79 | /// mouse events will stop being received. Needs to be 80 | /// called after enable_mouse_tracking, otherwise the 81 | /// terminal will not stop sending mouse events, even when the program 82 | /// has finished. 83 | pub const disable_mouse_tracking = "\x1b[?1003l"; 84 | 85 | pub inline fn comptimeCsi(comptime fmt: []const u8, args: anytype) []const u8 { 86 | const str = "\x1b[" ++ fmt; 87 | return std.fmt.comptimePrint(str, args); 88 | } 89 | 90 | /// Ensure that the current console has enabled support for Virtual Terminal Sequencies (VTS). 91 | /// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing 92 | /// TODO: should we add a disable method? 93 | pub fn enableWindowsVTS(handle: windows.HANDLE) !void { 94 | const old_mode = try winapiGlue.getConsoleMode(handle); 95 | 96 | const mode: windows.DWORD = old_mode | winapiGlue.ENABLE_PROCESSED_OUTPUT | winapiGlue.ENABLE_VIRTUAL_TERMINAL_PROCESSING; 97 | try winapiGlue.setConsoleMode(handle, mode); 98 | } 99 | -------------------------------------------------------------------------------- /src/style.zig: -------------------------------------------------------------------------------- 1 | const utils = @import("main.zig").utils; 2 | const std = @import("std"); 3 | 4 | pub const print = struct { 5 | /// Returns the ANSI sequence as a []const u8 6 | pub const reset = utils.comptimeCsi(utils.reset, .{}); 7 | 8 | /// Returns the ANSI sequence to set bold mode 9 | pub const bold = utils.comptimeCsi(utils.style_bold, .{}); 10 | pub const no_bold = utils.comptimeCsi(utils.style_no_bold, .{}); 11 | 12 | /// Returns the ANSI sequence to set dim mode 13 | pub const dim = utils.comptimeCsi(utils.style_dim, .{}); 14 | pub const no_dim = utils.comptimeCsi(utils.style_no_dim, .{}); 15 | 16 | /// Returns the ANSI sequence to set italic mode 17 | pub const italic = utils.comptimeCsi(utils.style_italic, .{}); 18 | pub const no_italic = utils.comptimeCsi(utils.style_no_italic, .{}); 19 | 20 | /// Returns the ANSI sequence to set underline mode 21 | pub const underline = utils.comptimeCsi(utils.style_underline, .{}); 22 | pub const no_underline = utils.comptimeCsi(utils.style_no_underline, .{}); 23 | 24 | /// Returns the ANSI sequence to set blinking mode 25 | pub const blinking = utils.comptimeCsi(utils.style_blinking, .{}); 26 | pub const no_blinking = utils.comptimeCsi(utils.style_no_blinking, .{}); 27 | 28 | /// Returns the ANSI sequence to set reverse mode 29 | pub const reverse = utils.comptimeCsi(utils.style_reverse, .{}); 30 | pub const no_reverse = utils.comptimeCsi(utils.style_no_reverse, .{}); 31 | 32 | /// Returns the ANSI sequence to set hidden/invisible mode 33 | pub const invisible = utils.comptimeCsi(utils.style_invisible, .{}); 34 | pub const no_invisible = utils.comptimeCsi(utils.style_no_invisible, .{}); 35 | 36 | /// Returns the ANSI sequence to set strikethrough mode 37 | pub const strikethrough = utils.comptimeCsi(utils.style_strikethrough, .{}); 38 | pub const no_strikethrough = utils.comptimeCsi(utils.style_no_strikethrough, .{}); 39 | }; 40 | 41 | /// Returns the ANSI sequence as a []const u8 42 | pub fn reset(writer: *std.Io.Writer) !void { 43 | return writer.print(print.reset, .{}); 44 | } 45 | 46 | /// Outputs the ANSI sequence to set/unset bold mode 47 | pub fn bold(writer: *std.io.Writer, v: bool) !void { 48 | return if (v) writer.print(print.bold, .{}) else writer.print(print.no_bold, .{}); 49 | } 50 | 51 | /// Outputs the ANSI sequence to set/unset dim mode 52 | pub fn dim(writer: *std.io.Writer, v: bool) !void { 53 | return if (v) writer.print(print.dim, .{}) else writer.print(print.no_dim, .{}); 54 | } 55 | 56 | /// Outputs the ANSI sequence to set/unset italic mode 57 | pub fn italic(writer: *std.io.Writer, v: bool) !void { 58 | return if (v) writer.print(print.italic, .{}) else writer.print(print.no_italic, .{}); 59 | } 60 | 61 | /// Outputs the ANSI sequence to set/unset underline mode 62 | pub fn underline(writer: *std.io.Writer, v: bool) !void { 63 | return if (v) writer.print(print.underline, .{}) else writer.print(print.no_underline, .{}); 64 | } 65 | 66 | /// Outputs the ANSI sequence to set/unset blinking mode 67 | pub fn blinking(writer: *std.io.Writer, v: bool) !void { 68 | return if (v) writer.print(print.blinking, .{}) else writer.print(print.no_blinking, .{}); 69 | } 70 | 71 | /// Outputs the ANSI sequence to set/unset reverse mode 72 | pub fn reverse(writer: *std.io.Writer, v: bool) !void { 73 | return if (v) writer.print(print.reverse, .{}) else writer.print(print.no_reverse, .{}); 74 | } 75 | 76 | /// Outputs the ANSI sequence to set/unset hidden/invisible mode 77 | pub fn hidden(writer: *std.io.Writer, v: bool) !void { 78 | return if (v) writer.print(print.invisible, .{}) else writer.print(print.no_invisible, .{}); 79 | } 80 | 81 | /// Outputs the ANSI sequence to set/unset strikethrough mode 82 | pub fn strikethrough(writer: *std.io.Writer, v: bool) !void { 83 | return if (v) writer.print(print.strikethrough, .{}) else writer.print(print.no_strikethrough, .{}); 84 | } 85 | -------------------------------------------------------------------------------- /src/cursor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | 4 | const lib = @import("main.zig"); 5 | const utils = lib.utils; 6 | 7 | pub const print = struct { 8 | /// Moves cursor to `x` column and `y` row 9 | pub inline fn goTo(x: anytype, y: anytype) []const u8 { 10 | // i guess is ok with this size for now 11 | var buf: [30]u8 = undefined; 12 | return fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ y, x }) catch unreachable; 13 | } 14 | 15 | /// Moves cursor up `y` rows 16 | pub inline fn goUp(y: anytype) []const u8 { 17 | // i guess is ok with this size for now 18 | var buf: [30]u8 = undefined; 19 | return fmt.bufPrint(&buf, "\x1b[{d}A", .{y}) catch unreachable; 20 | } 21 | 22 | /// Moves cursor down `y` rows 23 | pub inline fn goDown(y: anytype) []const u8 { 24 | // i guess is ok with this size for now 25 | var buf: [30]u8 = undefined; 26 | return fmt.bufPrint(&buf, "\x1b[{d}B", .{y}) catch unreachable; 27 | } 28 | 29 | /// Moves cursor left `x` columns 30 | pub inline fn goLeft(x: anytype) []const u8 { 31 | // i guess is ok with this size for now 32 | var buf: [30]u8 = undefined; 33 | return fmt.bufPrint(&buf, "\x1b[{d}D", .{x}) catch unreachable; 34 | } 35 | 36 | /// Moves cursor right `x` columns 37 | pub inline fn goRight(x: anytype) []const u8 { 38 | // i guess is ok with this size for now 39 | var buf: [30]u8 = undefined; 40 | return fmt.bufPrint(&buf, "\x1b[{d}C", .{x}) catch unreachable; 41 | } 42 | 43 | /// Hide the cursor 44 | pub inline fn hide() []const u8 { 45 | return utils.comptimeCsi("?25l", .{}); 46 | } 47 | 48 | /// Show the cursor 49 | pub inline fn show() []const u8 { 50 | return utils.comptimeCsi("?25h", .{}); 51 | } 52 | 53 | /// Save cursor position 54 | pub inline fn save() []const u8 { 55 | return utils.comptimeCsi("s", .{}); 56 | } 57 | 58 | /// Restore cursor position 59 | pub inline fn restore() []const u8 { 60 | return utils.comptimeCsi("u", .{}); 61 | } 62 | }; 63 | 64 | /// Moves cursor to `x` column and `y` row 65 | pub fn goTo(writer: *std.Io.Writer, x: anytype, y: anytype) !void { 66 | return writer.print(utils.csi ++ "{d};{d}H", .{ y, x }); 67 | } 68 | 69 | /// Moves cursor up `y` rows 70 | pub fn goUp(writer: *std.Io.Writer, y: anytype) !void { 71 | return writer.print(utils.csi ++ "{d}A", .{y}); 72 | } 73 | 74 | /// Moves cursor down `y` rows 75 | pub fn goDown(writer: *std.Io.Writer, y: anytype) !void { 76 | return writer.print(utils.csi ++ "{d}B", .{y}); 77 | } 78 | 79 | /// Moves cursor left `x` columns 80 | pub fn goLeft(writer: *std.Io.Writer, x: anytype) !void { 81 | return writer.print(utils.csi ++ "{d}D", .{x}); 82 | } 83 | 84 | /// Moves cursor right `x` columns 85 | pub fn goRight(writer: *std.Io.Writer, x: anytype) !void { 86 | return writer.print(utils.csi ++ "{d}C", .{x}); 87 | } 88 | 89 | /// Hide the cursor 90 | pub fn hide(writer: *std.Io.Writer) !void { 91 | return writer.print(utils.csi ++ "?25l", .{}); 92 | } 93 | 94 | /// Show the cursor 95 | pub fn show(writer: *std.Io.Writer) !void { 96 | return writer.print(utils.csi ++ "?25h", .{}); 97 | } 98 | 99 | /// Save cursor position 100 | pub fn save(writer: *std.Io.Writer) !void { 101 | return writer.print(utils.csi ++ "s", .{}); 102 | } 103 | 104 | /// Restore cursor position 105 | pub fn restore(writer: *std.Io.Writer) !void { 106 | return writer.print(utils.csi ++ "u", .{}); 107 | } 108 | 109 | pub const Position = struct { 110 | row: usize, 111 | col: usize, 112 | }; 113 | 114 | /// Returns the cursor's coordinates. The terminal needs to be 115 | /// in raw mode or at least have echo disabled. 116 | pub fn getPosition(in: *std.Io.Reader, out: *std.io.Writer) !Position { 117 | try out.print("\x1b[6n", .{}); 118 | try out.flush(); 119 | 120 | var buf: [6]u8 = undefined; 121 | const bytes = try in.readSliceShort(&buf); 122 | const data = buf[0..bytes]; 123 | 124 | // example response: \x1B[12;45R 125 | if (data[0] != 0x1B or data[1] != '[') { 126 | return error.InvalidResponse; 127 | } 128 | 129 | var it = std.mem.tokenizeAny(u8, data[2..], ";R"); 130 | const row_str = it.next() orelse return error.InvalidResponse; 131 | const col_str = it.next() orelse return error.InvalidResponse; 132 | 133 | const row = try std.fmt.parseUnsigned(usize, row_str, 10); 134 | const col = try std.fmt.parseUnsigned(usize, col_str, 10); 135 | return .{ .row = row, .col = col }; 136 | } 137 | -------------------------------------------------------------------------------- /src/term.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const io = std.io; 4 | const posix = std.posix; 5 | const windows = std.os.windows; 6 | 7 | const utils = @import("utils.zig"); 8 | const winapiGlue = @import("winapiGlue.zig"); 9 | 10 | const builtin = @import("builtin"); 11 | 12 | pub fn enableRawMode(handle: std.fs.File.Handle) !RawTerm { 13 | switch (builtin.os.tag) { 14 | .linux => return enableRawModePosix(handle), 15 | .macos => return enableRawModePosix(handle), 16 | .windows => return enableRawModeWindows(handle), 17 | else => return error.UnsupportedPlatform, 18 | } 19 | } 20 | 21 | fn enableRawModePosix(handle: posix.fd_t) !RawTerm { 22 | const original_termios = try posix.tcgetattr(handle); 23 | 24 | var termios = original_termios; 25 | 26 | // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html 27 | // TCSETATTR(3) 28 | // reference: void cfmakeraw(struct termios *t) 29 | 30 | termios.iflag.BRKINT = false; 31 | termios.iflag.ICRNL = false; 32 | termios.iflag.INPCK = false; 33 | termios.iflag.ISTRIP = false; 34 | termios.iflag.IXON = false; 35 | 36 | termios.oflag.OPOST = false; 37 | 38 | termios.lflag.ECHO = false; 39 | termios.lflag.ICANON = false; 40 | termios.lflag.IEXTEN = false; 41 | termios.lflag.ISIG = false; 42 | 43 | termios.cflag.CSIZE = .CS8; 44 | 45 | termios.cc[@intFromEnum(posix.V.MIN)] = 1; 46 | termios.cc[@intFromEnum(posix.V.TIME)] = 0; 47 | 48 | // apply changes 49 | try posix.tcsetattr(handle, .FLUSH, termios); 50 | 51 | return RawTerm{ 52 | .context = original_termios, 53 | .handle = handle, 54 | }; 55 | } 56 | 57 | fn enableRawModeWindows(handle: windows.HANDLE) !RawTerm { 58 | const old_mode = try winapiGlue.getConsoleMode(handle); 59 | 60 | const mode: windows.DWORD = winapiGlue.ENABLE_MOUSE_INPUT | winapiGlue.ENABLE_WINDOW_INPUT; 61 | try winapiGlue.setConsoleMode(handle, mode); 62 | 63 | return RawTerm{ 64 | .context = old_mode, 65 | .handle = handle, 66 | }; 67 | } 68 | 69 | /// A raw terminal representation, you can enter terminal raw mode 70 | /// using this struct. Raw mode is essential to create a TUI. 71 | pub const RawTerm = struct { 72 | context: switch (builtin.os.tag) { 73 | .windows => windows.DWORD, 74 | else => posix.termios, 75 | }, 76 | 77 | /// The OS-specific file descriptor or file handle. 78 | handle: std.fs.File.Handle, 79 | 80 | const Self = @This(); 81 | 82 | /// Returns to the previous terminal state 83 | pub fn disableRawMode(self: *Self) !void { 84 | switch (builtin.os.tag) { 85 | .linux => try self.disableRawModePosix(), 86 | .macos => try self.disableRawModePosix(), 87 | .windows => try self.disableRawModeWindows(), 88 | else => return error.UnsupportedPlatform, 89 | } 90 | } 91 | 92 | fn disableRawModePosix(self: *Self) !void { 93 | try posix.tcsetattr(self.handle, .FLUSH, self.context); 94 | } 95 | 96 | fn disableRawModeWindows(self: *Self) !void { 97 | try winapiGlue.setConsoleMode(self.handle, self.context); 98 | } 99 | }; 100 | 101 | /// Returned by `getSize()` 102 | pub const TermSize = struct { 103 | width: u16, 104 | height: u16, 105 | }; 106 | 107 | /// Get the terminal size, use `fd` equals to 0 use stdin 108 | pub fn getSize(handle: std.fs.File.Handle) !TermSize { 109 | switch (builtin.os.tag) { 110 | .linux => return getSizePosix(handle), 111 | .macos => return getSizePosix(handle), 112 | .windows => return getSizeWindows(handle), 113 | else => return error.UnsupportedPlatform, 114 | } 115 | } 116 | 117 | fn getSizePosix(fd: posix.fd_t) !TermSize { 118 | var ws: posix.winsize = undefined; 119 | 120 | // tty_ioctl(4) 121 | const err = std.posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&ws)); 122 | if (posix.errno(err) != .SUCCESS) { 123 | return error.IoctlError; 124 | } 125 | 126 | return TermSize{ 127 | .width = ws.col, 128 | .height = ws.row, 129 | }; 130 | } 131 | 132 | fn getSizeWindows(handle: windows.HANDLE) !TermSize { 133 | const csbi = try winapiGlue.getConsoleScreenBufferInfo(handle); 134 | 135 | return TermSize{ 136 | .width = @intCast(csbi.srWindow.Right - csbi.srWindow.Left + 1), 137 | .height = @intCast(csbi.srWindow.Bottom - csbi.srWindow.Top + 1), 138 | }; 139 | } 140 | 141 | /// Switches to an alternate screen mode in the console. 142 | /// `out`: needs to be writer 143 | pub fn enterAlternateScreen(writer: *std.Io.Writer) !void { 144 | try writer.print("{s}", .{utils.comptimeCsi("?1049h", .{})}); 145 | } 146 | 147 | /// Returns the console to its normal screen mode after using the alternate screen mode. 148 | /// `out`: needs to be writer 149 | pub fn exitAlternateScreen(writer: *std.Io.Writer) !void { 150 | try writer.print("{s}", .{utils.comptimeCsi("?1049l", .{})}); 151 | } 152 | -------------------------------------------------------------------------------- /src/color.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | 4 | const utils = @import("main.zig").utils; 5 | 6 | /// 256 colors 7 | /// https://www.ditig.com/256-colors-cheat-sheet 8 | pub const Color = enum(u8) { 9 | // System colors (0–15) 10 | black = 0, 11 | maroon = 1, 12 | green = 2, 13 | olive = 3, 14 | navy = 4, 15 | purple = 5, 16 | teal = 6, 17 | silver = 7, 18 | grey = 8, 19 | red = 9, 20 | lime = 10, 21 | yellow = 11, 22 | blue = 12, 23 | fuchsia = 13, 24 | aqua = 14, 25 | white = 15, 26 | 27 | // Non-system colors (16–255) 28 | grey_0 = 16, 29 | navy_blue = 17, 30 | dark_blue = 18, 31 | blue_3 = 19, 32 | blue_3b = 20, 33 | blue_1 = 21, 34 | dark_green = 22, 35 | deep_sky_blue_4 = 23, 36 | deep_sky_blue_4b = 24, 37 | deep_sky_blue_4c = 25, 38 | 39 | dodger_blue_3 = 26, 40 | dodger_blue_2 = 27, 41 | green_4 = 28, 42 | 43 | spring_green_4 = 29, 44 | turquoise_4 = 30, 45 | 46 | deep_sky_blue_3 = 31, 47 | deep_sky_blue_3b = 32, 48 | dodger_blue_1 = 33, 49 | green_3 = 34, 50 | spring_green_3 = 35, 51 | dark_cyan = 36, 52 | light_sea_green = 37, 53 | deep_sky_blue_2 = 38, 54 | deep_sky_blue_1 = 39, 55 | green_3_2 = 40, 56 | spring_green_3b = 41, 57 | spring_green_2 = 42, 58 | cyan_3 = 43, 59 | dark_turquoise = 44, 60 | turquoise_2 = 45, 61 | green_1 = 46, 62 | spring_green_2b = 47, 63 | spring_green_1 = 48, 64 | medium_spring_green = 49, 65 | cyan_2 = 50, 66 | cyan_1 = 51, 67 | dark_red = 52, 68 | deep_pink_4 = 53, 69 | purple_4 = 54, 70 | purple_4b = 55, 71 | purple_3 = 56, 72 | blue_violet = 57, 73 | orange_4 = 58, 74 | grey_37 = 59, 75 | medium_purple_4 = 60, 76 | slate_blue_3 = 61, 77 | slate_blue_3b = 62, 78 | royal_blue_1 = 63, 79 | chartreuse_4 = 64, 80 | dark_sea_green_4 = 65, 81 | pale_turquoise_4 = 66, 82 | steel_blue = 67, 83 | steel_blue_3 = 68, 84 | cornflower_blue = 69, 85 | chartreuse_3 = 70, 86 | dark_sea_green_4b = 71, 87 | cadet_blue = 72, 88 | cadet_blue_2 = 73, 89 | sky_blue_3 = 74, 90 | steel_blue_1 = 75, 91 | chartreuse_3b = 76, 92 | pale_green_3 = 77, 93 | sea_green_3 = 78, 94 | aquamarine_3 = 79, 95 | medium_turquoise = 80, 96 | steel_blue_1b = 81, 97 | chartreuse_2 = 82, 98 | sea_green_2 = 83, 99 | sea_green_1 = 84, 100 | sea_green_1b = 85, 101 | aquamarine_1 = 86, 102 | dark_slate_gray_2 = 87, 103 | dark_red_2 = 88, 104 | deep_pink_4b = 89, 105 | dark_magenta = 90, 106 | dark_magenta_2 = 91, 107 | dark_violet = 92, 108 | purple_2 = 93, 109 | orange_4b = 94, 110 | light_pink_4 = 95, 111 | plum_4 = 96, 112 | medium_purple_3 = 97, 113 | medium_purple_3b = 98, 114 | slate_blue_1b = 99, 115 | yellow_4 = 100, 116 | wheat_4 = 101, 117 | grey_53 = 102, 118 | light_slate_grey = 103, 119 | medium_purple = 104, 120 | light_slate_blue = 105, 121 | yellow_4b = 106, 122 | dark_olive_green_3 = 107, 123 | dark_sea_green = 108, 124 | light_sky_blue_3 = 109, 125 | light_sky_blue_3b = 110, 126 | sky_blue_2 = 111, 127 | chartreuse_2b = 112, 128 | dark_olive_green_3b = 113, 129 | pale_green_3b = 114, 130 | dark_sea_green_3 = 115, 131 | dark_slate_gray_3 = 116, 132 | sky_blue_1 = 117, 133 | chartreuse_1 = 118, 134 | light_green = 119, 135 | light_green_2 = 120, 136 | pale_green_1 = 121, 137 | aquamarine_1b = 122, 138 | dark_slate_gray_1 = 123, 139 | red_3 = 124, 140 | deep_pink_4c = 125, 141 | medium_violet_red = 126, 142 | magenta_3 = 127, 143 | dark_violet_2 = 128, 144 | purple_3b = 129, 145 | dark_orange_3 = 130, 146 | indian_red = 131, 147 | hot_pink_3 = 132, 148 | medium_orchid_3 = 133, 149 | medium_orchid = 134, 150 | medium_purple_2 = 135, 151 | dark_goldenrod = 136, 152 | light_salmon_3 = 137, 153 | rosy_brown = 138, 154 | grey_63 = 139, 155 | medium_purple_2b = 140, 156 | medium_purple_1 = 141, 157 | gold_3 = 142, 158 | dark_khaki = 143, 159 | navajo_white_3 = 144, 160 | grey_69 = 145, 161 | light_steel_blue_3 = 146, 162 | light_steel_blue = 147, 163 | yellow_3 = 148, 164 | bark_olive_green_3c = 149, 165 | dark_sea_green_3b = 150, 166 | dark_sea_green_2 = 151, 167 | light_cyan_3 = 152, 168 | light_sky_blue_1 = 153, 169 | green_yellow = 154, 170 | dark_olive_green_2 = 155, 171 | pale_green_1b = 156, 172 | dark_sea_green_2b = 157, 173 | dark_sea_green_1 = 158, 174 | pale_turquoise_1 = 159, 175 | red_3b = 160, 176 | deep_pink_3 = 161, 177 | deep_pink_3b = 162, 178 | magenta_3b = 163, 179 | magenta_3c = 164, 180 | magenta_2 = 165, 181 | dark_orange_3b = 166, 182 | indian_red_2 = 167, 183 | hot_pink_3b = 168, 184 | hot_pink_2 = 169, 185 | orchid = 170, 186 | medium_orchid_1 = 171, 187 | orange_3 = 172, 188 | light_salmon_3b = 173, 189 | light_pink_3 = 174, 190 | pink_3 = 175, 191 | plum_3 = 176, 192 | violet = 177, 193 | gold_3b = 178, 194 | light_goldenrod_3 = 179, 195 | tan = 180, 196 | misty_rose_3 = 181, 197 | thistle_3 = 182, 198 | plum_2 = 183, 199 | yellow_3b = 184, 200 | khaki_3 = 185, 201 | light_goldenrod_2 = 186, 202 | light_yellow_3 = 187, 203 | grey_84 = 188, 204 | light_steel_blue_1 = 189, 205 | yellow_2 = 190, 206 | dark_olive_green_1 = 191, 207 | dark_olive_green_1b = 192, 208 | dark_sea_green_1b = 193, 209 | honeydew_2 = 194, 210 | light_cyan_1 = 195, 211 | red_1 = 196, 212 | deep_pink_2 = 197, 213 | deep_pink_1 = 198, 214 | deep_pink_1b = 199, 215 | magenta_2b = 200, 216 | magenta_1 = 201, 217 | orange_red_1 = 202, 218 | indian_red_1 = 203, 219 | indian_red_1b = 204, 220 | hot_pink = 205, 221 | hot_pink_2b = 206, 222 | medium_orchid_1b = 207, 223 | dark_orange = 208, 224 | salmon_1 = 209, 225 | light_coral = 210, 226 | pale_violet_red_1 = 211, 227 | orchid_2 = 212, 228 | orchid_1 = 213, 229 | orange_1 = 214, 230 | sandy_brown = 215, 231 | light_salmon_1 = 216, 232 | light_pink_1 = 217, 233 | pink_1 = 218, 234 | plum_1 = 219, 235 | gold_1 = 220, 236 | light_goldenrod_2b = 221, 237 | light_goldenrod_2c = 222, 238 | navajo_white_1 = 223, 239 | misty_rose_1 = 224, 240 | thistle_1 = 225, 241 | yellow_1 = 226, 242 | light_goldenrod_1 = 227, 243 | khaki_1 = 228, 244 | wheat_1 = 229, 245 | cornsilk_1 = 230, 246 | grey_100 = 231, 247 | grey_3 = 232, 248 | grey_7 = 233, 249 | grey_11 = 234, 250 | grey_15 = 235, 251 | grey_19 = 236, 252 | grey_23 = 237, 253 | grey_27 = 238, 254 | grey_30 = 239, 255 | grey_35 = 240, 256 | grey_39 = 241, 257 | grey_42 = 242, 258 | grey_46 = 243, 259 | grey_50 = 244, 260 | grey_54 = 245, 261 | grey_58 = 246, 262 | grey_62 = 247, 263 | grey_66 = 248, 264 | grey_70 = 249, 265 | grey_74 = 250, 266 | grey_78 = 251, 267 | grey_82 = 252, 268 | grey_85 = 253, 269 | grey_89 = 254, 270 | grey_93 = 255, 271 | }; 272 | 273 | pub const print = struct { 274 | /// Returns a string to change text foreground using 256 colors 275 | pub inline fn fg(comptime color: Color) []const u8 { 276 | return utils.comptimeCsi("38;5;{d}m", .{@intFromEnum(color)}); 277 | } 278 | 279 | /// Returns a string to change text background using 256 colors 280 | pub inline fn bg(comptime color: Color) []const u8 { 281 | return utils.comptimeCsi("48;5;{d}m", .{@intFromEnum(color)}); 282 | } 283 | 284 | /// Returns a string to change text foreground using rgb colors 285 | /// Uses a buffer. 286 | pub inline fn fgRGB(r: u8, g: u8, b: u8) []const u8 { 287 | var buf: [22]u8 = undefined; 288 | return fmt.bufPrint(&buf, "\x1b[38;2;{d};{d};{d}m", .{ r, g, b }) catch unreachable; 289 | } 290 | 291 | /// Returns a string to change text background using rgb colors 292 | /// Uses a buffer. 293 | pub inline fn bgRGB(r: u8, g: u8, b: u8) []const u8 { 294 | var buf: [22]u8 = undefined; 295 | return fmt.bufPrint(&buf, "\x1b[48;2;{d};{d};{d}m", .{ r, g, b }) catch unreachable; 296 | } 297 | 298 | pub const reset = utils.comptimeCsi("0m", .{}); 299 | }; 300 | 301 | /// Writes the escape sequence code to change foreground to `color` (using 256 colors) 302 | pub fn fg256(writer: *std.Io.Writer, color: Color) !void { 303 | return writer.print(utils.csi ++ utils.fg_256 ++ "{d}m", .{@intFromEnum(color)}); 304 | } 305 | 306 | /// Writes the escape sequence code to change background to `color` (using 256 colors) 307 | pub fn bg256(writer: *std.Io.Writer, color: Color) !void { 308 | return writer.print(utils.csi ++ utils.bg_256 ++ "{d}m", .{@intFromEnum(color)}); 309 | } 310 | 311 | /// Writes the escape sequence code to change foreground to rgb color 312 | pub fn fgRGB(writer: *std.Io.Writer, r: u8, g: u8, b: u8) !void { 313 | return writer.print(utils.csi ++ utils.fg_rgb ++ "{d};{d};{d}m", .{ r, g, b }); 314 | } 315 | 316 | /// Writes the escape sequence code to change background to rgb color 317 | pub fn bgRGB(writer: *std.Io.Writer, r: u8, g: u8, b: u8) !void { 318 | return writer.print(utils.csi ++ utils.bg_rgb ++ "{d};{d};{d}m", .{ r, g, b }); 319 | } 320 | 321 | /// Writes the escape code to reset style and color 322 | pub fn resetAll(writer: *std.Io.Writer) !void { 323 | return writer.print(utils.csi ++ utils.reset_all, .{}); 324 | } 325 | -------------------------------------------------------------------------------- /src/event.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const io = std.io; 3 | const unicode = std.unicode; 4 | const windows = std.os.windows; 5 | 6 | pub const Modifiers = packed struct { 7 | shift: bool = false, 8 | alt: bool = false, 9 | ctrl: bool = false, 10 | }; 11 | 12 | pub const KeyCode = union(enum) { 13 | char: u21, 14 | enter, 15 | esc, 16 | backspace, 17 | tab, 18 | up, 19 | down, 20 | left, 21 | right, 22 | home, 23 | end, 24 | page_up, 25 | page_down, 26 | insert, 27 | delete, 28 | f1, 29 | f2, 30 | f3, 31 | f4, 32 | f5, 33 | f6, 34 | f7, 35 | f8, 36 | f9, 37 | f10, 38 | f11, 39 | f12, 40 | }; 41 | 42 | pub const Key = struct { 43 | mods: Modifiers = .{}, 44 | code: KeyCode, 45 | 46 | pub fn format(this: @This(), writer: *std.io.Writer) std.io.Writer.Error!void { 47 | try writer.writeAll("Key{ "); 48 | var first = true; 49 | 50 | if (this.mods.shift or this.mods.alt or this.mods.ctrl) { 51 | try writer.writeAll("mods: "); 52 | try writer.print("{{shift: {}, alt: {}, ctrl: {}}}", .{ this.mods.shift, this.mods.alt, this.mods.ctrl }); 53 | first = false; 54 | } 55 | 56 | if (!first) try writer.writeAll(", "); 57 | 58 | switch (this.code) { 59 | KeyCode.char => try writer.print("char: {u}", .{this.code.char}), 60 | else => try writer.print("code: {s}", .{@tagName(this.code)}), 61 | } 62 | 63 | try writer.writeAll(" }"); 64 | } 65 | }; 66 | 67 | pub const Event = union(enum) { 68 | key: Key, 69 | mouse: Mouse, 70 | resize, 71 | invalid, 72 | timeout, 73 | none, 74 | 75 | pub fn matchesChar(self: @This(), char: u21, mods: Modifiers) bool { 76 | switch (self) { 77 | .key => |k| switch (k.code) { 78 | .char => |c| { 79 | return c == char and std.meta.eql(k.mods, mods); 80 | }, 81 | else => return false, 82 | }, 83 | else => return false, 84 | } 85 | } 86 | }; 87 | 88 | // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking 89 | pub const Mouse = struct { 90 | x: u16, 91 | y: u16, 92 | button: MouseButton, 93 | is_alt: bool, 94 | is_shift: bool, 95 | is_ctrl: bool, 96 | 97 | pub fn format(this: @This(), writer: *std.io.Writer) std.io.Writer.Error!void { 98 | try writer.writeAll("Mouse."); 99 | try writer.print("x: {d}, y: {d}, button: {any}, is_alt: {any}, is_shift: {any}, is_ctrl: {any}", .{ this.x, this.y, this.button, this.is_alt, this.is_shift, this.is_ctrl }); 100 | } 101 | }; 102 | 103 | pub const MouseButton = enum { 104 | left, 105 | middle, 106 | right, 107 | release, 108 | scroll_up, 109 | scroll_down, 110 | move, 111 | move_rightclick, 112 | __non_exhaustive, 113 | }; 114 | 115 | /// Returns the next event received. If no event is received within the timeout, 116 | /// it returns `.timeout`. Timeout is in miliseconds 117 | /// 118 | /// When used in canonical mode, the user needs to press Enter to receive the event. 119 | /// When raw terminal mode is activated, the function waits up to the specified timeout 120 | /// for at least one event before returning. 121 | pub fn nextWithTimeout(file: std.fs.File, timeout_ms: i32) !Event { 122 | switch (@import("builtin").os.tag) { 123 | .linux => return nextWithTimeoutPosix(file, timeout_ms), 124 | .macos => return nextWithTimeoutPosix(file, timeout_ms), 125 | else => return error.UnsupportedPlatform, 126 | } 127 | } 128 | 129 | fn nextWithTimeoutPosix(file: std.fs.File, timeout_ms: i32) !Event { 130 | var polls: [1]std.posix.pollfd = .{.{ 131 | .fd = file.handle, 132 | .events = std.posix.POLL.IN, 133 | .revents = 0, 134 | }}; 135 | if ((try std.posix.poll(&polls, timeout_ms)) > 0) { 136 | return next(file); 137 | } 138 | 139 | return .timeout; 140 | } 141 | 142 | /// Returns true if there are events, false otherwise 143 | fn terminalHasEvent(file: std.fs.File) !bool { 144 | var polls: [1]std.posix.pollfd = .{.{ 145 | .fd = file.handle, 146 | .events = std.posix.POLL.IN, 147 | .revents = 0, 148 | }}; 149 | 150 | return (try std.posix.poll(&polls, 0)) > 0; 151 | } 152 | 153 | fn readByteOrNull(reader: *std.Io.Reader) !?u8 { 154 | return reader.takeByte() catch |err| switch (err) { 155 | error.EndOfStream => null, 156 | else => return err, 157 | }; 158 | } 159 | 160 | fn parseEscapeSequence(reader: *std.Io.Reader) !Event { 161 | const c1 = try readByteOrNull(reader) orelse return .invalid; 162 | 163 | switch (c1) { 164 | '[' => { 165 | const c2 = try readByteOrNull(reader) orelse return .invalid; 166 | switch (c2) { 167 | 'A' => return Event{ .key = .{ .code = KeyCode.up } }, 168 | 'B' => return Event{ .key = .{ .code = KeyCode.down } }, 169 | 'C' => return Event{ .key = .{ .code = KeyCode.right } }, 170 | 'D' => return Event{ .key = .{ .code = KeyCode.left } }, 171 | '0'...'9' => { 172 | // handle complex/large escape sequences 173 | var buffer: [33]u8 = [_]u8{0} ** 33; 174 | buffer[0] = c2; 175 | 176 | var i: usize = 1; 177 | while (i < 32) : (i += 1) { 178 | const c = try readByteOrNull(reader) orelse break; 179 | buffer[i] = c; 180 | // read until we found a sequence terminator 181 | if (c == '~' or c == 'M' or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')) { 182 | break; 183 | } 184 | } 185 | 186 | i += 1; 187 | buffer[i] = 0; 188 | 189 | // special keys 190 | if (std.mem.eql(u8, buffer[0..i], "1~")) { 191 | return Event{ .key = .{ .code = .home } }; 192 | } else if (std.mem.eql(u8, buffer[0..1], "2~")) { 193 | return Event{ .key = .{ .code = .insert } }; 194 | } else if (std.mem.eql(u8, buffer[0..i], "3~")) { 195 | return Event{ .key = .{ .code = .delete } }; 196 | } else if (std.mem.eql(u8, buffer[0..i], "4~")) { 197 | return Event{ .key = .{ .code = .end } }; 198 | } else if (std.mem.eql(u8, buffer[0..i], "5~")) { 199 | return Event{ .key = .{ .code = .page_up } }; 200 | } else if (std.mem.eql(u8, buffer[0..i], "6~")) { 201 | return Event{ .key = .{ .code = .page_down } }; 202 | } 203 | 204 | // fn-keys 205 | if (std.mem.eql(u8, buffer[0..i], "11~")) { 206 | return Event{ .key = .{ .code = .f1 } }; 207 | } else if (std.mem.eql(u8, buffer[0..i], "12~")) { 208 | return Event{ .key = .{ .code = .f2 } }; 209 | } else if (std.mem.eql(u8, buffer[0..i], "13~")) { 210 | return Event{ .key = .{ .code = .f3 } }; 211 | } else if (std.mem.eql(u8, buffer[0..i], "14~")) { 212 | return Event{ .key = .{ .code = .f4 } }; 213 | } else if (std.mem.eql(u8, buffer[0..i], "15~")) { 214 | return Event{ .key = .{ .code = .f5 } }; 215 | } else if (std.mem.eql(u8, buffer[0..i], "17~")) { 216 | return Event{ .key = .{ .code = .f6 } }; 217 | } else if (std.mem.eql(u8, buffer[0..i], "18~")) { 218 | return Event{ .key = .{ .code = .f7 } }; 219 | } else if (std.mem.eql(u8, buffer[0..i], "19~")) { 220 | return Event{ .key = .{ .code = .f8 } }; 221 | } else if (std.mem.eql(u8, buffer[0..i], "20~")) { 222 | return Event{ .key = .{ .code = .f9 } }; 223 | } else if (std.mem.eql(u8, buffer[0..i], "21~")) { 224 | return Event{ .key = .{ .code = .f10 } }; 225 | } else if (std.mem.eql(u8, buffer[0..i], "23~")) { 226 | return Event{ .key = .{ .code = .f11 } }; 227 | } else if (std.mem.eql(u8, buffer[0..i], "24~")) { 228 | return Event{ .key = .{ .code = .f12 } }; 229 | } 230 | 231 | // Modified arrow keys - Shift (modifier 2) 232 | if (std.mem.eql(u8, buffer[0..i], "1;2A")) { 233 | return Event{ .key = .{ .code = .up, .mods = .{ .shift = true } } }; 234 | } else if (std.mem.eql(u8, buffer[0..i], "1;2B")) { 235 | return Event{ .key = .{ .code = .down, .mods = .{ .shift = true } } }; 236 | } else if (std.mem.eql(u8, buffer[0..i], "1;2C")) { 237 | return Event{ .key = .{ .code = .right, .mods = .{ .shift = true } } }; 238 | } else if (std.mem.eql(u8, buffer[0..i], "1;2D")) { 239 | return Event{ .key = .{ .code = .left, .mods = .{ .shift = true } } }; 240 | } 241 | 242 | // Modified arrow keys - Alt (modifier 3) 243 | if (std.mem.eql(u8, buffer[0..i], "1;3A")) { 244 | return Event{ .key = .{ .code = .up, .mods = .{ .alt = true } } }; 245 | } else if (std.mem.eql(u8, buffer[0..i], "1;3B")) { 246 | return Event{ .key = .{ .code = .down, .mods = .{ .alt = true } } }; 247 | } else if (std.mem.eql(u8, buffer[0..i], "1;3C")) { 248 | return Event{ .key = .{ .code = .right, .mods = .{ .alt = true } } }; 249 | } else if (std.mem.eql(u8, buffer[0..i], "1;3D")) { 250 | return Event{ .key = .{ .code = .left, .mods = .{ .alt = true } } }; 251 | } 252 | 253 | // Modified arrow keys - Shift+Alt (modifier 4) 254 | if (std.mem.eql(u8, buffer[0..i], "1;4A")) { 255 | return Event{ .key = .{ .code = .up, .mods = .{ .alt = true, .shift = true } } }; 256 | } else if (std.mem.eql(u8, buffer[0..i], "1;4B")) { 257 | return Event{ .key = .{ .code = .down, .mods = .{ .alt = true, .shift = true } } }; 258 | } else if (std.mem.eql(u8, buffer[0..i], "1;4C")) { 259 | return Event{ .key = .{ .code = .right, .mods = .{ .alt = true, .shift = true } } }; 260 | } else if (std.mem.eql(u8, buffer[0..i], "1;4D")) { 261 | return Event{ .key = .{ .code = .left, .mods = .{ .alt = true, .shift = true } } }; 262 | } 263 | 264 | // Modified arrow keys - Ctrl (modifier 5) 265 | if (std.mem.eql(u8, buffer[0..i], "1;5A")) { 266 | return Event{ .key = .{ .code = .up, .mods = .{ .ctrl = true } } }; 267 | } else if (std.mem.eql(u8, buffer[0..i], "1;5B")) { 268 | return Event{ .key = .{ .code = .down, .mods = .{ .ctrl = true } } }; 269 | } else if (std.mem.eql(u8, buffer[0..i], "1;5C")) { 270 | return Event{ .key = .{ .code = .right, .mods = .{ .ctrl = true } } }; 271 | } else if (std.mem.eql(u8, buffer[0..i], "1;5D")) { 272 | return Event{ .key = .{ .code = .left, .mods = .{ .ctrl = true } } }; 273 | } 274 | 275 | // Modified arrow keys - Ctrl+Shift (modifier 6) 276 | if (std.mem.eql(u8, buffer[0..i], "1;6A")) { 277 | return Event{ .key = .{ .code = .up, .mods = .{ .ctrl = true, .shift = true } } }; 278 | } else if (std.mem.eql(u8, buffer[0..i], "1;6B")) { 279 | return Event{ .key = .{ .code = .down, .mods = .{ .ctrl = true, .shift = true } } }; 280 | } else if (std.mem.eql(u8, buffer[0..i], "1;6C")) { 281 | return Event{ .key = .{ .code = .right, .mods = .{ .ctrl = true, .shift = true } } }; 282 | } else if (std.mem.eql(u8, buffer[0..i], "1;6D")) { 283 | return Event{ .key = .{ .code = .left, .mods = .{ .ctrl = true, .shift = true } } }; 284 | } 285 | 286 | // Modified arrow keys - Ctrl+Alt (modifier 7) 287 | if (std.mem.eql(u8, buffer[0..i], "1;7A")) { 288 | return Event{ .key = .{ .code = .up, .mods = .{ .ctrl = true, .alt = true } } }; 289 | } else if (std.mem.eql(u8, buffer[0..i], "1;7B")) { 290 | return Event{ .key = .{ .code = .down, .mods = .{ .ctrl = true, .alt = true } } }; 291 | } else if (std.mem.eql(u8, buffer[0..i], "1;7C")) { 292 | return Event{ .key = .{ .code = .right, .mods = .{ .ctrl = true, .alt = true } } }; 293 | } else if (std.mem.eql(u8, buffer[0..i], "1;7D")) { 294 | return Event{ .key = .{ .code = .left, .mods = .{ .ctrl = true, .alt = true } } }; 295 | } 296 | 297 | // Modified arrow keys - Ctrl+Shift+Alt (modifier 8) 298 | if (std.mem.eql(u8, buffer[0..i], "1;8A")) { 299 | return Event{ .key = .{ .code = .up, .mods = .{ .ctrl = true, .alt = true, .shift = true } } }; 300 | } else if (std.mem.eql(u8, buffer[0..i], "1;8B")) { 301 | return Event{ .key = .{ .code = .down, .mods = .{ .ctrl = true, .alt = true, .shift = true } } }; 302 | } else if (std.mem.eql(u8, buffer[0..i], "1;8C")) { 303 | return Event{ .key = .{ .code = .right, .mods = .{ .ctrl = true, .alt = true, .shift = true } } }; 304 | } else if (std.mem.eql(u8, buffer[0..i], "1;8D")) { 305 | return Event{ .key = .{ .code = .left, .mods = .{ .ctrl = true, .alt = true, .shift = true } } }; 306 | } 307 | }, 308 | 'M' => { 309 | // parse mouse sequences (SGR format: