|
const std = @import("std"); |
|
const c = @cImport({ |
|
@cInclude("SDL3/SDL.h"); |
|
@cInclude("SDL3_ttf/SDL_ttf.h"); |
|
}); |
|
|
|
const MAX_LINES = 22; |
|
const MAX_LINE_LEN = 256; // Expanded to accommodate longer raw command inputs before chunking |
|
const FONT_SIZE = 20.0; |
|
const LINE_HEIGHT = 25.0; |
|
const WRAP_LIMIT = 70; // Maximum characters visible on a single row before wrapping down |
|
|
|
// Struct to manage the internal state of our terminal emulator |
|
const Terminal = struct { |
|
log: [MAX_LINES][MAX_LINE_LEN]u8 = undefined, |
|
log_lens: [MAX_LINES]usize = .{0} ** MAX_LINES, |
|
log_line_count: usize = 0, |
|
input_buffer: [MAX_LINE_LEN]u8 = undefined, |
|
input_len: usize = 0, |
|
|
|
// Appends a text string to our log matrix, automatically slicing it into multi-line wrapped chunks |
|
fn appendToLog(self: *Terminal, text: []const u8) void { |
|
var remainder = text; |
|
|
|
// Loop and chunk the text whenever it exceeds the screen column boundary |
|
while (remainder.len > 0) { |
|
const chunk_len = @min(remainder.len, WRAP_LIMIT); |
|
const chunk = remainder[0..chunk_len]; |
|
|
|
if (self.log_line_count < MAX_LINES) { |
|
@memcpy(self.log[self.log_line_count][0..chunk_len], chunk); |
|
self.log_lens[self.log_line_count] = chunk_len; |
|
self.log_line_count += 1; |
|
} else { |
|
// Scroll all active rows up by one row index position to make room at the bottom |
|
for (1..MAX_LINES) |i| { |
|
self.log[i - 1] = self.log[i]; |
|
self.log_lens[i - 1] = self.log_lens[i]; |
|
} |
|
@memcpy(self.log[MAX_LINES - 1][0..chunk_len], chunk); |
|
self.log_lens[MAX_LINES - 1] = chunk_len; |
|
} |
|
remainder = remainder[chunk_len..]; |
|
} |
|
} |
|
|
|
// Evaluates entered string commands |
|
fn executeCommand(self: *Terminal, cmd: []const u8, running: *bool) void { |
|
if (std.mem.eql(u8, cmd, "clear")) { |
|
self.log_line_count = 0; |
|
} else if (std.mem.eql(u8, cmd, "help")) { |
|
self.appendToLog("Available commands: help, clear, hello, status, longtext, exit"); |
|
} else if (std.mem.eql(u8, cmd, "hello")) { |
|
self.appendToLog("Hello from Zig 0.16.0 & SDL3 text engine!"); |
|
} else if (std.mem.eql(u8, cmd, "status")) { |
|
self.appendToLog("Systems: OPTIMAL | Core: SECURE | Language: ZIG"); |
|
} else if (std.mem.eql(u8, cmd, "longtext")) { |
|
self.appendToLog("This is an exceptionally long sequence designed to test our brand-new automated word-wrapping subsystem matrix inside the native window pipeline seamlessly!"); |
|
} else if (std.mem.eql(u8, cmd, "exit")) { |
|
running.* = false; |
|
} else if (cmd.len > 0) { |
|
var buf: [MAX_LINE_LEN + 32]u8 = undefined; |
|
if (std.fmt.bufPrint(&buf, "Unknown command: {s}", .{cmd})) |msg| { |
|
self.appendToLog(msg); |
|
} else |_| { |
|
self.appendToLog("Unknown command"); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
pub fn main(init: std.process.Init) !void { |
|
_ = init; |
|
|
|
// 1. Initialize SDL Subsystems |
|
if (!c.SDL_Init(c.SDL_INIT_VIDEO)) { |
|
std.debug.print("SDL Init Error: {s}\n", .{c.SDL_GetError()}); |
|
return error.SDLInitializationFailed; |
|
} |
|
defer c.SDL_Quit(); |
|
|
|
// 2. Initialize SDL_ttf Subsystem |
|
if (!c.TTF_Init()) { |
|
std.debug.print("TTF Init Error: {s}\n", .{c.SDL_GetError()}); |
|
return error.TTFInitializationFailed; |
|
} |
|
defer c.TTF_Quit(); |
|
|
|
// 3. Create Window and Renderer |
|
var window: ?*c.SDL_Window = null; |
|
var renderer: ?*c.SDL_Renderer = null; |
|
if (!c.SDL_CreateWindowAndRenderer("Zig SDL3 Terminal Core", 800, 600, 0, &window, &renderer)) { |
|
std.debug.print("Window/Renderer Error: {s}\n", .{c.SDL_GetError()}); |
|
return error.SDLWindowCreationFailed; |
|
} |
|
defer c.SDL_DestroyRenderer(renderer); |
|
defer c.SDL_DestroyWindow(window); |
|
|
|
// 4. Load the Font Asset |
|
const font_path = "assets/fonts/m6x11.ttf"; |
|
const font: ?*c.TTF_Font = c.TTF_OpenFont(font_path, FONT_SIZE); |
|
if (font == null) { |
|
std.debug.print("Failed to load font at {s}: {s}\n", .{ font_path, c.SDL_GetError() }); |
|
return error.FontLoadFailed; |
|
} |
|
defer c.TTF_CloseFont(font); |
|
|
|
// 5. Create Text Engine |
|
const text_engine = c.TTF_CreateRendererTextEngine(renderer); |
|
if (text_engine == null) { |
|
std.debug.print("Failed to create text engine: {s}\n", .{c.SDL_GetError()}); |
|
return error.TextEngineCreationFailed; |
|
} |
|
defer c.TTF_DestroyRendererTextEngine(text_engine); |
|
|
|
// Setup terminal instance state |
|
var terminal = Terminal{}; |
|
terminal.appendToLog("Zig & SDL3 Terminal Interface Booted Successfully."); |
|
terminal.appendToLog("Type 'help' to reveal available system modules."); |
|
|
|
// Turn on the OS text input processing pipe |
|
_ = c.SDL_StartTextInput(window); |
|
|
|
var running = true; |
|
var event: c.SDL_Event = undefined; |
|
|
|
while (running) { |
|
while (c.SDL_PollEvent(&event)) { |
|
switch (event.type) { |
|
c.SDL_EVENT_QUIT => { |
|
running = false; |
|
}, |
|
c.SDL_EVENT_TEXT_INPUT => { |
|
const text_slice = std.mem.span(event.text.text); |
|
if (terminal.input_len + text_slice.len < MAX_LINE_LEN) { |
|
@memcpy(terminal.input_buffer[terminal.input_len..][0..text_slice.len], text_slice); |
|
terminal.input_len += text_slice.len; |
|
} |
|
}, |
|
c.SDL_EVENT_KEY_DOWN => { |
|
switch (event.key.key) { |
|
c.SDLK_ESCAPE => { |
|
running = false; |
|
}, |
|
c.SDLK_BACKSPACE => { |
|
if (terminal.input_len > 0) { |
|
terminal.input_len -= 1; |
|
} |
|
}, |
|
c.SDLK_RETURN => { |
|
var echo_buf: [MAX_LINE_LEN + 4]u8 = undefined; |
|
const cmd = terminal.input_buffer[0..terminal.input_len]; |
|
|
|
// Echo prompt out to visual history log |
|
if (std.fmt.bufPrint(&echo_buf, "> {s}", .{cmd})) |echo| { |
|
terminal.appendToLog(echo); |
|
} else |_| {} |
|
|
|
terminal.executeCommand(cmd, &running); |
|
terminal.input_len = 0; // Wipe transient input track |
|
}, |
|
else => {}, |
|
} |
|
}, |
|
else => {}, |
|
} |
|
} |
|
|
|
// Clean Canvas (Dark-tinted terminal palette setup) |
|
_ = c.SDL_SetRenderDrawColor(renderer, 10, 20, 15, 255); |
|
_ = c.SDL_RenderClear(renderer); |
|
|
|
var current_y: f32 = 15.0; |
|
|
|
// 6. Draw past terminal history rows line by line via SDL3 Text Engine |
|
for (0..terminal.log_line_count) |i| { |
|
const line = terminal.log[i][0..terminal.log_lens[i]]; |
|
if (line.len > 0) { |
|
const text_obj = c.TTF_CreateText(text_engine, font, line.ptr, line.len); |
|
if (text_obj) |t| { |
|
_ = c.TTF_SetTextColor(t, 50, 255, 50, 255); // Matrix Green |
|
_ = c.TTF_DrawRendererText(t, 15.0, current_y); |
|
c.TTF_DestroyText(t); |
|
} |
|
} |
|
current_y += LINE_HEIGHT; |
|
} |
|
|
|
// 7. Format, wrap, and paint active prompt line dynamically |
|
var prompt_buf: [MAX_LINE_LEN + 4]u8 = undefined; |
|
const current_input = terminal.input_buffer[0..terminal.input_len]; |
|
if (std.fmt.bufPrint(&prompt_buf, "> {s}_", .{current_input})) |prompt_str| { |
|
var remainder = prompt_str; |
|
|
|
// Incrementally slice and render the prompt line if it overflows the display bounds |
|
while (remainder.len > 0) { |
|
const chunk_len = @min(remainder.len, WRAP_LIMIT); |
|
const chunk = remainder[0..chunk_len]; |
|
|
|
const text_obj = c.TTF_CreateText(text_engine, font, chunk.ptr, chunk.len); |
|
if (text_obj) |t| { |
|
_ = c.TTF_SetTextColor(t, 120, 255, 120, 255); // Bright Green |
|
_ = c.TTF_DrawRendererText(t, 15.0, current_y); |
|
c.TTF_DestroyText(t); |
|
} |
|
|
|
current_y += LINE_HEIGHT; |
|
remainder = remainder[chunk_len..]; |
|
} |
|
} else |_| {} |
|
|
|
_ = c.SDL_RenderPresent(renderer); |
|
c.SDL_Delay(16); // Constrain update intervals (~60 FPS) |
|
} |
|
|
|
_ = c.SDL_StopTextInput(window); |
|
} |