Skip to content

Instantly share code, notes, and snippets.

@Lightnet
Last active June 26, 2026 18:33
Show Gist options
  • Select an option

  • Save Lightnet/5395d4516b022fe33c7f232abf873ba3 to your computer and use it in GitHub Desktop.

Select an option

Save Lightnet/5395d4516b022fe33c7f232abf873ba3 to your computer and use it in GitHub Desktop.
Fake Terminal Input Zig 0.16.0 SDL 3
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