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;
}