Mercurial
view postdog/main.c @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 0face9898d04 |
| children |
line wrap: on
line source
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/stat.h> #include <uv.h> #ifdef _WIN32 #include <direct.h> #include <io.h> #define mkdir(path, mode) _mkdir(path) #define access _access #define F_OK 0 #else #include <sys/stat.h> #include <dirent.h> #include <unistd.h> #endif #include "dowa/dowa.h" #include "seobeo/seobeo.h" #include "third_party/raylib/include/raylib.h" #include "third_party/raylib/include/raygui.h" #include "third_party/raylib/custom.h" #ifndef POSTDOG_PATHS #define POSTDOG_PATHS "/home/june/zenbu/postdog/history" #endif #define SCREEN_WIDTH 1280 #define SCREEN_HEIGHT 780 #define MAX_SCROLL_HEIGHT 10000 #define HEADER_BUFFER_LENGTH 1024 * 4 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4 #define URL_TEXT_BUFFER_LENGTH 1024 * 10 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5 // #define URL_TEXT_DEFAULT "https://httpbin.org/get" #define URL_TEXT_DEFAULT "wss://mrjunejune.com/echo" #define HEADER_TEXT_DEFAULT "Content-Type: application/json" #define BODY_TEXT_DEFAULT "" #define GET_PARAM_TEXT_DEFAULT "foo bar" // ============================================================================ // TextArea Component // ============================================================================ #define TEXT_SIZE_DEFAULT 20 // used to calcualte spacing #define TEXT_AREA_LINE_HEIGHT GuiGetStyle(DEFAULT, TEXT_SIZE) #define TEXT_AREA_PADDING 30 #define TEXT_AREA_CURSOR_WIDTH 2 #define TEXT_AREA_MAX_UNDO_STATES 64 #define TEXT_AREA_MAX_INSTANCES 8 typedef struct { char *text; int cursor_pos; int selection_start; int selection_end; } TextAreaUndoEntry; // Cached line info for fast rendering typedef struct { int start_pos; // Character position where this line starts int end_pos; // Character position where this line ends (exclusive) int char_count; // Number of characters in this line } LineInfo; typedef struct { int id; // Unique ID for this text area int cursor_pos; // Current cursor position in text int selection_start; // Selection start (-1 if no selection) int selection_end; // Selection end (-1 if no selection) float scroll_offset_y; // Vertical scroll offset float scroll_offset_x; // Horizontal scroll offset (for non-wrap mode) boolean is_selecting; // Currently dragging to select boolean is_initialized; // State has been initialized // Undo history TextAreaUndoEntry *undo_stack; // Dowa array of undo entries int undo_index; // Current position in undo stack // Internal tracking double last_blink_time; boolean cursor_visible; // Line cache for fast rendering LineInfo *line_cache; // Dowa array of line info int cached_text_len; // Text length when cache was built int cached_text_hash; // Simple hash to detect text changes float cached_content_width; // Content width when cache was built boolean cache_valid; // Whether cache is valid } TextAreaState; static TextAreaState g_text_area_states[TEXT_AREA_MAX_INSTANCES] = {0}; static int g_text_area_state_count = 0; static char *g_clipboard_text = NULL; static Dowa_Arena *g_text_area_arena = NULL; // Helper functions static int TA_Min_Int(int a, int b) { return a < b ? a : b; } static int TA_Max_Int(int a, int b) { return a > b ? a : b; } static float TA_Min_Float(float a, float b) { return a < b ? a : b; } static float TA_Max_Float(float a, float b) { return a > b ? a : b; } // Sanitize text for display - replace tabs with spaces, remove other non-printable chars static void SanitizeTextForDisplay(char *text) { if (!text) return; char *read = text; char *write = text; while (*read) { if (*read == '\t') { // Replace tab with 2 spaces *write++ = ' '; *write++ = ' '; } else if (*read == '\n' || *read == '\r') { // Keep newlines and carriage returns *write++ = *read; } else if ((unsigned char)*read >= 32 && (unsigned char)*read < 127) { // Keep printable ASCII *write++ = *read; } else if ((unsigned char)*read >= 128) { // Keep UTF-8 characters (high bit set) *write++ = *read; } // Skip other non-printable characters read++; } *write = '\0'; } static TextAreaState* GetTextAreaState(int id) { for (int i = 0; i < g_text_area_state_count; i++) { if (g_text_area_states[i].id == id && g_text_area_states[i].is_initialized) { return &g_text_area_states[i]; } } return NULL; } static TextAreaState* CreateTextAreaState(int id) { // First, check if there's an existing slot with this ID (possibly reset) for (int i = 0; i < g_text_area_state_count; i++) { if (g_text_area_states[i].id == id) { TextAreaState *state = &g_text_area_states[i]; // Reuse this slot - clear it but keep the id memset(state, 0, sizeof(TextAreaState)); state->id = id; state->selection_start = -1; state->selection_end = -1; state->undo_index = -1; state->cursor_visible = TRUE; state->is_initialized = TRUE; return state; } } // No existing slot, create a new one if (g_text_area_state_count >= TEXT_AREA_MAX_INSTANCES) { return &g_text_area_states[0]; // Reuse first slot as fallback } TextAreaState *state = &g_text_area_states[g_text_area_state_count++]; memset(state, 0, sizeof(TextAreaState)); state->id = id; state->selection_start = -1; state->selection_end = -1; state->undo_index = -1; state->cursor_visible = TRUE; state->is_initialized = TRUE; return state; } static void GetLineAndColumn(const char *text, int pos, int *out_line, int *out_column) { int line = 0; int column = 0; for (int i = 0; i < pos && text[i] != '\0'; i++) { if (text[i] == '\n') { line++; column = 0; } else { column++; } } *out_line = line; *out_column = column; } static int GetPosFromLineColumn(const char *text, int line, int column) { int current_line = 0; int current_col = 0; int i = 0; while (text[i] != '\0') { if (current_line == line && current_col == column) { return i; } if (text[i] == '\n') { if (current_line == line) { return i; } current_line++; current_col = 0; } else { current_col++; } i++; } return i; } static int GetLineStart(const char *text, int pos) { int i = pos; while (i > 0 && text[i - 1] != '\n') { i--; } return i; } static int GetLineEnd(const char *text, int pos) { int i = pos; int len = strlen(text); while (i < len && text[i] != '\n') { i++; } return i; } static int CountLines(const char *text) { int count = 1; for (int i = 0; text[i] != '\0'; i++) { if (text[i] == '\n') count++; } return count; } // Simple hash to detect text changes (djb2) static int SimpleTextHash(const char *text, int len) { int hash = 5381; int sample_count = len < 100 ? len : 100; // Sample first 100 chars for (int i = 0; i < sample_count; i++) { hash = ((hash << 5) + hash) + text[i]; } // Also sample some chars from middle and end if (len > 200) { for (int i = len/2; i < len/2 + 50 && i < len; i++) { hash = ((hash << 5) + hash) + text[i]; } } return hash; } // Build line cache - O(n) once, then O(1) for rendering static void BuildLineCache(TextAreaState *state, const char *text, float content_width, int font_size, boolean wrap, Dowa_Arena *arena) { int text_len = strlen(text); // Check if cache is still valid int new_hash = SimpleTextHash(text, text_len); if (state->cache_valid && state->cached_text_len == text_len && state->cached_text_hash == new_hash && state->cached_content_width == content_width) { return; // Cache is valid, no need to rebuild } // Clear old cache if (state->line_cache) { Dowa_Array_Free(state->line_cache); state->line_cache = NULL; } // Approximate character width (for non-monospace, use average) float avg_char_width = font_size * 0.6f; // Reasonable approximation for most fonts int max_chars_per_line = (int)(content_width / avg_char_width); if (max_chars_per_line < 1) max_chars_per_line = 1; Dowa_Array_Reserve(state->line_cache, text_len / max_chars_per_line + 10); int line_start = 0; int i = 0; while (i <= text_len) { boolean is_end = (i == text_len); boolean is_newline = (!is_end && text[i] == '\n'); int chars_in_line = i - line_start; // Check if we need to wrap (based on character count approximation) boolean should_wrap = wrap && !is_end && !is_newline && chars_in_line >= max_chars_per_line; if (is_end || is_newline || should_wrap) { LineInfo line = { .start_pos = line_start, .end_pos = i, .char_count = chars_in_line }; Dowa_Array_Push(state->line_cache, line); if (is_newline) { line_start = i + 1; } else if (should_wrap) { // Try to find a space to wrap at int wrap_pos = i; for (int j = i - 1; j > line_start && j > i - 20; j--) { if (text[j] == ' ') { wrap_pos = j; break; } } // Update the last line entry with correct end state->line_cache[Dowa_Array_Length(state->line_cache) - 1].end_pos = wrap_pos; state->line_cache[Dowa_Array_Length(state->line_cache) - 1].char_count = wrap_pos - line_start; line_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; i = line_start - 1; // Will be incremented at end of loop } else { line_start = i + 1; } } i++; } // Ensure at least one line exists if (Dowa_Array_Length(state->line_cache) == 0) { LineInfo empty_line = { .start_pos = 0, .end_pos = 0, .char_count = 0 }; Dowa_Array_Push(state->line_cache, empty_line); } // Update cache metadata state->cached_text_len = text_len; state->cached_text_hash = new_hash; state->cached_content_width = content_width; state->cache_valid = TRUE; } static int MeasureTextRange(const char *text, int start, int end, int font_size) { if (start >= end) return 0; char temp[1024]; int len = TA_Min_Int(end - start, 1023); strncpy(temp, text + start, len); temp[len] = '\0'; return MeasureTextEx(GuiGetFont(), temp, font_size, TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE)).x; } static int GetCharIndexFromPos(const char *text, Rectangle bounds, Vector2 pos, boolean wrap, float scroll_y, int font_size, int line_height) { if (!text || strlen(text) == 0) return 0; float content_x = bounds.x + TEXT_AREA_PADDING; float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; float content_width = bounds.width - TEXT_AREA_PADDING * 2; int text_len = strlen(text); float click_line_y = (pos.y - content_y) / line_height; int target_visual_line = (int)click_line_y; if (target_visual_line < 0) target_visual_line = 0; int current_visual_line = 0; int i = 0; int line_char_start = 0; while (i <= text_len) { boolean is_newline = (i < text_len && text[i] == '\n'); if (wrap && i > line_char_start) { int line_width = MeasureTextRange(text, line_char_start, i, font_size); if (line_width > content_width && i > line_char_start + 1) { int wrap_pos = i - 1; for (int j = i - 1; j > line_char_start; j--) { if (text[j] == ' ') { wrap_pos = j; break; } } if (current_visual_line == target_visual_line) { float click_x = pos.x - content_x; int best_pos = line_char_start; float best_dist = 99999; for (int k = line_char_start; k <= wrap_pos; k++) { int char_x = MeasureTextRange(text, line_char_start, k, font_size); float dist = (float)(click_x - char_x); if (dist < 0) dist = -dist; if (dist < best_dist) { best_dist = dist; best_pos = k; } } return best_pos; } current_visual_line++; line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; i = line_char_start; continue; } } if (is_newline || i == text_len) { if (current_visual_line == target_visual_line || i == text_len) { float click_x = pos.x - content_x; int line_end = i; int best_pos = line_char_start; float best_dist = 99999; for (int k = line_char_start; k <= line_end; k++) { int char_x = MeasureTextRange(text, line_char_start, k, font_size); float dist = (float)(click_x - char_x); if (dist < 0) dist = -dist; if (dist < best_dist) { best_dist = dist; best_pos = k; } } return TA_Min_Int(best_pos, text_len); } current_visual_line++; line_char_start = i + 1; } i++; } return text_len; } // Get character index from mouse position using line cache static int GetCharIndexFromPosWithCache(const char *text, Rectangle bounds, Vector2 pos, float scroll_y, int font_size, int line_height, LineInfo *line_cache) { if (!text || strlen(text) == 0) return 0; if (!line_cache || Dowa_Array_Length(line_cache) == 0) return 0; float content_x = bounds.x + TEXT_AREA_PADDING; float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; int text_len = strlen(text); int line_count = Dowa_Array_Length(line_cache); // Find which visual line was clicked float click_line_y = (pos.y - content_y) / line_height; int target_line = (int)click_line_y; if (target_line < 0) target_line = 0; if (target_line >= line_count) target_line = line_count - 1; if (target_line < 0) return 0; // Safety check LineInfo *line = &line_cache[target_line]; // Find character position within the line float click_x = pos.x - content_x; if (click_x < 0) return line->start_pos; // Linear search for the character closest to click position int best_pos = line->start_pos; float best_dist = 99999; for (int k = line->start_pos; k <= line->end_pos; k++) { int char_x = MeasureTextRange(text, line->start_pos, k, font_size); float dist = click_x - char_x; if (dist < 0) dist = -dist; if (dist < best_dist) { best_dist = dist; best_pos = k; } } return TA_Min_Int(best_pos, text_len); } // Get cursor position using line cache (fast O(log n) or O(n) worst case) static Vector2 GetCursorScreenPosFromCache(const char *text, int cursor_pos, Rectangle bounds, float scroll_y, int font_size, int line_height, LineInfo *line_cache) { float content_x = bounds.x + TEXT_AREA_PADDING; float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y; if (!text || cursor_pos == 0) { return (Vector2){content_x, content_y}; } if (!line_cache || Dowa_Array_Length(line_cache) == 0) { return (Vector2){content_x, content_y}; } int text_len = strlen(text); cursor_pos = TA_Min_Int(cursor_pos, text_len); // Find which line contains the cursor int line_count = Dowa_Array_Length(line_cache); for (int i = 0; i < line_count; i++) { LineInfo *line = &line_cache[i]; // Cursor is within this line or at the end of this line if (cursor_pos >= line->start_pos && cursor_pos <= line->end_pos) { float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size); float y = content_y + i * line_height; return (Vector2){x, y}; } } // Fallback: cursor at the end int last_line = line_count > 0 ? line_count - 1 : 0; LineInfo *line = &line_cache[last_line]; float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size); float y = content_y + last_line * line_height; return (Vector2){x, y}; } static float GetContentHeight(const char *text, Rectangle bounds, boolean wrap, int font_size, int line_height) { if (!text || strlen(text) == 0) return line_height; float content_width = bounds.width - TEXT_AREA_PADDING * 2; int visual_lines = 1; int line_char_start = 0; int text_len = strlen(text); for (int i = 0; i <= text_len; i++) { if (i == text_len || text[i] == '\n') { visual_lines++; line_char_start = i + 1; } else if (wrap) { int line_width = MeasureTextRange(text, line_char_start, i + 1, font_size); if (line_width > content_width && i > line_char_start) { int wrap_pos = i; for (int j = i; j > line_char_start; j--) { if (text[j] == ' ') { wrap_pos = j; break; } } visual_lines++; line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos; } } } return visual_lines * line_height; } static void PushUndoState(TextAreaState *state, const char *text, Dowa_Arena *arena) { TextAreaUndoEntry entry; entry.text = Dowa_String_Copy_Arena((char*)text, arena); entry.cursor_pos = state->cursor_pos; entry.selection_start = state->selection_start; entry.selection_end = state->selection_end; if (state->undo_index < (int)Dowa_Array_Length(state->undo_stack) - 1) { dowa__header(state->undo_stack)->length = state->undo_index + 1; } Dowa_Array_Push_Arena(state->undo_stack, entry, arena); state->undo_index = Dowa_Array_Length(state->undo_stack) - 1; if (Dowa_Array_Length(state->undo_stack) > TEXT_AREA_MAX_UNDO_STATES) { for (int i = 0; i < (int)Dowa_Array_Length(state->undo_stack) - 1; i++) { state->undo_stack[i] = state->undo_stack[i + 1]; } dowa__header(state->undo_stack)->length--; state->undo_index--; } } static boolean PerformUndo(TextAreaState *state, char *text, int text_size) { if (state->undo_index <= 0) return FALSE; state->undo_index--; TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; strncpy(text, entry->text, text_size - 1); text[text_size - 1] = '\0'; state->cursor_pos = entry->cursor_pos; state->selection_start = entry->selection_start; state->selection_end = entry->selection_end; return TRUE; } static boolean PerformRedo(TextAreaState *state, char *text, int text_size) { if (state->undo_index >= (int)Dowa_Array_Length(state->undo_stack) - 1) return FALSE; state->undo_index++; TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index]; strncpy(text, entry->text, text_size - 1); text[text_size - 1] = '\0'; state->cursor_pos = entry->cursor_pos; state->selection_start = entry->selection_start; state->selection_end = entry->selection_end; return TRUE; } static void InsertTextAtCursor(char *text, int text_size, int cursor_pos, const char *insert_text, int *new_cursor_pos) { int text_len = strlen(text); int insert_len = strlen(insert_text); if (text_len + insert_len >= text_size - 1) { insert_len = text_size - 1 - text_len; if (insert_len <= 0) return; } memmove(text + cursor_pos + insert_len, text + cursor_pos, text_len - cursor_pos + 1); memcpy(text + cursor_pos, insert_text, insert_len); *new_cursor_pos = cursor_pos + insert_len; } static void DeleteTextRange(char *text, int start, int end) { if (start >= end) return; int text_len = strlen(text); if (start < 0) start = 0; if (end > text_len) end = text_len; memmove(text + start, text + end, text_len - end + 1); } static char* GetSelectedText(const char *text, int sel_start, int sel_end, Dowa_Arena *arena) { if (sel_start < 0 || sel_end < 0 || sel_start >= sel_end) return NULL; int len = sel_end - sel_start; char *result = Dowa_Arena_Allocate(arena, len + 1); strncpy(result, text + sel_start, len); result[len] = '\0'; return result; } boolean GuiTextArea(int id, Rectangle bounds, char *text, int text_size, boolean is_edit_mode, boolean should_text_wrap, boolean readonly, Dowa_Arena *arena) { boolean should_toggle_edit_mode = FALSE; // Get or create state for this text area TextAreaState *state = GetTextAreaState(id); if (!state) { state = CreateTextAreaState(id); state->cursor_pos = readonly ? 0 : strlen(text); // Readonly starts at beginning state->last_blink_time = GetTime(); if (!readonly) PushUndoState(state, text, arena); } int text_len = strlen(text); Vector2 mouse_pos = GetMousePosition(); boolean mouse_in_bounds = CheckCollisionPointRec(mouse_pos, bounds); // Handle click to enter/exit edit mode (readonly still allows selection mode) if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (mouse_in_bounds && !is_edit_mode) { should_toggle_edit_mode = TRUE; } else if (!mouse_in_bounds && is_edit_mode) { should_toggle_edit_mode = TRUE; } } // Content area - build/update line cache float content_width = bounds.width - TEXT_AREA_PADDING * 2; BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena); // Calculate content height from cache (O(1)) int total_lines = Dowa_Array_Length(state->line_cache); float content_height = total_lines * TEXT_AREA_LINE_HEIGHT; float visible_height = bounds.height - TEXT_AREA_PADDING * 2; float max_scroll = TA_Max_Float(0, content_height - visible_height); // Handle scrolling float wheel = GetMouseWheelMove(); if (mouse_in_bounds && wheel != 0) { state->scroll_offset_y -= wheel * TEXT_AREA_LINE_HEIGHT * 3; state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); } if (is_edit_mode) { boolean ctrl_pressed = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER); boolean shift_pressed = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); boolean text_changed = FALSE; double current_time = GetTime(); if (current_time - state->last_blink_time > 0.5) { state->cursor_visible = !state->cursor_visible; state->last_blink_time = current_time; } // Mouse Selection (use line cache for accurate position) if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && mouse_in_bounds) { int click_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos, state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, state->line_cache); state->cursor_pos = click_pos; state->selection_start = -1; state->selection_end = -1; state->is_selecting = TRUE; state->cursor_visible = TRUE; state->last_blink_time = current_time; } if (state->is_selecting && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { int drag_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos, state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, state->line_cache); if (drag_pos != state->cursor_pos) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; } state->selection_end = drag_pos; } } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { state->is_selecting = FALSE; if (state->selection_start >= 0 && state->selection_end >= 0) { if (state->selection_start > state->selection_end) { int temp = state->selection_start; state->selection_start = state->selection_end; state->selection_end = temp; } if (state->selection_start == state->selection_end) { state->selection_start = -1; state->selection_end = -1; } } } // Ctrl+A: Select All if (ctrl_pressed && IsKeyPressed(KEY_A)) { state->selection_start = 0; state->selection_end = text_len; state->cursor_pos = text_len; } // Ctrl+C: Copy if (ctrl_pressed && IsKeyPressed(KEY_C)) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); char *selected = GetSelectedText(text, sel_min, sel_max, arena); if (selected) { if (g_clipboard_text) free(g_clipboard_text); g_clipboard_text = strdup(selected); SetClipboardText(g_clipboard_text); } } } // Ctrl+X: Cut (not allowed in readonly mode) if (!readonly && ctrl_pressed && IsKeyPressed(KEY_X)) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); char *selected = GetSelectedText(text, sel_min, sel_max, arena); if (selected) { if (g_clipboard_text) free(g_clipboard_text); g_clipboard_text = strdup(selected); SetClipboardText(g_clipboard_text); } PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; text_changed = TRUE; } } // Ctrl+V: Paste (not allowed in readonly mode) if (!readonly && ctrl_pressed && IsKeyPressed(KEY_V)) { const char *clipboard = GetClipboardText(); if (clipboard && strlen(clipboard) > 0) { // Skip leading whitespace (spaces, tabs, newlines) while (*clipboard && (*clipboard == ' ' || *clipboard == '\t' || *clipboard == '\n' || *clipboard == '\r')) { clipboard++; } if (strlen(clipboard) > 0) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; } else { PushUndoState(state, text, arena); } int new_cursor; InsertTextAtCursor(text, text_size, state->cursor_pos, clipboard, &new_cursor); state->cursor_pos = new_cursor; text_changed = TRUE; } } } // Ctrl+Z: Undo / Ctrl+Shift+Z: Redo (not allowed in readonly mode) if (!readonly && ctrl_pressed && IsKeyPressed(KEY_Z)) { if (shift_pressed) { PerformRedo(state, text, text_size); } else { PerformUndo(state, text, text_size); } } // Arrow Keys Navigation if (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) { if (state->selection_start >= 0 && !shift_pressed) { state->cursor_pos = TA_Min_Int(state->selection_start, state->selection_end); state->selection_start = -1; state->selection_end = -1; } else if (state->cursor_pos > 0) { if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->cursor_pos--; state->selection_end = state->cursor_pos; } else { state->cursor_pos--; } } state->cursor_visible = TRUE; state->last_blink_time = current_time; } if (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) { if (state->selection_start >= 0 && !shift_pressed) { state->cursor_pos = TA_Max_Int(state->selection_start, state->selection_end); state->selection_start = -1; state->selection_end = -1; } else if (state->cursor_pos < text_len) { if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->cursor_pos++; state->selection_end = state->cursor_pos; } else { state->cursor_pos++; } } state->cursor_visible = TRUE; state->last_blink_time = current_time; } if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) { int line, col; GetLineAndColumn(text, state->cursor_pos, &line, &col); if (line > 0) { int new_pos = GetPosFromLineColumn(text, line - 1, col); if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->selection_end = new_pos; } else { state->selection_start = -1; state->selection_end = -1; } state->cursor_pos = new_pos; } state->cursor_visible = TRUE; state->last_blink_time = current_time; } if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) { int line, col; GetLineAndColumn(text, state->cursor_pos, &line, &col); int total_lines = CountLines(text); if (line < total_lines - 1) { int new_pos = GetPosFromLineColumn(text, line + 1, col); if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->selection_end = new_pos; } else { state->selection_start = -1; state->selection_end = -1; } state->cursor_pos = new_pos; } state->cursor_visible = TRUE; state->last_blink_time = current_time; } // Home/End keys if (IsKeyPressed(KEY_HOME)) { int line_start = GetLineStart(text, state->cursor_pos); if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->selection_end = line_start; } else { state->selection_start = -1; state->selection_end = -1; } state->cursor_pos = line_start; } if (IsKeyPressed(KEY_END)) { int line_end = GetLineEnd(text, state->cursor_pos); if (shift_pressed) { if (state->selection_start < 0) { state->selection_start = state->cursor_pos; state->selection_end = state->cursor_pos; } state->selection_end = line_end; } else { state->selection_start = -1; state->selection_end = -1; } state->cursor_pos = line_end; } // Text Input (not allowed in readonly mode) if (!readonly && !ctrl_pressed) { int key = GetCharPressed(); while (key > 0) { if (key >= 32 && key <= 126) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; text_changed = TRUE; } else if (!text_changed) { PushUndoState(state, text, arena); } char insert_str[2] = {(char)key, '\0'}; int new_cursor; InsertTextAtCursor(text, text_size, state->cursor_pos, insert_str, &new_cursor); state->cursor_pos = new_cursor; text_changed = TRUE; } key = GetCharPressed(); } } else if (readonly) { // Consume key presses to prevent them from being handled elsewhere while (GetCharPressed() > 0) {} } // Enter key (not allowed in readonly mode) if (!readonly && (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER))) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; } else { PushUndoState(state, text, arena); } int new_cursor; InsertTextAtCursor(text, text_size, state->cursor_pos, "\n", &new_cursor); state->cursor_pos = new_cursor; text_changed = TRUE; } // Backspace (not allowed in readonly mode) if (!readonly && (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE))) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; text_changed = TRUE; } else if (state->cursor_pos > 0) { PushUndoState(state, text, arena); DeleteTextRange(text, state->cursor_pos - 1, state->cursor_pos); state->cursor_pos--; text_changed = TRUE; } } // Delete key (not allowed in readonly mode) if (!readonly && (IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE))) { text_len = strlen(text); if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; text_changed = TRUE; } else if (state->cursor_pos < text_len) { PushUndoState(state, text, arena); DeleteTextRange(text, state->cursor_pos, state->cursor_pos + 1); text_changed = TRUE; } } // Tab key (not allowed in readonly mode) if (!readonly && IsKeyPressed(KEY_TAB)) { if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); PushUndoState(state, text, arena); DeleteTextRange(text, sel_min, sel_max); state->cursor_pos = sel_min; state->selection_start = -1; state->selection_end = -1; } else { PushUndoState(state, text, arena); } int new_cursor; InsertTextAtCursor(text, text_size, state->cursor_pos, " ", &new_cursor); state->cursor_pos = new_cursor; text_changed = TRUE; } // Auto-scroll to keep cursor visible // Rebuild cache if text changed during editing if (text_changed) { state->cache_valid = FALSE; BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena); } Vector2 cursor_screen = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds, state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, state->line_cache); float visible_top = bounds.y + TEXT_AREA_PADDING; float visible_bottom = bounds.y + bounds.height - TEXT_AREA_PADDING - TEXT_AREA_LINE_HEIGHT; if (cursor_screen.y < visible_top) { state->scroll_offset_y -= visible_top - cursor_screen.y; } else if (cursor_screen.y > visible_bottom) { state->scroll_offset_y += cursor_screen.y - visible_bottom; } state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll)); } // Drawing DrawRectangleSelectiveRounded(bounds, 0.2, 1, g_colors.secondary, FALSE, FALSE, TRUE, TRUE); DrawRectangleRec(AddPadding(bounds, 10), WHITE); // DrawRectangleRec(bounds, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255}); // DrawRectangleLinesEx(bounds, 1, is_edit_mode ? WHITE : GRAY); // DrawRectangleRoundedLines(bounds, 0.2, 1, is_edit_mode ? BLACK : GRAY); BeginScissorMode((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height); float content_x = bounds.x + TEXT_AREA_PADDING; float content_y = bounds.y + TEXT_AREA_PADDING - state->scroll_offset_y; text_len = strlen(text); // Calculate visible line range (only render what's visible) int first_visible_line = (int)(state->scroll_offset_y / TEXT_AREA_LINE_HEIGHT); int last_visible_line = first_visible_line + (int)(visible_height / TEXT_AREA_LINE_HEIGHT) + 2; int line_count = Dowa_Array_Length(state->line_cache); if (first_visible_line < 0) first_visible_line = 0; if (last_visible_line > line_count) last_visible_line = line_count; // Draw selection highlight (only for visible lines) if (state->selection_start >= 0 && state->selection_end >= 0 && state->selection_start != state->selection_end) { int sel_min = TA_Min_Int(state->selection_start, state->selection_end); int sel_max = TA_Max_Int(state->selection_start, state->selection_end); for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) { LineInfo *line = &state->line_cache[line_idx]; // Check if selection intersects this line if (sel_min < line->end_pos && sel_max > line->start_pos) { int highlight_start = TA_Max_Int(sel_min, line->start_pos); int highlight_end = TA_Min_Int(sel_max, line->end_pos); float x1 = content_x + MeasureTextRange(text, line->start_pos, highlight_start, GuiGetStyle(DEFAULT, TEXT_SIZE)); float x2 = content_x + MeasureTextRange(text, line->start_pos, highlight_end, GuiGetStyle(DEFAULT, TEXT_SIZE)); float y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT; DrawRectangle((int)x1, (int)y, (int)(x2 - x1), TEXT_AREA_LINE_HEIGHT, Fade(SKYBLUE, 0.5f)); } } } // Draw text using line cache (only visible lines) Color text_color = should_text_wrap ? BLACK : WHITE; for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) { LineInfo *line = &state->line_cache[line_idx]; if (line->char_count > 0) { char line_buffer[1024]; int line_len = TA_Min_Int(line->end_pos - line->start_pos, 1023); strncpy(line_buffer, text + line->start_pos, line_len); line_buffer[line_len] = '\0'; Vector2 draw_text_vector = { .x = content_x, .y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT }; DrawTextEx(GuiGetFont(), line_buffer, draw_text_vector, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), text_color); } } // Draw cursor if (is_edit_mode && state->cursor_visible) { Vector2 cursor_pos = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds, state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT, state->line_cache); DrawRectangle((int)cursor_pos.x, (int)cursor_pos.y, TEXT_AREA_CURSOR_WIDTH, TEXT_AREA_LINE_HEIGHT, BLACK); } EndScissorMode(); // Draw scrollbar if needed if (max_scroll > 0) { float scrollbar_height = (visible_height / content_height) * visible_height; float scrollbar_y = bounds.y + TEXT_AREA_PADDING + (state->scroll_offset_y / max_scroll) * (visible_height - scrollbar_height); DrawRectangle((int)(bounds.x + bounds.width - 8), (int)scrollbar_y, 6, (int)scrollbar_height, Fade(WHITE, 0.3f)); } return should_toggle_edit_mode; } void GuiTextAreaResetState(int id) { TextAreaState *state = GetTextAreaState(id); if (state) { if (state->undo_stack) { Dowa_Array_Free(state->undo_stack); state->undo_stack = NULL; } if (state->line_cache) { Dowa_Array_Free(state->line_cache); state->line_cache = NULL; } state->cache_valid = FALSE; state->is_initialized = FALSE; } } void GuiTextAreaResetAllStates(void) { for (int i = 0; i < g_text_area_state_count; i++) { if (g_text_area_states[i].undo_stack) { Dowa_Array_Free(g_text_area_states[i].undo_stack); g_text_area_states[i].undo_stack = NULL; } if (g_text_area_states[i].line_cache) { Dowa_Array_Free(g_text_area_states[i].line_cache); g_text_area_states[i].line_cache = NULL; } g_text_area_states[i].cache_valid = FALSE; g_text_area_states[i].is_initialized = FALSE; } g_text_area_state_count = 0; } // ============================================================================ // End TextArea Component // ============================================================================ typedef Dowa_KV(char*, char*) INPUT_HASHMAP; typedef struct { char *data; size_t size; } ResponseBuffer; typedef struct { char *filename; char *title; Rectangle rect; long time_modified; boolean deleted; } HistoryItem; typedef struct { Rectangle rectangle; char *label; bool active; } TabItem; typedef enum { TAB_HEADER = 0, TAB_BODY, TAB_GET_PARAMS, TAB_WEBSOCKET, TAB_LENGTH } PostDog_Tab_Enum; typedef enum { RESULT_TAB_BODY = 0, RESULT_TAB_HEADERS, RESULT_TAB_LENGTH } PostDog_ResultTab_Enum; // Text area IDs #define TEXT_AREA_ID_INPUT_HEADER 1 #define TEXT_AREA_ID_INPUT_BODY 2 #define TEXT_AREA_ID_INPUT_PARAMS 3 #define TEXT_AREA_ID_INPUT_WS 4 #define TEXT_AREA_ID_RESULT_BODY 5 #define TEXT_AREA_ID_RESULT_HEADERS 6 static uint32 counter = 0; static uv_mutex_t history_mutex; static uv_loop_t *main_loop = NULL; HistoryItem *history_items = NULL; HistoryItem *new_history_items = NULL; // Global UI state char *url_input_text = NULL; char **result_body_array = NULL; // [RESULT_TAB_BODY, RESULT_TAB_HEADERS] char **input_body_array = NULL; int active_method_dropdown = 0; int active_input_tab = 0; int active_result_tab = 0; Seobeo_WebSocket *ws = NULL; boolean WS_BREAK = FALSE; uv_thread_t websocket_thread_id; Color TEXT_COLOR = BLACK; boolean LOADING = FALSE; int CompareHistoryItemsByDate(const void *a, const void *b) { HistoryItem *itemA = (HistoryItem *)a; HistoryItem *itemB = (HistoryItem *)b; return (itemB->time_modified - itemA->time_modified); } char *PostDog_Extract_Title(const char *filename) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "r"); if (!file) return strdup(filename); char *title = malloc(sizeof(char) * 512); if (!fgets(title, 512, file)) { fclose(file); free(title); return strdup(filename); } fclose(file); // Strip trailing newline title[strcspn(title, "\n")] = '\0'; return title; } // TODO: Make this into generic fucntion so I can use it across different thing. void PostDog_List_Directory(const char *path, HistoryItem **p_file_arr) { HistoryItem *file_arr = *p_file_arr; #ifdef _WIN32 struct _finddata_t fileinfo; intptr_t handle; char search_path[256]; sprintf(search_path, "%s\\*", path); if ((handle = _findfirst(search_path, &fileinfo)) == -1L) { printf("Directory is empty or cannot be read.\n"); } else { do { HistoryItem item = {0}; item.filename = strdup(fileinfo.name); item.title = PostDog_Extract_Title(fileinfo.name); item.time_modified = fileinfo.time_write; item.deleted = FALSE; Dowa_Array_Push(file_arr, item); } while (_findnext(handle, &fileinfo) == 0); _findclose(handle); } #else struct dirent *entry; struct stat file_stat; DIR *dp = opendir(path); if (dp == NULL) return; char full_path[256]; while ((entry = readdir(dp))) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); if (stat(full_path, &file_stat) == 0) { HistoryItem item = {0}; item.filename = strdup(entry->d_name); item.title = PostDog_Extract_Title(entry->d_name); item.time_modified = file_stat.st_mtime; item.deleted = FALSE; Dowa_Array_Push(file_arr, item); } } closedir(dp); #endif // Update the caller's pointer in case array was reallocated *p_file_arr = file_arr; int count = Dowa_Array_Length(file_arr); if (count > 1) { qsort(file_arr, count, sizeof(HistoryItem), CompareHistoryItemsByDate); } } int PostDog_History_Load(HistoryItem **p_history_files) { if (access(POSTDOG_PATHS, F_OK) == -1) { printf("Directory '%s' not found. Creating it...\n", POSTDOG_PATHS); if (mkdir(POSTDOG_PATHS, 0777) != 0) return -1; return 0; } printf("Directory '%s' already exists.\n", POSTDOG_PATHS); PostDog_List_Directory(POSTDOG_PATHS, p_history_files); return 0; } bool InArea(Vector2 mouse_position, Rectangle area) { return ( mouse_position.x >= area.x && mouse_position.x < area.x + area.width && mouse_position.y >= area.y && mouse_position.y < area.y + area.height ); } bool Clicked(Vector2 mouse_position, Rectangle area) { return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)); } // -------- END of UI ---- // char *PostDog_Enum_To_String(int active_enum) { switch(active_enum) { case 0: return "GET"; case 1: return "POST"; case 2: return "PUT"; case 3: return "DELETE"; } return 0; } char *PostDog_Construct_URL(char *filename, char *out_buffer, size_t buffer_size) { snprintf(out_buffer, buffer_size, "%s/%s", POSTDOG_PATHS, filename); return out_buffer; } boolean PostDog_History_CreateFile(char *filename, char* values) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "w"); if (!file) { fprintf(stderr, "Failed to create a file: %s\n", full_file_path); return FALSE; } fwrite(values, 1, strlen(values), file); fclose(file); return TRUE; } void PostDog_Request_SaveFile(void) { const char *method = PostDog_Enum_To_String(active_method_dropdown); size_t new_file_size = 1024 * 1024; Dowa_Arena *arena = Dowa_Arena_Create(1024 * 1024 * 2); char *new_file = Dowa_Arena_Allocate(arena, 1024 * 1024); char *title = Dowa_Arena_Allocate(arena, strlen(method) + strlen(url_input_text) + 2); sprintf(title, "%s %s", method, url_input_text); snprintf( new_file, new_file_size, "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n" "---\n" "%s\n", title, url_input_text, method, input_body_array[TAB_HEADER], input_body_array[TAB_BODY], input_body_array[TAB_GET_PARAMS], input_body_array[TAB_WEBSOCKET], result_body_array[RESULT_TAB_BODY] ); char *filename = Dowa_Arena_Allocate(arena, 1024); if (!filename) { perror("Error opening file"); exit(EXIT_FAILURE); } char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37); if (!uuid4) { perror("Error uuid"); exit(EXIT_FAILURE); } uv_mutex_lock(&history_mutex); int32 seed = (uint32)time(NULL) ^ counter++; Dowa_String_UUID(seed, uuid4); snprintf(filename, 1024, "%s.txt", uuid4); if (PostDog_History_CreateFile(filename, new_file)) { HistoryItem item = {0}; item.filename = strdup(filename); item.title = strdup(title); item.deleted = FALSE; Dowa_Array_Push(new_history_items, item); } uv_mutex_unlock(&history_mutex); Dowa_Arena_Free(arena); } void PostDog_Websocket_Listen(void) { while (TRUE) { if (WS_BREAK) break; Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(ws); if (p_msg) { if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT) { printf("Response: %.*s\n", (int)p_msg->length, (char*)p_msg->data); char *body = result_body_array[RESULT_TAB_BODY]; size_t current_len = strlen(body); snprintf(body + current_len, RESULT_BUFFER_LENGTH - current_len, "\n%s", (char*)p_msg->data); } Seobeo_WebSocket_Message_Destroy(p_msg); } usleep(10000); } return; } void PostDog_Websocket_Connect(void) { // Pass headers from the Headers tab when connecting const char *headers = input_body_array[TAB_HEADER]; if (headers && strlen(headers) > 0) ws = Seobeo_WebSocket_Connect_With_Headers(url_input_text, headers); else ws = Seobeo_WebSocket_Connect(url_input_text); result_body_array[RESULT_TAB_BODY][0] = '\0'; result_body_array[RESULT_TAB_HEADERS][0] = '\0'; // Reset result text area states GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY); GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS); } int PostDog_Websocket_Send(void) { char *body = result_body_array[RESULT_TAB_BODY]; if (Seobeo_WebSocket_Send_Text(ws, input_body_array[active_input_tab]) < 0) { snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body), "Failed to send message\n"); return -1; } snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body), "\n%s", input_body_array[active_input_tab]); return 0; } void PostDog_Websocket_Destroy(uv_thread_t thread_id) { Seobeo_WebSocket_Destroy(ws); uv_thread_join(&thread_id); } void PostDog_Websocket_Start(void *arg) { PostDog_Websocket_Connect(); PostDog_Websocket_Listen(); } uv_thread_t PostDog_Websocket_Start_Thread() { uv_thread_t thread_id; if (uv_thread_create(&thread_id, PostDog_Websocket_Start, NULL) != 0) { perror("Failed to create thread"); memset(&thread_id, 0, sizeof(thread_id)); return thread_id; } return thread_id; } int PostDog_Http_Request(void) { Seobeo_Client_Request *req = Seobeo_Client_Request_Create(url_input_text); printf("URL: %s\n", url_input_text); Seobeo_Client_Response *res; switch (active_method_dropdown) { case 0: { Seobeo_Client_Request_Set_Method(req, "GET"); break; } case 1: { Seobeo_Client_Request_Set_Method(req, "POST"); break; } case 2: { Seobeo_Client_Request_Set_Method(req, "PUT"); break; } case 3: { Seobeo_Client_Request_Set_Method(req, "DELETE"); break; } } if (input_body_array[TAB_HEADER] && strlen(input_body_array[TAB_HEADER]) > 0) { char *headersCopy = strdup(input_body_array[TAB_HEADER]); char *line = strtok(headersCopy, "\n"); while (line != NULL) { while (*line == ' ' || *line == '\t') line++; if (strlen(line) > 0) Seobeo_Client_Request_Add_Header_Array(req, line); line = strtok(NULL, "\n"); } } Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number; res = Seobeo_Client_Request_Execute(req); if (res == NULL) { snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "Error: Failed to send the request\n"); result_body_array[RESULT_TAB_HEADERS][0] = '\0'; } else { // Store response body snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "%s", res->body ? res->body : ""); // Sanitize body for display (replace tabs, remove non-printable chars) SanitizeTextForDisplay(result_body_array[RESULT_TAB_BODY]); // Store response headers int offset = snprintf(result_body_array[RESULT_TAB_HEADERS], RESULT_BUFFER_LENGTH, "HTTP Status: %d %s\n\n", res->status_code, res->status_text ? res->status_text : ""); if (res->headers) { size_t header_count = Dowa_Array_Length(res->headers); for (size_t i = 0; i < header_count && offset < RESULT_BUFFER_LENGTH - 100; i++) { offset += snprintf(result_body_array[RESULT_TAB_HEADERS] + offset, RESULT_BUFFER_LENGTH - offset, "%s: %s\n", res->headers[i].key, res->headers[i].value); } } } printf("Body: %s\n", res ? res->body : "NULL"); Seobeo_Client_Request_Destroy(req); Seobeo_Client_Response_Destroy(res); PostDog_Request_SaveFile(); // Reset result text area states so scroll/cursor don't carry over GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY); GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS); return 0; } void PostDog_Http_Work(uv_work_t *req) { PostDog_Http_Request(); printf("HTTP request finished.\n"); } void PostDog_Http_Work_Done(uv_work_t *req, int status) { LOADING = FALSE; free(req); } void PostDog_Http_Thread_Request() { uv_work_t *work_req = malloc(sizeof(uv_work_t)); if (!work_req) { perror("Failed to allocate work request"); return; } LOADING = TRUE; if (uv_queue_work(main_loop, work_req, PostDog_Http_Work, PostDog_Http_Work_Done) != 0) { perror("Failed to queue work"); free(work_req); LOADING = FALSE; } } void PostDog_Update_URL(boolean is_url_updated) { char *params_start = strchr(url_input_text, '?'); if (is_url_updated) { if (!params_start) return; params_start++; Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); char buffer[4096] = ""; char **lines = Dowa_String_Split(params_start, "&", URL_TEXT_BUFFER_LENGTH, 1, arena); for (int i = 0; i < Dowa_Array_Length(lines); i++) { char *line = lines[i]; char **key_value = Dowa_String_Split(line, "=", (int)strlen(line), 1, arena); if (Dowa_Array_Length(key_value) < 2) break; snprintf(buffer + strlen(buffer), 4096 - strlen(buffer), "%s %s\n", key_value[0], key_value[1]); } snprintf(input_body_array[TAB_GET_PARAMS], URL_TEXT_BUFFER_LENGTH, "%s", buffer); int length = strlen(input_body_array[TAB_GET_PARAMS]); input_body_array[TAB_GET_PARAMS][length--] = '\0'; Dowa_Arena_Free(arena); } else { if (params_start) { size_t length = params_start - url_input_text; url_input_text[length] = '\0'; } int get_params_length = (int)strlen(input_body_array[TAB_GET_PARAMS]); if (get_params_length == 0) return; char *separator = "?"; Dowa_Arena *arena = Dowa_Arena_Create(1024*1024); char **lines = Dowa_String_Split(input_body_array[TAB_GET_PARAMS], "\n", get_params_length, 1, arena); for (int i = 0; i < Dowa_Array_Length(lines); i++) { char *line = lines[i]; char **key_value = Dowa_String_Split(line, " ", (int)strlen(line), 1, arena); if (Dowa_Array_Length(key_value) < 2) break; strcat(url_input_text, separator); strcat(url_input_text, key_value[0]); strcat(url_input_text, "="); for (int i = 1; i < Dowa_Array_Length(key_value); i++) { if (!key_value[i] || key_value[i][0] == '\0') break; if (i > 1) strcat(url_input_text, "%20"); strcat(url_input_text, key_value[i]); } separator = "&"; } Dowa_Arena_Free(arena); } } int PostDog_String_To_MethodEnum(char *value) { if (strstr(value, "GET")) return 0; if (strstr(value, "POST")) return 1; if (strstr(value, "PUT")) return 2; if (strstr(value, "DELETE")) return 3; return 0; } void PostDog_Params_Reset(void) { url_input_text[0] = '\0'; active_method_dropdown = 0; active_result_tab = 0; for (int i = 0; i < Dowa_Array_Length(input_body_array); i++) input_body_array[i][0] = '\0'; for (int i = 0; i < Dowa_Array_Length(result_body_array); i++) result_body_array[i][0] = '\0'; // Reset text area states when clearing GuiTextAreaResetAllStates(); } void PostDog_Load_File(const char *filename) { char full_file_path[512] = {0}; snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename); FILE *file = fopen(full_file_path, "r"); if (!file) return; fseek(file, 0, SEEK_END); size_t file_size = ftell(file); fseek(file, 0, SEEK_SET); Dowa_Arena *init_arena = Dowa_Arena_Create(file_size + 2); Dowa_Arena *split_arena = Dowa_Arena_Create(file_size * 2); char *file_buffer = Dowa_Arena_Allocate(init_arena, file_size+1); fread(file_buffer, 1, file_size, file); fclose(file); char **values = Dowa_String_Split(file_buffer, "---\n", file_size, 4, split_arena); for (int i = 0; i < Dowa_Array_Length(values); i++) { switch (i) { case 0: // Title - skip break; case 1: // URL snprintf(url_input_text, strlen(values[i]) + 1, "%s", values[i]); url_input_text[strcspn(url_input_text, "\n")] = '\0'; break; case 2: // Method active_method_dropdown = PostDog_String_To_MethodEnum(values[i]); break; case 3: // Headers (TAB_HEADER) case 4: // Body (TAB_BODY) case 5: // Get Params (TAB_GET_PARAMS) case 6: // Websocket (TAB_WEBSOCKET) { int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3 snprintf(input_body_array[map_index], strlen(values[i]) + 1, "%s", values[i]); // Trim trailing newlines for (int j = strlen(values[i]); j > 0; j--) { if (input_body_array[map_index][j] == '\n') { input_body_array[map_index][j] = '\0'; break; } } break; } default: // Response (index 7+) - load into body tab for backward compatibility snprintf(result_body_array[RESULT_TAB_BODY], strlen(values[i]) + 1, "%s", values[i]); break; } } // Reset result tab to body active_result_tab = RESULT_TAB_BODY; // Reset text area states when loading new file GuiTextAreaResetAllStates(); Dowa_Arena_Free(init_arena); Dowa_Arena_Free(split_arena); } // ============================================================================ // UI LAYOUT STRUCTURE - All rectangles for the UI // ============================================================================ typedef struct { // Main areas Rectangle screen; Rectangle sidebar; Rectangle content; // Sidebar sections Rectangle logo_area; Rectangle history_list; // URL bar section Rectangle url_bar; Rectangle method_dropdown; Rectangle url_input; Rectangle send_button; // Body section (input + result split) Rectangle body_area; Rectangle input_panel; Rectangle result_panel; // Input panel internals Rectangle input_tabs; Rectangle input_body; // Result panel internals Rectangle result_tabs; Rectangle result_body; } UILayout; // ============================================================================ // UI STATE - All interactive state // ============================================================================ typedef struct { boolean url_edit_mode; boolean method_edit_mode; boolean input_body_edit_mode; boolean result_body_edit_mode; int prev_input_tab; float history_scroll_offset; } UIState; // ============================================================================ // LAYOUT CALCULATION FUNCTIONS // ============================================================================ static UILayout CalculateLayout(int screen_width, int screen_height, float padding) { UILayout layout = {0}; // Screen layout.screen = (Rectangle){0, 0, screen_width, screen_height}; // Main split: sidebar (20%) | content (80%) layout.sidebar = LeftColumn(layout.screen, 0.20f, 0); layout.content = RightColumn(layout.screen, layout.sidebar, 0); // Sidebar sections layout.logo_area = (Rectangle){ .x = layout.sidebar.x, .y = layout.sidebar.y, .width = layout.sidebar.width, .height = 120 }; layout.history_list = (Rectangle){ .x = layout.sidebar.x + padding, .y = layout.logo_area.y + layout.logo_area.height + padding, .width = layout.sidebar.width - (2 * padding), .height = layout.sidebar.height - layout.logo_area.height - (2 * padding) }; // URL bar section (top of content area) float url_bar_height = 60; layout.url_bar = (Rectangle){ .x = layout.content.x, .y = layout.content.y + padding, .width = layout.content.width, .height = url_bar_height }; // URL bar components (method dropdown + URL input + send button) float dropdown_width = 100; float button_width = 80; float url_bar_inner_padding = 8; float control_height = 36; float control_y = layout.url_bar.y + (layout.url_bar.height - control_height) / 2; layout.method_dropdown = (Rectangle){ .x = layout.url_bar.x + padding, .y = control_y, .width = dropdown_width, .height = control_height }; layout.url_input = (Rectangle){ .x = layout.method_dropdown.x + layout.method_dropdown.width + url_bar_inner_padding, .y = control_y, .width = layout.url_bar.width - dropdown_width - button_width - (4 * padding) - (2 * url_bar_inner_padding), .height = control_height }; layout.send_button = (Rectangle){ .x = layout.url_input.x + layout.url_input.width + url_bar_inner_padding, .y = control_y, .width = button_width, .height = control_height }; // Body area (below URL bar) layout.body_area = (Rectangle){ .x = layout.content.x, .y = layout.url_bar.y + layout.url_bar.height + padding, .width = layout.content.width, .height = layout.content.height - layout.url_bar.height - (3 * padding) }; // Split body into input (left) and result (right) panels float split_ratio = 0.5f; layout.input_panel = (Rectangle){ .x = layout.body_area.x, .y = layout.body_area.y, .width = layout.body_area.width * split_ratio, .height = layout.body_area.height }; layout.result_panel = (Rectangle){ .x = layout.body_area.x + layout.input_panel.width, .y = layout.body_area.y, .width = layout.body_area.width - layout.input_panel.width, .height = layout.body_area.height }; // Input panel internals float tab_height = 40; layout.input_tabs = (Rectangle){ .x = layout.input_panel.x + padding, .y = layout.input_panel.y + padding, .width = layout.input_panel.width - (2 * padding), .height = tab_height }; layout.input_body = (Rectangle){ .x = layout.input_panel.x + padding, .y = layout.input_tabs.y + layout.input_tabs.height, .width = layout.input_panel.width - (2 * padding), .height = layout.input_panel.height - tab_height - (2 * padding) }; // Result panel internals layout.result_tabs = (Rectangle){ .x = layout.result_panel.x + padding, .y = layout.result_panel.y + padding, .width = layout.result_panel.width - (2 * padding), .height = tab_height }; layout.result_body = (Rectangle){ .x = layout.result_panel.x + padding, .y = layout.result_tabs.y + layout.result_tabs.height, .width = layout.result_panel.width - (2 * padding), .height = layout.result_panel.height - tab_height - (2 * padding) }; return layout; } // ============================================================================ // DRAWING FUNCTIONS - Separated for clarity // ============================================================================ static void DrawSidebar(UILayout *layout, Texture2D logo_texture, float padding) { // Sidebar background with rounded left corners DrawRectangleRec(layout->sidebar, g_colors.primary); // Logo Rectangle logo_inner = AddPadding(layout->logo_area, padding); float logo_size = logo_inner.height < logo_inner.width ? logo_inner.height : logo_inner.width; Rectangle dest_rect = { .x = logo_inner.x + (logo_inner.width - logo_size) / 2, .y = logo_inner.y, .width = logo_size, .height = logo_size }; Rectangle source_rect = {0, 0, logo_texture.width, logo_texture.height}; DrawTexturePro(logo_texture, source_rect, dest_rect, (Vector2){0, 0}, 0.0f, WHITE); } static void DrawHistoryList(UILayout *layout, Vector2 mouse_pos, float padding, float scroll_offset) { int32 new_len = Dowa_Array_Length(new_history_items); int32 history_len = Dowa_Array_Length(history_items); int32 total = new_len + history_len; float item_height = 70; BeginScissorMode(layout->history_list.x, layout->history_list.y, layout->history_list.width, layout->history_list.height); int32 visible_index = 0; for (int i = 0; i < total; i++) { HistoryItem *item = i < new_len ? &new_history_items[new_len - i - 1] : &history_items[i - new_len]; if (item->deleted) continue; // Calculate item rectangle Rectangle item_rect = { .x = layout->history_list.x, .y = layout->history_list.y + (padding + item_height) * visible_index + scroll_offset, .width = layout->history_list.width, .height = item_height }; item->rect = item_rect; visible_index++; // Skip if not visible if (item_rect.y + item_rect.height < layout->history_list.y || item_rect.y > layout->history_list.y + layout->history_list.height) continue; // Draw item background DrawRectangleRounded(item_rect, 0.3f, 8, g_colors.secondary); // Title area (top 60%) Rectangle title_rect = item_rect; title_rect.height = item_rect.height * 0.55f; // Draw title text Rectangle title_text_rect = AddPadding(title_rect, 8); if (item->title) { DrawTextEx(GuiGetFont(), item->title, (Vector2){ .x=title_text_rect.x, .y=title_text_rect.y + 4 }, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text); } // Button area (bottom 40%) Rectangle button_area = { .x = item_rect.x, .y = title_rect.y + title_rect.height, .width = item_rect.width, .height = item_rect.height - title_rect.height }; Rectangle btn_view = { .x = button_area.x + padding, .y = button_area.y, .width = (button_area.width - 3 * padding) / 2, .height = button_area.height - padding }; Rectangle btn_delete = { .x = btn_view.x + btn_view.width + padding, .y = btn_view.y, .width = btn_view.width, .height = btn_view.height }; // Hover cursor for buttons if (CheckCollisionPointRec(mouse_pos, btn_view) || CheckCollisionPointRec(mouse_pos, btn_delete)) SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); if (GuiButton(btn_view, "View")) PostDog_Load_File(item->filename); if (GuiButton(btn_delete, "Delete")) { char delete_path[512]; if (!remove(PostDog_Construct_URL(item->filename, delete_path, sizeof(delete_path)))) item->deleted = TRUE; else fprintf(stderr, "Couldn't delete file: %s\n", item->filename); } } EndScissorMode(); // Scrollbar float content_height = visible_index * (item_height + padding); if (content_height > layout->history_list.height) { float scrollbar_height = (layout->history_list.height / content_height) * layout->history_list.height; float scrollbar_y = layout->history_list.y - (scroll_offset / content_height) * layout->history_list.height; Rectangle scrollbar_rect = { layout->history_list.x + layout->history_list.width - 6, scrollbar_y, 4, scrollbar_height }; DrawRectangleRounded(scrollbar_rect, 0.5f, 4, Fade(g_colors.text_light, 0.5f)); } } static void DrawURLBar(UILayout *layout, UIState *state, float padding) { // URL bar background Rectangle url_bar_bg = AddPadding(layout->url_bar, padding / 2); DrawRectangleRounded(url_bar_bg, 0.3f, 8, g_colors.secondary); // Combined method dropdown + URL input with rounded corners Rectangle combined_input = { .x = layout->method_dropdown.x, .y = layout->method_dropdown.y, .width = layout->url_input.x + layout->url_input.width - layout->method_dropdown.x, .height = layout->method_dropdown.height }; // Method colors: GET=Green, POST=Blue, PUT=Orange, DELETE=Red Color method_colors[] = { (Color){76, 175, 80, 255}, // GET - Green (Color){33, 150, 243, 255}, // POST - Blue (Color){255, 152, 0, 255}, // PUT - Orange (Color){244, 67, 54, 255} // DELETE - Red }; // Method dropdown + URL input combined component DropdownTextBoxConfig url_config = { .dropdown_items = "GET;POST;PUT;DELETE", .dropdown_active = &active_method_dropdown, .dropdown_edit_mode = &state->method_edit_mode, .dropdown_width = layout->method_dropdown.width, .text_buffer = url_input_text, .text_buffer_size = URL_TEXT_BUFFER_LENGTH, .text_edit_mode = &state->url_edit_mode, .corner_radius = 0.2f, .background_color = g_colors.background, .border_color = g_colors.border, .text_color = g_colors.text, .item_colors = method_colors, .item_count = 4 }; PostDog_DropdownTextBox(combined_input, url_config); // Handle Enter key in URL input if (state->url_edit_mode && IsKeyPressed(KEY_ENTER)) { PostDog_Http_Thread_Request(); state->url_edit_mode = FALSE; } // Send button DrawRectangleRounded(layout->send_button, 0.3f, 8, g_colors.primary); Rectangle btn_text_rect = layout->send_button; char *btn_text = "Send"; int text_width = MeasureText(btn_text, GuiGetStyle(DEFAULT, TEXT_SIZE)); DrawTextEx(GuiGetFont(), btn_text, (Vector2){ .x=btn_text_rect.x + (btn_text_rect.width - text_width) / 2, .y=btn_text_rect.y + (btn_text_rect.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 }, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text_light); if (CheckCollisionPointRec(GetMousePosition(), layout->send_button) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) PostDog_Http_Thread_Request(); } static void DrawBodyPanels(UILayout *layout, UIState *state, float padding) { // Draw split panel background (input left, result right) DrawRectangleSelectiveRounded(layout->input_panel, 12, 8, g_colors.background, TRUE, FALSE, FALSE, TRUE); DrawRectangleSelectiveRounded(layout->result_panel, 12, 8, LOADING ? Fade(g_colors.error, 0.3f) : g_colors.secondary, FALSE, TRUE, TRUE, FALSE); // Tab labels const char *tab_labels[] = {"Headers", "Body", "Params", "WebSocket"}; int tab_count = 4; // Draw custom tabs PostDog_TabBarSimple(layout->input_tabs, tab_labels, tab_count, &active_input_tab); // Input body text area int text_area_id = TEXT_AREA_ID_INPUT_HEADER + active_input_tab; if (GuiTextArea(text_area_id, layout->input_body, input_body_array[active_input_tab], BODY_BUFFER_LENGTH, state->input_body_edit_mode, TRUE, FALSE, g_text_area_arena)) state->input_body_edit_mode = !state->input_body_edit_mode; // WebSocket send button (only on WebSocket tab) if (active_input_tab == TAB_WEBSOCKET) { WS_BREAK = FALSE; Rectangle ws_send_btn = { .x = layout->input_body.x + layout->input_body.width - 90 - padding, .y = layout->input_body.y + layout->input_body.height - 35 - padding, .width = 90, .height = 35 }; DrawRectangleRounded(ws_send_btn, 0.3f, 8, g_colors.highlight); char *ws_text = !ws ? "Connect" : "Send"; int ws_text_width = MeasureText(ws_text, GuiGetStyle(DEFAULT, TEXT_SIZE)); DrawTextEx(GuiGetFont(), ws_text, (Vector2) { .x=ws_send_btn.x + (ws_send_btn.width - ws_text_width) / 2, .y=ws_send_btn.y + (ws_send_btn.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 }, GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text); if ((CheckCollisionPointRec(GetMousePosition(), ws_send_btn) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) || (state->input_body_edit_mode && IsKeyDown(KEY_LEFT_SHIFT) && IsKeyPressed(KEY_ENTER))) { if (!ws) websocket_thread_id = PostDog_Websocket_Start_Thread(); usleep(10000); PostDog_Websocket_Send(); } } else { WS_BREAK = TRUE; } // Result tabs const char *result_tab_labels[] = {"Body", "Headers"}; int result_tab_count = 2; PostDog_TabBarSimple(layout->result_tabs, result_tab_labels, result_tab_count, &active_result_tab); // Result body text area int result_text_area_id = TEXT_AREA_ID_RESULT_BODY + active_result_tab; // Result text area is readonly (selectable/copyable but not editable) if (GuiTextArea(result_text_area_id, layout->result_body, result_body_array[active_result_tab], RESULT_BUFFER_LENGTH, state->result_body_edit_mode, TRUE, TRUE, g_text_area_arena)) state->result_body_edit_mode = !state->result_body_edit_mode; PostDog_Update_URL(state->url_edit_mode); } // ============================================================================ // MAIN FUNCTION // ============================================================================ int main() { // ======================================================================== // INITIALIZATION // ======================================================================== SetConfigFlags(FLAG_WINDOW_UNDECORATED); // Initialize libuv main_loop = uv_default_loop(); uv_mutex_init(&history_mutex); // Initialize color scheme PostDog_InitColorScheme(); // Initialize window InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog"); SetWindowState(FLAG_WINDOW_RESIZABLE); SetTargetFPS(60); // Load resources Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0); GuiSetFont(customFont); GuiSetStyle(DEFAULT, TEXT_SIZE, TEXT_SIZE_DEFAULT); Image logo_original = LoadImage("postdog/logo_bigger.png"); ImageResize(&logo_original, 200, 200); SetWindowIcon(logo_original); Texture2D logo_texture = LoadTextureFromImage(logo_original); // ToggleBorderlessWindowed(); UnloadImage(logo_original); // Initialize text area arena g_text_area_arena = Dowa_Arena_Create(1024 * 1024 * 4); // Initialize history Dowa_Array_Reserve(history_items, 10); Dowa_Array_Reserve(new_history_items, 10); PostDog_History_Load(&history_items); // Initialize text buffers url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH); snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, URL_TEXT_DEFAULT); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); snprintf(input_body_array[TAB_HEADER], HEADER_BUFFER_LENGTH, HEADER_TEXT_DEFAULT); snprintf(input_body_array[TAB_BODY], BODY_BUFFER_LENGTH, BODY_TEXT_DEFAULT); snprintf(input_body_array[TAB_GET_PARAMS], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT); snprintf(input_body_array[TAB_WEBSOCKET], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT); // Initialize result buffers (body and headers tabs) Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH)); Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH)); result_body_array[RESULT_TAB_BODY][0] = '\0'; result_body_array[RESULT_TAB_HEADERS][0] = '\0'; // Initialize UI state UIState ui_state = {0}; float padding = 10.0f; // ======================================================================== // MAIN LOOP // ======================================================================== while (!WindowShouldClose()) { // Process libuv events (non-blocking) uv_run(main_loop, UV_RUN_NOWAIT); // Handle keyboard shortcuts DefaultBehaviours(); // ==================================================================== // LAYOUT CALCULATION // ==================================================================== int screen_width = GetScreenWidth(); int screen_height = GetScreenHeight(); UILayout layout = CalculateLayout(screen_width, screen_height, padding); // ==================================================================== // INPUT HANDLING // ==================================================================== Vector2 mouse_pos = GetMousePosition(); float mouse_wheel = GetMouseWheelMove(); // Reset cursor SetMouseCursor(MOUSE_CURSOR_DEFAULT); // History scroll if (CheckCollisionPointRec(mouse_pos, layout.history_list) && mouse_wheel != 0) { ui_state.history_scroll_offset += mouse_wheel * 30; int32 total = Dowa_Array_Length(new_history_items) + Dowa_Array_Length(history_items); float content_height = total * 80; float max_scroll = content_height - layout.history_list.height; if (ui_state.history_scroll_offset > 0) ui_state.history_scroll_offset = 0; if (max_scroll > 0 && ui_state.history_scroll_offset < -max_scroll) ui_state.history_scroll_offset = -max_scroll; } // Cursor changes for interactive areas if (CheckCollisionPointRec(mouse_pos, layout.send_button) || CheckCollisionPointRec(mouse_pos, layout.input_tabs) || CheckCollisionPointRec(mouse_pos, layout.logo_area)) SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Logo click resets if (CheckCollisionPointRec(mouse_pos, layout.logo_area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) PostDog_Params_Reset(); // ==================================================================== // DRAWING // ==================================================================== BeginDrawing(); ClearBackground(g_colors.background); DrawRectangleSelectiveRounded(layout.content, 12, 8, g_colors.secondary, FALSE, TRUE, TRUE, FALSE); DrawSidebar(&layout, logo_texture, padding); DrawHistoryList(&layout, mouse_pos, padding, ui_state.history_scroll_offset); DrawBodyPanels(&layout, &ui_state, padding); DrawURLBar(&layout, &ui_state, padding); EndDrawing(); } // ======================================================================== // CLEANUP // ======================================================================== GuiTextAreaResetAllStates(); Dowa_Arena_Free(g_text_area_arena); uv_mutex_destroy(&history_mutex); uv_loop_close(main_loop); UnloadTexture(logo_texture); CloseWindow(); return 0; }