Last active
June 26, 2026 18:33
-
-
Save Lightnet/5395d4516b022fe33c7f232abf873ba3 to your computer and use it in GitHub Desktop.
Fake Terminal Input Zig 0.16.0 SDL 3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const std = @import("std"); | |
| const zsdl3 = @import("zsdl3"); // https://github.com/felixuxx/zsdl3 | |
| pub const TerminalInput = struct { | |
| buffer: std.ArrayList(u8) = .empty, | |
| history: std.ArrayList([]const u8) = .empty, | |
| cursor_pos: usize = 0, // tracks byte index offset from the START | |
| pub fn init() TerminalInput { | |
| return .{}; | |
| } | |
| pub fn deinit(self: *TerminalInput, gpa: std.mem.Allocator) void { | |
| self.buffer.deinit(gpa); | |
| // --- CLEAN UP STORED STRINGS ON EXIT --- | |
| for (self.history.items) |cmd| { | |
| gpa.free(cmd); | |
| } | |
| self.history.deinit(gpa); | |
| } | |
| pub fn activate(window: *zsdl3.SDL_Window) void { | |
| _ = zsdl3.input.startTextInput(window); | |
| } | |
| pub fn deactivate(window: *zsdl3.SDL_Window) void { | |
| _ = zsdl3.input.stopTextInput(window); | |
| } | |
| pub fn handleEvent(self: *TerminalInput, gpa: std.mem.Allocator, event: *const zsdl3.SDL_Event) !bool { | |
| switch (event.type) { | |
| zsdl3.SDL_EVENT_TEXT_INPUT => { | |
| const text_slice = std.mem.span(event.text.text); | |
| if (text_slice.len == 0) return false; | |
| try self.buffer.insertSlice(gpa, self.cursor_pos, text_slice); | |
| self.cursor_pos += text_slice.len; | |
| return true; | |
| }, | |
| zsdl3.SDL_EVENT_KEY_DOWN => { | |
| const sym = event.key.key; | |
| const scancode = event.key.scancode; | |
| if (sym == zsdl3.SDLK_BACKSPACE) { | |
| if (self.cursor_pos > 0) { | |
| var i = self.cursor_pos; | |
| while (i > 0) { | |
| i -= 1; | |
| if ((self.buffer.items[i] & 0xC0) != 0x80) { | |
| const bytes_to_remove = self.cursor_pos - i; | |
| try self.buffer.replaceRange(gpa, i, bytes_to_remove, &[_]u8{}); | |
| self.cursor_pos = i; | |
| break; | |
| } | |
| } | |
| return true; | |
| } | |
| } else if (scancode == zsdl3.SDL_SCANCODE_DELETE) { | |
| if (self.cursor_pos < self.buffer.items.len) { | |
| var i = self.cursor_pos + 1; | |
| while (i < self.buffer.items.len) : (i += 1) { | |
| if ((self.buffer.items[i] & 0xC0) != 0x80) { | |
| break; | |
| } | |
| } | |
| const bytes_to_remove = i - self.cursor_pos; | |
| try self.buffer.replaceRange(gpa, self.cursor_pos, bytes_to_remove, &[_]u8{}); | |
| return true; | |
| } | |
| } else if (sym == zsdl3.SDLK_LEFT) { | |
| if (self.cursor_pos > 0) { | |
| var i = self.cursor_pos; | |
| while (i > 0) { | |
| i -= 1; | |
| if ((self.buffer.items[i] & 0xC0) != 0x80) { | |
| self.cursor_pos = i; | |
| break; | |
| } | |
| } | |
| return true; | |
| } | |
| } else if (sym == zsdl3.SDLK_RIGHT) { | |
| if (self.cursor_pos < self.buffer.items.len) { | |
| var i = self.cursor_pos + 1; | |
| while (i < self.buffer.items.len) : (i += 1) { | |
| if ((self.buffer.items[i] & 0xC0) != 0x80) { | |
| break; | |
| } | |
| } | |
| self.cursor_pos = i; | |
| return true; | |
| } | |
| } else if (sym == zsdl3.SDLK_RETURN or scancode == zsdl3.keycode.SDL_SCANCODE_KP_ENTER) { | |
| // --- PASS GPA ALLOCATOR HERE --- | |
| self.executeCommand(gpa); | |
| // Only append to history if it wasn't a "clear" command | |
| if (self.buffer.items.len > 0 and !std.mem.eql(u8, self.buffer.items, "clear")) { | |
| const saved_cmd = try gpa.dupe(u8, self.buffer.items); | |
| try self.history.append(gpa, saved_cmd); | |
| } | |
| self.buffer.clearRetainingCapacity(); | |
| self.cursor_pos = 0; | |
| return true; | |
| } | |
| }, | |
| else => {}, | |
| } | |
| return false; | |
| } | |
| fn executeCommand(self: *TerminalInput, gpa: std.mem.Allocator) void { | |
| if (self.buffer.items.len == 0) return; | |
| // Check if the user typed "clear" | |
| if (std.mem.eql(u8, self.buffer.items, "clear")) { | |
| // Free the memory of each string stored in history | |
| for (self.history.items) |cmd| { | |
| gpa.free(cmd); | |
| } | |
| // Reset the list length to 0 while keeping allocated capacity | |
| self.history.clearRetainingCapacity(); | |
| return; | |
| } | |
| // Default behavior for other commands | |
| std.debug.print("Executing terminal command: {s}\n", .{self.buffer.items}); | |
| } | |
| }; | |
| pub fn main(init: std.process.Init) !void { | |
| const allocator = init.gpa; | |
| if (!zsdl3.init(zsdl3.SDL_INIT_VIDEO)) return; | |
| defer zsdl3.quit(); | |
| const window = zsdl3.createWindow("Fake Terminal", 800, 600, zsdl3.SDL_WINDOW_RESIZABLE) orelse return; | |
| defer zsdl3.destroyWindow(window); | |
| const renderer = zsdl3.createRenderer(window, null) orelse return; | |
| defer zsdl3.destroyRenderer(renderer); | |
| var term_input = TerminalInput.init(); | |
| defer term_input.deinit(allocator); | |
| TerminalInput.activate(window); | |
| var running = true; | |
| while (running) { | |
| var event: zsdl3.SDL_Event = undefined; | |
| while (zsdl3.pollEvent(&event)) { | |
| if (event.type == zsdl3.SDL_EVENT_QUIT) { | |
| running = false; | |
| } | |
| _ = try term_input.handleEvent(allocator, &event); | |
| } | |
| _ = zsdl3.setRenderDrawColor(renderer, 15, 15, 15, 255); | |
| _ = zsdl3.renderClear(renderer); | |
| var win_w: i32 = 800; | |
| var win_h: i32 = 600; | |
| _ = zsdl3.getWindowSize(window, &win_w, &win_h); | |
| const margin_x: f32 = 10.0; | |
| const margin_y: f32 = 10.0; | |
| const line_height: f32 = 14.0; | |
| const char_width: f32 = 8.0; | |
| const max_chars_per_line = @max(@as(usize, @intFromFloat(@floor((@as(f32, @floatFromInt(win_w)) - (margin_x * 2.0)) / char_width))), 1); | |
| // Calculate the maximum number of text rows the window can physically display | |
| const usable_height = @as(f32, @floatFromInt(win_h)) - (margin_y * 2.0); | |
| const max_visible_rows = @max(@as(usize, @intFromFloat(@floor(usable_height / line_height))), 1); | |
| // ================================================================= | |
| // PASS 1: PRE-CALCULATE TOTAL ROWS TO COMPUTE VERTICAL SCROLL OFFSET | |
| // ================================================================= | |
| var total_rows: usize = 0; | |
| var calc_col: usize = 0; | |
| // Count lines needed for all history items | |
| for (term_input.history.items) |cmd| { | |
| const hist_prefix = "> "; | |
| calc_col = 0; | |
| // Process prefix length | |
| calc_col += hist_prefix.len; | |
| total_rows += calc_col / max_chars_per_line; | |
| calc_col %= max_chars_per_line; | |
| // Process text characters | |
| var b_idx: usize = 0; | |
| while (b_idx < cmd.len) { | |
| var c_len: usize = 1; | |
| while (b_idx + c_len < cmd.len and (cmd[b_idx + c_len] & 0xC0) == 0x80) { | |
| c_len += 1; | |
| } | |
| if (calc_col >= max_chars_per_line) { | |
| calc_col = 0; | |
| total_rows += 1; | |
| } | |
| calc_col += 1; | |
| b_idx += c_len; | |
| } | |
| total_rows += 1; // Carriage return for next item | |
| } | |
| // Count lines needed for the active input prompt | |
| calc_col = 0; | |
| const prefix = "user@terminal:~$ "; | |
| calc_col += prefix.len; | |
| total_rows += calc_col / max_chars_per_line; | |
| calc_col %= max_chars_per_line; | |
| var active_idx: usize = 0; | |
| while (active_idx < term_input.buffer.items.len) { | |
| var c_len: usize = 1; | |
| while (active_idx + c_len < term_input.buffer.items.len and (term_input.buffer.items[active_idx + c_len] & 0xC0) == 0x80) { | |
| c_len += 1; | |
| } | |
| if (calc_col >= max_chars_per_line) { | |
| calc_col = 0; | |
| total_rows += 1; | |
| } | |
| calc_col += 1; | |
| active_idx += c_len; | |
| } | |
| total_rows += 1; // Count the current active row itself | |
| // Determine how many lines have overflowed past the bottom edge | |
| const scroll_row_offset = if (total_rows > max_visible_rows) total_rows - max_visible_rows else 0; | |
| // ================================================================= | |
| // PASS 2: RENDER VISIBLE ROWS SUBTRACTING THE SCROLL OFFSET | |
| // ================================================================= | |
| _ = zsdl3.setRenderDrawColor(renderer, 0, 255, 0, 255); | |
| var current_col: usize = 0; | |
| var current_row: usize = 0; // Absolute layout tracking layout step | |
| // Helper to convert internal absolute row index to screen space rendering coordinate | |
| const getRenderY = struct { | |
| fn get(row: usize, offset: usize, m_y: f32, l_h: f32) f32 { | |
| if (row < offset) return -100.0; // Push offscreen if culled | |
| return m_y + (@as(f32, @floatFromInt(row - offset)) * l_h); | |
| } | |
| }.get; | |
| // 1. Render Past History Commands | |
| for (term_input.history.items) |cmd| { | |
| const hist_prefix = "> "; | |
| current_col = 0; | |
| for (hist_prefix) |char| { | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| const y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| if (y >= 0) { | |
| const x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| const single_char_str = [_:0]u8{char}; | |
| _ = zsdl3.renderDebugText(renderer, x, y, &single_char_str); | |
| } | |
| current_col += 1; | |
| } | |
| var b_idx: usize = 0; | |
| while (b_idx < cmd.len) { | |
| var c_len: usize = 1; | |
| while (b_idx + c_len < cmd.len and (cmd[b_idx + c_len] & 0xC0) == 0x80) { | |
| c_len += 1; | |
| } | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| const y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| if (y >= 0) { | |
| const x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| var temp_glyph = [_:0]u8{0} ** 5; | |
| @memcpy(temp_glyph[0..c_len], cmd[b_idx .. b_idx + c_len]); | |
| _ = zsdl3.renderDebugText(renderer, x, y, &temp_glyph); | |
| } | |
| b_idx += c_len; | |
| current_col += 1; | |
| } | |
| current_row += 1; | |
| } | |
| // 2. Render Active Input Field | |
| current_col = 0; | |
| var cursor_render_x: f32 = margin_x; | |
| var cursor_render_y: f32 = -100.0; // Hide default unless assigned | |
| for (prefix) |char| { | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| const y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| if (y >= 0) { | |
| const x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| const single_char_str = [_:0]u8{char}; | |
| _ = zsdl3.renderDebugText(renderer, x, y, &single_char_str); | |
| } | |
| current_col += 1; | |
| } | |
| var byte_idx: usize = 0; | |
| while (byte_idx < term_input.buffer.items.len) { | |
| if (byte_idx == term_input.cursor_pos) { | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| cursor_render_x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| cursor_render_y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| } | |
| var char_len: usize = 1; | |
| while (byte_idx + char_len < term_input.buffer.items.len and | |
| (term_input.buffer.items[byte_idx + char_len] & 0xC0) == 0x80) | |
| { | |
| char_len += 1; | |
| } | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| const y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| if (y >= 0) { | |
| const x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| var temp_glyph = [_:0]u8{0} ** 5; | |
| @memcpy(temp_glyph[0..char_len], term_input.buffer.items[byte_idx .. byte_idx + char_len]); | |
| _ = zsdl3.renderDebugText(renderer, x, y, &temp_glyph); | |
| } | |
| byte_idx += char_len; | |
| current_col += 1; | |
| } | |
| if (term_input.cursor_pos == term_input.buffer.items.len) { | |
| if (current_col >= max_chars_per_line) { | |
| current_col = 0; | |
| current_row += 1; | |
| } | |
| cursor_render_x = margin_x + (@as(f32, @floatFromInt(current_col)) * char_width); | |
| cursor_render_y = getRenderY(current_row, scroll_row_offset, margin_y, line_height); | |
| } | |
| // 3. Draw Flashing Cursor Block if visible on screen | |
| if (cursor_render_y >= 0 and (@divFloor(zsdl3.getTicks(), 500) % 2) == 0) { | |
| _ = zsdl3.renderDebugText(renderer, cursor_render_x, cursor_render_y, "█"); | |
| } | |
| // ---------------------------------------------------- | |
| _ = zsdl3.renderPresent(renderer); | |
| zsdl3.delay(16); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment