view postdog/main.c @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -0800
parents fff1b048dda6
children 48f260576059
line wrap: on
line source

/**
 * Entirely written by Claude AI.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <dirent.h>


// third party
#include <curl/curl.h>
#include "third_party/raylib/include/raylib.h"
#define RAYGUI_IMPLEMENTATION
#include "third_party/raylib/include/raygui.h"

#define SCREEN_WIDTH 1280
#define SCREEN_HEIGHT 780
#define TEXT_SIZE 10
#define LINE_HEIGHT 15
#define GENERIC_PADDING 15

#define SIDEBAR_WIDTH 200
#define SIDEBAR_PADDING_GENERAL 5
#define SIDEBAR_AREA_PADDING_X 10
#define SIDEBAR_AREA_PADDING_Y 10
#define SIDEBAR_REFERSH_BUTTON_WIDTH 90
#define SIDEBAR_REFERSH_BUTTON_HEIGHT 20
#define SIDEBAR_HISTORY_ITEM_HEIGHT 45

#define URL_INPUT_HEIGHT 40

#define TAB_LEN 3
#define METHOD_BUTTON_WIDTH 100
#define METHOD_BUTTON_HEIGHT 40
#define SEND_BUTTON_WIDTH 100
#define SEND_BUTTON_HEIGHT 40

#define JSON_INPUT_BUFFER_LEN 8192
#define HEADER_INPUT_BUFFER_LEN 4096
#define PARAM_INPUT_BUFFER_LEN 4096
#define MAX_HISTORY_ITEMS 100

// Structure to hold response data
typedef struct {
  char *data;
  size_t size;
} ResponseBuffer;

// Structure to hold history item
typedef struct {
  char filename[256];
  char displayName[128];
  char method[16];
  time_t timestamp;
} HistoryItem;

typedef enum {
  ActiveTab_JSON = 0,
  ActiveTab_Headers, 
  ActiveTab_Params,
} ActiveTab;

// Callback function for curl to write response data
static size_t Postdog_Curl_Callback(void *contents, size_t size, size_t nmemb, void *userp)
{
  size_t real_size = size * nmemb;
  ResponseBuffer *buf = (ResponseBuffer *)userp;

  char *ptr = realloc(buf->data, buf->size + real_size + 1);
  if (ptr == NULL)
  {
    printf("Not enough memory for response\n");
    return 0;
  }

  buf->data = ptr;
  memcpy(&(buf->data[buf->size]), contents, real_size);
  buf->size += real_size;
  buf->data[buf->size] = 0;

  return real_size;
}

// Function to make HTTP request using curl
int PostDog_Make_HttpRequest(const char *url, const char *method, const char *headers,
                    const char *body, char *response, size_t responseSize)
{
  CURL *curl;
  CURLcode res;
  ResponseBuffer buffer = { .data = malloc(1), .size = 0 };

  if (buffer.data == NULL)
  {
    snprintf(response, responseSize, "Error: Failed to allocate memory");
    return -1;
  }
  buffer.data[0] = '\0';

  curl_global_init(CURL_GLOBAL_DEFAULT);
  curl = curl_easy_init();

  if (curl)
  {
    struct curl_slist *headerList = NULL;

    // Set URL
    curl_easy_setopt(curl, CURLOPT_URL, url);

    // Set HTTP method
    if (strcmp(method, "POST") == 0)
    {
      curl_easy_setopt(curl, CURLOPT_POST, 1L);
      if (body && strlen(body) > 0) {
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
      }
    } 
    else if (strcmp(method, "PUT") == 0)
    {
      curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
      if (body && strlen(body) > 0) {
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
      }
    }
    else if (strcmp(method, "DELETE") == 0)
    {
      curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    }
    // Default is GET

    // Parse and add headers
    if (headers && strlen(headers) > 0)
    {
      char *headersCopy = strdup(headers);
      char *line = strtok(headersCopy, "\n");
      while (line != NULL) {
        // Trim whitespace
        while (*line == ' ' || *line == '\t') line++;
        if (strlen(line) > 0) {
          headerList = curl_slist_append(headerList, line);
        }
        line = strtok(NULL, "\n");
      }
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList);
      free(headersCopy);
    }

    // Set write callback
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Postdog_Curl_Callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer);

    // Follow redirects
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

    // Set timeout
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

    // Perform request
    res = curl_easy_perform(curl);

    if (res != CURLE_OK) {
      snprintf(response, responseSize, "Error: %s\n", curl_easy_strerror(res));
    } else {
      long response_code;
      curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

      snprintf(response, responseSize, "HTTP Status: %ld\n\n%s",
               response_code, buffer.data ? buffer.data : "");
    }

    // Cleanup
    if (headerList) curl_slist_free_all(headerList);
    curl_easy_cleanup(curl);
  } else {
    snprintf(response, responseSize, "Error: Failed to initialize curl");
  }

  free(buffer.data);
  curl_global_cleanup();

  return 0;
}

void PostDog_HistoryDirectory_Exists()
{
  struct stat st = {0};
  if (stat("history", &st) == -1)
  {
    mkdir("history", 0700);
  }
}

int PostDog_HistoryDistory_ItemsLoad(HistoryItem *items, int maxItems)
{
  DIR *dir = opendir("history");
  if (!dir) return 0;

  struct dirent *entry;
  int count = 0;

  while ((entry = readdir(dir)) != NULL && count < maxItems)
  {
    if (entry->d_name[0] == '.') continue;
    if (strstr(entry->d_name, ".txt") == NULL) continue;

    // Parse filename: YYYYMMDD_HHMMSS_METHOD.txt
    strncpy(items[count].filename, entry->d_name, sizeof(items[count].filename) - 1);

    // Extract method from filename
    char *methodStart = strrchr(entry->d_name, '_');
    if (methodStart)
    {
      methodStart++; // Skip underscore
      char *dotPos = strchr(methodStart, '.');
      if (dotPos) {
        int len = dotPos - methodStart;
        if (len < 16) {
          strncpy(items[count].method, methodStart, len);
          items[count].method[len] = '\0';
        }
      }
    }

    // Create display name: METHOD - YYYYMMDD HHMMSS
    char dateTime[32] = "";
    if (strlen(entry->d_name) >= 15)
    {
      snprintf(dateTime, sizeof(dateTime), "%.8s %.6s",
               entry->d_name, entry->d_name + 9);
    }
    snprintf(items[count].displayName, sizeof(items[count].displayName),
             "%s - %s", items[count].method, dateTime);
    count++;
  }

  closedir(dir);
  return count;
}

int PostDog_HistoryDirectory_LoadRequest(const char *filename, char *url, char *method, char *headers, char *body)
{
  char filepath[512];
  snprintf(filepath, sizeof(filepath), "history/%s", filename);

  FILE *f = fopen(filepath, "r");
  if (!f) return -1;

  char line[2048];
  int section = 0; // 0=url, 1=method, 2=headers, 3=body

  url[0] = '\0';
  method[0] = '\0';
  headers[0] = '\0';
  body[0] = '\0';

  while (fgets(line, sizeof(line), f))
  {
    // Remove newline
    line[strcspn(line, "\n")] = 0;

    if (section == 0)
    {
      // First line is URL
      strncpy(url, line, 1024 - 1);
      section = 1;
    } 
    else if (strncmp(line, "Method: ", 8) == 0)
    {
      strncpy(method, line + 8, 15);
      section = 2;
    }
    else if (strncmp(line, "Headers:", 8) == 0)
    {
      section = 2;
    }
    else if (strcmp(line, "---") == 0)
    {
      section = 3;
    }
    else if (strncmp(line, "Body:", 5) == 0)
    {
      section = 3;
    }
    else if (section == 2 && strcmp(line, "None") != 0)
    {
      // Add header line
      if (strlen(headers) > 0) strcat(headers, "\n");
      strncat(headers, line, HEADER_INPUT_BUFFER_LEN - strlen(headers) - 1);
    }
    else if (section == 3 && strcmp(line, "None") != 0)
    {
      // Add body line
      if (strlen(body) > 0) strcat(body, "\n");
      strncat(body, line, JSON_INPUT_BUFFER_LEN - strlen(body) - 1);
    }
  }

  fclose(f);
  return 0;
}

void Postdog_UpdateUrlWithParams(char *url, size_t urlSize, const char *baseUrl, const char *params)
{
  // Find if there's already a ? in the URL
  char *questionMark = strchr(baseUrl, '?');

  if (questionMark != NULL) {
    // URL already has params, just copy the base
    strncpy(url, baseUrl, urlSize - 1);
  } else {
    // No params yet, add them
    snprintf(url, urlSize, "%s", baseUrl);
  }

  // Parse and append params
  if (params && strlen(params) > 0) {
    char *paramsCopy = strdup(params);
    char *line = strtok(paramsCopy, "\n");
    bool firstParam = (questionMark == NULL);

    while (line != NULL) {
      // Trim whitespace
      while (*line == ' ' || *line == '\t') line++;

      if (strlen(line) > 0 && strchr(line, '=')) {
        size_t currentLen = strlen(url);
        if (currentLen + 2 < urlSize) {
          strcat(url, firstParam ? "?" : "&");
          firstParam = false;
          strncat(url, line, urlSize - strlen(url) - 1);
        }
      }
      line = strtok(NULL, "\n");
    }
    free(paramsCopy);
  }
}

// Save request to history file
void Postdog_SaveRequestToHistory(const char *url, const char *method, const char *headers, const char *body)
{
  PostDog_HistoryDirectory_Exists();

  // Generate filename with timestamp
  time_t now = time(NULL);
  struct tm *t = localtime(&now);
  char filename[256];
  snprintf(filename, sizeof(filename), "history/%04d%02d%02d_%02d%02d%02d_%s.txt",
           t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
           t->tm_hour, t->tm_min, t->tm_sec, method);

  FILE *f = fopen(filename, "w");
  if (f == NULL) {
    printf("Failed to create history file\n");
    return;
  }

  // Write URL
  fprintf(f, "%s\n", url);

  // Write method
  fprintf(f, "Method: %s\n", method);

  // Write headers
  fprintf(f, "Headers:\n");
  if (headers && strlen(headers) > 0)
  {
    fprintf(f, "%s\n", headers);
  }
  else
  {
    fprintf(f, "None\n");
  }

  fprintf(f, "---\n");

  // Write body
  fprintf(f, "Body:\n");
  if (body && strlen(body) > 0)
  {
    fprintf(f, "%s\n", body);
  }
  else
  {
    fprintf(f, "None\n");
  }

  fclose(f);
  printf("Request saved to %s\n", filename);
}

void PostDog_Render_TextWithScroll(Rectangle textArea, Vector2 scroll, char *input)
{
  BeginScissorMode(textArea.x, textArea.y, textArea.width, textArea.height);
  
  int yPos = textArea.y + 5 - (int)scroll.y;
  int charactersPerLine = (int)(textArea.width / (TEXT_SIZE/1.5)); // Account for padding
  int totalPos = 0;
  int inputLen = strlen(input);
  
  while (totalPos < inputLen)
  {
    int lineEnd = totalPos;
    int lineLength = 0;
    
    while (lineEnd < inputLen && lineLength < charactersPerLine && input[lineEnd] != '\n')
    {
      lineEnd++;
      lineLength++;
    }
    
    if (yPos + LINE_HEIGHT > textArea.y && yPos < textArea.y + textArea.height)
    {
      char savedChar = input[lineEnd];
      input[lineEnd] = '\0';
      DrawText(&input[totalPos], textArea.x + 5, yPos, TEXT_SIZE, DARKGRAY);
      input[lineEnd] = savedChar;
    }
    
    yPos += LINE_HEIGHT;
    
    totalPos = lineEnd;
    if (totalPos < inputLen && input[totalPos] == '\n')
    {
      totalPos++;
    }
  }
  
  EndScissorMode();
}

int main()
{
  InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog - HTTP Client");
  SetWindowState(FLAG_WINDOW_RESIZABLE);
  SetTargetFPS(60);

  PostDog_HistoryDirectory_Exists();

  // UI State
  char urlInput[1024] = "https://httpbin.org/get";
  bool urlEditMode = false;

  char jsonInput[JSON_INPUT_BUFFER_LEN] = "{\"key\":\"value\"}";
  bool jsonEditMode = false;

  char headersInput[HEADER_INPUT_BUFFER_LEN] = "Content-Type: application/json";
  bool headersEditMode = false;

  char responseText[16384] = "Response will appear here...\n\nTry the default URL or enter your own!";

  char paramsInput[PARAM_INPUT_BUFFER_LEN] = "key1=value1\nkey2=value2";
  bool paramsEditMode = false;

  ActiveTab activeTab = ActiveTab_JSON; // 0 = JSON, 1 = Headers, 2 = Params

  // HTTP method selection
  int methodActive = 0;
  bool methodDropdown = false;
  const char *methods[] = { "GET", "POST", "PUT", "DELETE" };

  // Scroll support
  Vector2 jsonScroll = { 0, 0 };
  Vector2 headersScroll = { 0, 0 };
  Vector2 paramsScroll = { 0, 0 };
  Vector2 responseScroll = { 0, 0 };
  Vector2 historyScroll = { 0, 0 };

  // History
  HistoryItem historyItems[MAX_HISTORY_ITEMS];
  int historyCount = 0;
  int selectedHistoryIndex = -1;

  // Load initial history
  historyCount = PostDog_HistoryDistory_ItemsLoad(historyItems, MAX_HISTORY_ITEMS);

  while (!WindowShouldClose())
  {
    // Get current window dimensions for responsive layout
    int screenWidth = GetScreenWidth();
    int screenHeight = GetScreenHeight();

    // Layout calculations
    Rectangle sidebar = { 0, 10, SIDEBAR_WIDTH, screenHeight };

    float mainX = SIDEBAR_WIDTH + GENERIC_PADDING;
    float mainWidth = screenWidth - SIDEBAR_WIDTH - GENERIC_PADDING * 2;

    // URL input box - leave space for SEND button on the right
    Rectangle urlBox = {
      mainX,
      GENERIC_PADDING,
      mainWidth - SEND_BUTTON_WIDTH - GENERIC_PADDING,
      URL_INPUT_HEIGHT
    };

    // SEND button positioned beside URL input
    Rectangle sendButton = {
      urlBox.x + urlBox.width + GENERIC_PADDING,
      GENERIC_PADDING,
      SEND_BUTTON_WIDTH,
      SEND_BUTTON_HEIGHT
    };

    // Method dropdown below URL
    Rectangle methodButton = {
      mainX,
      urlBox.y + urlBox.height + GENERIC_PADDING,
      METHOD_BUTTON_WIDTH,
      METHOD_BUTTON_HEIGHT
    };

    float tabHeight = 30;
    float contentY = methodButton.y + methodButton.height + GENERIC_PADDING + tabHeight;
    float contentHeight = screenHeight - contentY - GENERIC_PADDING;

    float panelWidth = (mainWidth - GENERIC_PADDING) / 2;

    Rectangle tabBar = {
      mainX,
      methodButton.y + methodButton.height + GENERIC_PADDING,
      panelWidth,
      tabHeight
    };

    Rectangle requestPanel = {
      mainX,
      contentY,
      panelWidth,
      contentHeight
    };

    Rectangle responsePanel = {
      mainX + panelWidth + GENERIC_PADDING,
      contentY,
      panelWidth,
      contentHeight
    };

    BeginDrawing();
      ClearBackground(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));

      // --- Sidebar Component---
      DrawRectangleRec(sidebar, Fade(GRAY, 0.1f));
      GuiGroupBox(sidebar, "HISTORY");

      Rectangle refreshBtn = { 
        sidebar.x + SIDEBAR_PADDING_GENERAL, 
        sidebar.y + SIDEBAR_PADDING_GENERAL,
        SIDEBAR_REFERSH_BUTTON_WIDTH,
        SIDEBAR_REFERSH_BUTTON_HEIGHT
      };
      if (GuiButton(refreshBtn, "Refresh"))
      {
        historyCount = PostDog_HistoryDistory_ItemsLoad(historyItems, MAX_HISTORY_ITEMS);
      }

      Rectangle historyArea = { 
        sidebar.x + SIDEBAR_PADDING_GENERAL,
        sidebar.y + SIDEBAR_AREA_PADDING_Y, 
        sidebar.width - SIDEBAR_AREA_PADDING_X, 
        sidebar.height - SIDEBAR_AREA_PADDING_Y
      };
      if (CheckCollisionPointRec(GetMousePosition(), historyArea))
      {
        float wheel = GetMouseWheelMove();
        historyScroll.y += wheel * 20;
        if (historyScroll.y < 0) historyScroll.y = 0;
      }

      BeginScissorMode(historyArea.x, historyArea.y, historyArea.width, historyArea.height);

      if (historyCount == 0)
      {
        DrawText("No requests yet", historyArea.x + 5, historyArea.y + 25, 10, DARKGRAY);
      } 
      else
      {
        int item_y_position = historyArea.y + SIDEBAR_AREA_PADDING_Y + 5 - (int)historyScroll.y;
        for (
            int current_history_item_number = 0;
            current_history_item_number < historyCount;
            current_history_item_number++
        )
        {
          if (item_y_position > historyArea.y - SIDEBAR_HISTORY_ITEM_HEIGHT && item_y_position < historyArea.y + historyArea.height)
          {
            Rectangle itemRect = { historyArea.x, item_y_position, historyArea.width, SIDEBAR_HISTORY_ITEM_HEIGHT - 2 };

            // Draw button for history item
            Color bgColor = (selectedHistoryIndex == current_history_item_number) ? Fade(BLUE, 0.3f) : Fade(LIGHTGRAY, 0.5f);
            if (CheckCollisionPointRec(GetMousePosition(), itemRect) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
            {
              // TODO: This is cringe as fuck probably should just have a strucut that we assign and then zero out lol
              char tempUrl[1024], tempMethod[16], tempHeaders[HEADER_INPUT_BUFFER_LEN], tempBody[JSON_INPUT_BUFFER_LEN];

              if (PostDog_HistoryDirectory_LoadRequest(
                    historyItems[current_history_item_number].filename, tempUrl, tempMethod, tempHeaders, tempBody) == 0)
              {
                strncpy(urlInput, tempUrl, sizeof(urlInput) - 1);
                strncpy(headersInput, tempHeaders, sizeof(headersInput) - 1);
                strncpy(jsonInput, tempBody, sizeof(jsonInput) - 1);

                // Set method
                for (int m = 0; m < 4; m++)
                {
                  if (strcmp(methods[m], tempMethod) == 0)
                  {
                    methodActive = m;
                    break;
                  }
                }

                selectedHistoryIndex = current_history_item_number;
                strcpy(responseText, "Request loaded from history. Click SEND to execute.");
              }
            }

            DrawRectangleRec(itemRect, bgColor);
            DrawRectangleLinesEx(itemRect, 1, GRAY);

            // Draw method badge
            DrawText(historyItems[current_history_item_number].method, itemRect.x + 5, item_y_position + 5, 10, BLACK);

            // Draw timestamp (date only)
            char dateStr[16] = "";
            if (strlen(historyItems[current_history_item_number].filename) >= 8)
            {
              snprintf(dateStr, sizeof(dateStr), "%.4s-%.2s-%.2s",
                       historyItems[current_history_item_number].filename, historyItems[current_history_item_number].filename + 4, historyItems[current_history_item_number].filename + 6);
            }
            DrawText(dateStr, itemRect.x + 5, item_y_position + 18, 8, DARKGRAY);

            // Draw time
            char timeStr[16] = "";
            if (strlen(historyItems[current_history_item_number].filename) >= 15) {
              snprintf(timeStr, sizeof(timeStr), "%.2s:%.2s:%.2s",
                       historyItems[current_history_item_number].filename + 9, historyItems[current_history_item_number].filename + 11, historyItems[current_history_item_number].filename + 13);
            }
            DrawText(timeStr, itemRect.x + 5, item_y_position + 28, 8, DARKGRAY);
          }

          item_y_position += SIDEBAR_HISTORY_ITEM_HEIGHT;
        }
      }

      EndScissorMode();

      // --- URL Input Component ---
      GuiLabel((Rectangle){ mainX, GENERIC_PADDING - 15, 100, 20 }, "URL:");
      if (GuiTextBox(urlBox, urlInput, 1024, urlEditMode))
      {
        urlEditMode = !urlEditMode;
      }
      if (urlEditMode && (IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C))
      {
        SetClipboardText(urlInput);
      }

      // Send button (beside URL)
      if (GuiButton(sendButton, "SEND") || (urlEditMode && IsKeyDown(KEY_ENTER)))
      {
        strcpy(responseText, "Sending request...\n");

        // Make the actual HTTP request
        char tempResponse[16384];
        const char *selectedMethod = methods[methodActive];

        // Use JSON body for POST/PUT, otherwise use empty body
        const char *requestBody = (methodActive == 1 || methodActive == 2) ? jsonInput : NULL;
        const char *requestHeaders = headersInput;

        // Save request to history
        Postdog_SaveRequestToHistory(urlInput, selectedMethod, requestHeaders, requestBody);

        // Reload history to show the new request
        historyCount = PostDog_HistoryDistory_ItemsLoad(historyItems, MAX_HISTORY_ITEMS);

        // Making the requests.
        PostDog_Make_HttpRequest(urlInput, selectedMethod, requestHeaders,
                       requestBody, tempResponse, sizeof(tempResponse));
        strncpy(responseText, tempResponse, sizeof(responseText) - 1);
        responseText[sizeof(responseText) - 1] = '\0';
      }

      // Tab toggle (3 tabs now)
      float tabWidth = tabBar.width / 3;
      Rectangle jsonTab = { tabBar.x, tabBar.y - 10, tabWidth, tabBar.height };
      Rectangle headersTab = { tabBar.x + tabWidth, tabBar.y - 10, tabWidth, tabBar.height };
      Rectangle paramsTab = { tabBar.x + tabWidth * 2, tabBar.y - 10, tabWidth, tabBar.height };

      if (GuiButton(jsonTab, activeTab ==  ActiveTab_JSON ? "#191#Body" : "Body"))
      {
        activeTab = ActiveTab_JSON;
      }

      if (GuiButton(headersTab, activeTab == ActiveTab_Headers ? "#191#Headers" : "Headers"))
      {
        activeTab = ActiveTab_Headers;
      }

      if (GuiButton(paramsTab, activeTab == ActiveTab_Params ? "#191#Params" : "Params"))
      {
        activeTab = ActiveTab_Params;
      }

      const char *panelTitle;
      switch(activeTab) {
        case  ActiveTab_JSON:
        {
          panelTitle = "Request Body (JSON)";
          break;
        }
        case  ActiveTab_Headers:
        {
          panelTitle = "Request Headers";
          break;
        } 
        case  ActiveTab_Params:
        {
          panelTitle = "Query Parameters";
          break;
        }
      }

      // Panel title
      GuiGroupBox(requestPanel, panelTitle);
      Rectangle textArea = {
        requestPanel.x + 10,
        requestPanel.y + 30,
        requestPanel.width - 20,
        requestPanel.height - 40
      };

      // Handle click outside to disable edit mode
      if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
      {
        if (!CheckCollisionPointRec(GetMousePosition(), textArea))
        {
          jsonEditMode = false;
          headersEditMode = false;
          paramsEditMode = false;
        }
      }

      // Draw border for text area
      DrawRectangleLinesEx(textArea, 1, GRAY);

      // Manual scroll handling with mouse wheel
      if (CheckCollisionPointRec(GetMousePosition(), textArea))
      {
        float wheel = GetMouseWheelMove();
        switch(activeTab)
        {
          case  ActiveTab_JSON:
          {
            jsonScroll.y += wheel * 20;
            if (jsonScroll.y < 0) jsonScroll.y = 0;
          }
          case  ActiveTab_Headers:
          {
            headersScroll.y += wheel * 20;
            if (headersScroll.y < 0) headersScroll.y = 0;
          } 
          case  ActiveTab_Params:
          {
            paramsScroll.y += wheel * 20;
            if (paramsScroll.y < 0) paramsScroll.y = 0;
          }
        }
      }

      char *copyFromInput;
      bool *currentMode; 
      switch(activeTab)
      {
        case  ActiveTab_JSON:
        {
          PostDog_Render_TextWithScroll(textArea, jsonScroll, jsonInput);
          copyFromInput = jsonInput;
          currentMode = &jsonEditMode;
          break;
        }
        case  ActiveTab_Headers:
        {
          PostDog_Render_TextWithScroll(textArea, headersScroll, headersInput);
          copyFromInput = headersInput;
          currentMode = &headersEditMode;
          break;
        } 
        case  ActiveTab_Params:
        {
          PostDog_Render_TextWithScroll(textArea, paramsScroll, paramsInput);
          copyFromInput = paramsInput;
          currentMode = &paramsEditMode;

          Rectangle updateUrlBtn = { textArea.x + 30, textArea.y + textArea.height - 10, 120, 20 };
          // TODO: Automatic update
          if (GuiButton(updateUrlBtn, "Update URL"))
          {
            char tempUrl[1024];
            strncpy(tempUrl, urlInput, sizeof(tempUrl) - 1);

            // Remove existing params if any
            char *questionMark = strchr(tempUrl, '?');
            if (questionMark) *questionMark = '\0';

            Postdog_UpdateUrlWithParams(urlInput, sizeof(urlInput), tempUrl, paramsInput);
          }
          break;
        }
      }

      if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C))
      {
        SetClipboardText(copyFromInput);
      }

      Rectangle editBtn = { textArea.x + textArea.width - 60, textArea.y + textArea.height - 10, 50, 20 };
      if (GuiButton(editBtn, "Edit"))
      {
        *currentMode = !(*currentMode);
      }

      // Response Panel with scroll
      GuiGroupBox(responsePanel, "Response");

      Rectangle responseArea = {
        responsePanel.x + 10,
        responsePanel.y + 30,
        responsePanel.width - 20,
        responsePanel.height - 40
      };

      // Manual scroll for response
      if (CheckCollisionPointRec(GetMousePosition(), responseArea))
      {
        float wheel = GetMouseWheelMove();
        responseScroll.y += wheel * 20;
        if (responseScroll.y < 0) responseScroll.y = 0;
      }

      // Draw border
      DrawRectangleLinesEx(responseArea, 1, GRAY);

      // Draw response text with scroll
      PostDog_Render_TextWithScroll(responseArea, responseScroll, responseText);

      if ((IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_LEFT_CONTROL)) && IsKeyPressed(KEY_C))
      {
        if (CheckCollisionPointRec(GetMousePosition(), responseArea)) {
          SetClipboardText(responseText);
        }
      }

      // ---  Edit modal  ----
      if (jsonEditMode)
      {
        GuiTextBox(
            (Rectangle){ screenWidth/2 - 300, screenHeight/2 - 200, 600, 400 },
            jsonInput, JSON_INPUT_BUFFER_LEN, true);
      }

      if (headersEditMode)
      {
        GuiTextBox(
            (Rectangle){ screenWidth/2 - 300, screenHeight/2 - 200, 600, 400 },
            headersInput, HEADER_INPUT_BUFFER_LEN, true);
      }

      if (paramsEditMode)
      {
        GuiTextBox(
            (Rectangle){ screenWidth/2 - 300, screenHeight/2 - 200, 600, 400 },
            paramsInput, PARAM_INPUT_BUFFER_LEN, true);
      }

      if (GuiDropdownBox(methodButton, "GET;POST;PUT;DELETE", &methodActive, methodDropdown))
      {
        methodDropdown = !methodDropdown;
      }


    EndDrawing();
  }

  CloseWindow();
  return 0;
}