comparison postdog/main.c @ 152:7387eec8e7f8

[Postdog] Updated to use Seobeo_Client instead of CURL. Updated to handle websocket connection.
author June Park <parkjune1995@gmail.com>
date Sun, 11 Jan 2026 07:47:05 -0800
parents 249881ceff7b
children 790930d9bb90
comparison
equal deleted inserted replaced
150:c37490913530 152:7387eec8e7f8
1 #include <stdio.h> 1 #include <stdio.h>
2 #include <stdlib.h> 2 #include <stdlib.h>
3 #include <string.h> 3 #include <string.h>
4 #include <time.h> 4 #include <time.h>
5 #include <sys/stat.h> 5 #include <sys/stat.h>
6 #include "dowa/dowa.h" 6 #include <pthread.h>
7
8 #include <curl/curl.h>
9 #include "third_party/raylib/include/raylib.h"
10 #define RAYGUI_IMPLEMENTATION
11 #include "third_party/raylib/include/raygui.h"
12
13 #ifndef POSTDOG_PATHS
14 #define POSTDOG_PATHS "/Users/mrjunejune/zenbu/postdog/history"
15 #endif
16
17 #define SCREEN_WIDTH 1280
18 #define SCREEN_HEIGHT 780
19 #define MAX_SCROLL_HEIGHT 10000
20
21 #define HEADER_BUFFER_LENGTH 1024 * 4
22 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4
23 #define URL_TEXT_BUFFER_LENGTH 1024 * 10
24 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5
25 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5
26
27 7
28 #ifdef _WIN32 8 #ifdef _WIN32
29 #include <direct.h> 9 #include <direct.h>
30 #include <io.h> 10 #include <io.h>
31 #define mkdir(path, mode) _mkdir(path) 11 #define mkdir(path, mode) _mkdir(path)
35 #include <sys/stat.h> 15 #include <sys/stat.h>
36 #include <dirent.h> 16 #include <dirent.h>
37 #include <unistd.h> 17 #include <unistd.h>
38 #endif 18 #endif
39 19
20
21 #include "third_party/raylib/include/raylib.h"
22 #define RAYGUI_IMPLEMENTATION
23 #include "third_party/raylib/include/raygui.h"
24
25 #include "dowa/dowa.h"
26 #include "seobeo/seobeo.h"
27
28 #ifndef POSTDOG_PATHS
29 #define POSTDOG_PATHS "/Users/mrjunejune/zenbu/postdog/history"
30 #endif
31
32 #define SCREEN_WIDTH 1280
33 #define SCREEN_HEIGHT 780
34 #define MAX_SCROLL_HEIGHT 10000
35
36 #define HEADER_BUFFER_LENGTH 1024 * 4
37 #define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4
38 #define URL_TEXT_BUFFER_LENGTH 1024 * 10
39 #define BODY_BUFFER_LENGTH 1024 * 1024 * 5
40 #define RESULT_BUFFER_LENGTH 1024 * 1024 * 5
41
40 typedef Dowa_KV(char*, char*) INPUT_HASHMAP; 42 typedef Dowa_KV(char*, char*) INPUT_HASHMAP;
41 43
42 typedef struct { 44 typedef struct {
43 char *data; 45 char *data;
44 size_t size; 46 size_t size;
60 62
61 typedef enum { 63 typedef enum {
62 TAB_HEADER = 0, 64 TAB_HEADER = 0,
63 TAB_BODY, 65 TAB_BODY,
64 TAB_GET_PARAMS, 66 TAB_GET_PARAMS,
65 TAB_BAR, 67 TAB_WEBSOCKET,
66 TAB_LENGTH 68 TAB_LENGTH
67 } PostDog_Tab_Enum; 69 } PostDog_Tab_Enum;
68 70
69 static uint32 counter = 0; 71 static uint32 counter = 0;
70 HistoryItem *history_items = NULL; 72 HistoryItem *history_items = NULL;
73 // Global UI state 75 // Global UI state
74 char *url_input_text = NULL; 76 char *url_input_text = NULL;
75 char *url_result_text = NULL; 77 char *url_result_text = NULL;
76 char **url_body_map = NULL; 78 char **url_body_map = NULL;
77 int active_method_dropdown = 0; 79 int active_method_dropdown = 0;
80 int active_input_tab = 0;
81 Seobeo_WebSocket *ws = NULL;
78 82
79 int CompareHistoryItemsByDate(const void *a, const void *b) { 83 int CompareHistoryItemsByDate(const void *a, const void *b) {
80 HistoryItem *itemA = (HistoryItem *)a; 84 HistoryItem *itemA = (HistoryItem *)a;
81 HistoryItem *itemB = (HistoryItem *)b; 85 HistoryItem *itemB = (HistoryItem *)b;
82 return (itemB->time_modified - itemA->time_modified); 86 return (itemB->time_modified - itemA->time_modified);
244 url_input_text, 248 url_input_text,
245 method, 249 method,
246 url_body_map[TAB_HEADER], 250 url_body_map[TAB_HEADER],
247 url_body_map[TAB_BODY], 251 url_body_map[TAB_BODY],
248 url_body_map[TAB_GET_PARAMS], 252 url_body_map[TAB_GET_PARAMS],
249 url_body_map[TAB_BAR], 253 url_body_map[TAB_WEBSOCKET],
250 url_result_text 254 url_result_text
251 ); 255 );
252 char *filename = Dowa_Arena_Allocate(arena, 1024); 256 char *filename = Dowa_Arena_Allocate(arena, 1024);
253 if (!filename) 257 if (!filename)
254 { 258 {
274 Dowa_Array_Push(new_history_items, item); 278 Dowa_Array_Push(new_history_items, item);
275 279
276 Dowa_Arena_Free(arena); 280 Dowa_Arena_Free(arena);
277 } 281 }
278 282
279 static size_t Postdog_Curl_Callback(void *contents, size_t size, size_t nmemb, void *userp) 283 int PostDog_Websocket_Send(void)
280 { 284 {
281 size_t real_size = size * nmemb; 285 if (!ws)
282 ResponseBuffer *buf = (ResponseBuffer *)userp; 286 ws = Seobeo_WebSocket_Connect(url_input_text);
283 287
284 char *ptr = realloc(buf->data, buf->size + real_size + 1); 288 printf("URL %s\n", url_input_text);
285 if (ptr == NULL) 289 if (Seobeo_WebSocket_Send_Text(ws, url_body_map[active_input_tab]) < 0)
286 { 290 printf("Failed to send message\n");
287 printf("Not enough memory for response\n"); 291
288 return 0; 292 printf("Receiving responses...\n");
289 } 293
290 294 int received = 0;
291 buf->data = ptr; 295 while (TRUE)
292 memcpy(&(buf->data[buf->size]), contents, real_size); 296 {
293 buf->size += real_size; 297 Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(ws);
294 buf->data[buf->size] = 0; 298 if (p_msg)
295 299 {
296 return real_size; 300 if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT)
301 {
302 printf("Response %d: %.*s\n", received + 1, (int)p_msg->length, (char*)p_msg->data);
303 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "%s", p_msg->data);
304 received++;
305 }
306 Seobeo_WebSocket_Message_Destroy(p_msg);
307 }
308 usleep(10000);
309 }
310 printf("Received %d/%d messages\n", received, 3);
311 }
312
313 void* PostDog_Websocket_Thread(void* arg)
314 {
315 PostDog_Websocket_Send();
316 printf("Websocket request finished.\n");
317 return NULL;
318 }
319
320 void Trigger_Async_Send()
321 {
322 pthread_t thread_id;
323
324 if (pthread_create(&thread_id, NULL, PostDog_Websocket_Thread, NULL) != 0)
325 {
326 perror("Failed to create thread");
327 return;
328 }
329 pthread_detach(thread_id);
297 } 330 }
298 331
299 int PostDog_Http_Request(void) 332 int PostDog_Http_Request(void)
300 { 333 {
301 const char *method = PostDog_Enum_To_String(active_method_dropdown); 334 Seobeo_Client_Request *req = Seobeo_Client_Request_Create(url_input_text);
302 CURL *curl; 335 Seobeo_Client_Response *res;
303 CURLcode res; 336 switch (active_method_dropdown)
304 ResponseBuffer buffer = { .data = malloc(1), .size = 0 }; 337 {
305 338 case 0:
306 url_result_text[0] = '\n'; 339 {
307 340 Seobeo_Client_Request_Set_Method(req, "GET");
308 if (buffer.data == NULL) 341 break;
309 { 342 }
310 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: Failed to allocate memory"); 343 case 1:
311 return -1; 344 {
312 } 345 Seobeo_Client_Request_Set_Method(req, "POST");
313 buffer.data[0] = '\0'; 346 break;
314 347 }
315 curl_global_init(CURL_GLOBAL_DEFAULT); 348 case 2:
316 curl = curl_easy_init(); 349 {
317 350 Seobeo_Client_Request_Set_Method(req, "PUT");
318 if (curl) 351 break;
319 { 352 }
320 struct curl_slist *headerList = NULL; 353 case 3:
321 354 {
322 // Set URL 355 Seobeo_Client_Request_Set_Method(req, "DELETE");
323 curl_easy_setopt(curl, CURLOPT_URL, url_input_text); 356 break;
324 357 }
325 // Set HTTP method 358 }
326 if (strcmp(method, "POST") == 0) 359
327 { 360 {
328 curl_easy_setopt(curl, CURLOPT_POST, 1L);
329 if (url_body_map[TAB_BODY] && strlen(url_body_map[TAB_BODY]) > 0)
330 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, url_body_map[TAB_BODY]);
331 }
332 else if (strcmp(method, "PUT") == 0)
333 {
334 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
335 if (url_body_map[TAB_BODY] && strlen(url_body_map[TAB_BODY]) > 0)
336 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, url_body_map[TAB_BODY]);
337 }
338 else if (strcmp(method, "DELETE") == 0)
339 {
340 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
341 }
342 // Default is GET
343
344 // Parse and add headers
345 if (url_body_map[TAB_HEADER] && strlen(url_body_map[TAB_HEADER]) > 0) 361 if (url_body_map[TAB_HEADER] && strlen(url_body_map[TAB_HEADER]) > 0)
346 { 362 {
347 char *headersCopy = strdup(url_body_map[TAB_HEADER]); 363 char *headersCopy = strdup(url_body_map[TAB_HEADER]);
348 char *line = strtok(headersCopy, "\n"); 364 char *line = strtok(headersCopy, "\n");
349 while (line != NULL) { 365 while (line != NULL)
366 {
350 while (*line == ' ' || *line == '\t') line++; 367 while (*line == ' ' || *line == '\t') line++;
351 if (strlen(line) > 0) 368 if (strlen(line) > 0)
352 headerList = curl_slist_append(headerList, line); 369 Seobeo_Client_Request_Add_Header_Array(req, line);
353 line = strtok(NULL, "\n"); 370 line = strtok(NULL, "\n");
354 } 371 }
355 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); 372
356 free(headersCopy); 373 }
357 } 374 Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number;
358 375 res = Seobeo_Client_Request_Execute(req);
359 // Set write callback 376
360 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Postdog_Curl_Callback); 377 if (res == NULL)
361 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer); 378 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: Failed to send the request\n");
362
363 // Follow redirects
364 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
365 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
366 res = curl_easy_perform(curl);
367
368 if (res != CURLE_OK)
369 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: %s\n", curl_easy_strerror(res));
370 else 379 else
371 { 380 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "HTTP Status: %d\n\n%s",
372 long response_code; 381 res->status_code, res->body ? res->body : "");
373 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); 382 }
374 383 Seobeo_Client_Request_Destroy(req);
375 if (buffer.size > RESULT_BUFFER_LENGTH) 384 Seobeo_Client_Response_Destroy(res);
376 printf("TODO: Realloc\n");
377
378 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "HTTP Status: %ld\n\n%s",
379 response_code, buffer.data ? buffer.data : "");
380 }
381
382 if (headerList) curl_slist_free_all(headerList);
383 curl_easy_cleanup(curl);
384 }
385 else
386 snprintf(url_result_text, RESULT_BUFFER_LENGTH, "Error: Failed to initialize curl");
387
388 free(buffer.data);
389 curl_global_cleanup();
390
391 PostDog_Request_SaveFile(); 385 PostDog_Request_SaveFile();
392 return 0; 386 return 0;
393 } 387 }
394 388
395 void PostDog_Update_URL(void) 389 void PostDog_Update_URL(void)
490 break; 484 break;
491 485
492 case 3: // Headers (TAB_HEADER) 486 case 3: // Headers (TAB_HEADER)
493 case 4: // Body (TAB_BODY) 487 case 4: // Body (TAB_BODY)
494 case 5: // Get Params (TAB_GET_PARAMS) 488 case 5: // Get Params (TAB_GET_PARAMS)
495 case 6: // Bar (TAB_BAR) 489 case 6: // Bar (TAB_WEBSOCKET)
496 { 490 {
497 int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3 491 int map_index = i - 3; // 3->0, 4->1, 5->2, 6->3
498 snprintf(url_body_map[map_index], strlen(values[i]) + 1, "%s", values[i]); 492 snprintf(url_body_map[map_index], strlen(values[i]) + 1, "%s", values[i]);
499 // Trim trailing newlines 493 // Trim trailing newlines
500 for (int j = strlen(values[i]); j > 0; j--) 494 for (int j = strlen(values[i]); j > 0; j--)
631 Rectangle method_dropdown = { 0 }; 625 Rectangle method_dropdown = { 0 };
632 626
633 627
634 // Initialize global UI state 628 // Initialize global UI state
635 url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH); 629 url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH);
636 snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, "https://httpbin.org/get"); 630 snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, "wss://mrjunejune.com/echo");
637 631
638 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH)); 632 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH));
639 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH)); 633 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH));
640 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); 634 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));
641 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH)); 635 Dowa_Array_Push(url_body_map, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));
660 Rectangle result_area = { 0 }; 654 Rectangle result_area = { 0 };
661 Rectangle result_body = { 0 }; 655 Rectangle result_body = { 0 };
662 656
663 // General styling. 657 // General styling.
664 float padding = 10; // TODO make it % based? 658 float padding = 10; // TODO make it % based?
665 int active_input_tab = 0;
666 int prev_input_tab = 0; 659 int prev_input_tab = 0;
667 660
668 // Scroll offsets 661 // Scroll offsets
669 float history_scroll_offset = 0; 662 float history_scroll_offset = 0;
670 float input_body_scroll_offset = 0; 663 float input_body_scroll_offset = 0;
783 776
784 Vector2 mouse_position = GetMousePosition(); 777 Vector2 mouse_position = GetMousePosition();
785 float mouse_wheel = GetMouseWheelMove(); 778 float mouse_wheel = GetMouseWheelMove();
786 779
787 // Reset input body scroll when tab changes 780 // Reset input body scroll when tab changes
788 if (prev_input_tab != active_input_tab) { 781 if (prev_input_tab != active_input_tab)
782 {
789 input_body_scroll_offset = 0; 783 input_body_scroll_offset = 0;
790 prev_input_tab = active_input_tab; 784 prev_input_tab = active_input_tab;
791 } 785 }
792 786
793 // Handle scroll wheel for history 787 // Handle scroll wheel for history
903 897
904 BeginScissorMode(input_body.x, input_body.y, input_body.width, input_body.height); 898 BeginScissorMode(input_body.x, input_body.y, input_body.width, input_body.height);
905 Rectangle scrolled_input = AddPadding(input_body, padding * 2); 899 Rectangle scrolled_input = AddPadding(input_body, padding * 2);
906 scrolled_input.y += input_body_scroll_offset; 900 scrolled_input.y += input_body_scroll_offset;
907 scrolled_input.height = MAX_SCROLL_HEIGHT; 901 scrolled_input.height = MAX_SCROLL_HEIGHT;
908 if (JUNE_GuiTextBox(scrolled_input, url_body_map[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool)) 902 if (active_input_tab != TAB_WEBSOCKET)
909 input_body_bool = !input_body_bool; 903 {
904 if (JUNE_GuiTextBox(scrolled_input, url_body_map[active_input_tab], DEFAULT_TEXT_BUFFER_LENGTH, input_body_bool))
905 input_body_bool = !input_body_bool;
906 }
907 else
908 {
909 boolean temp = true;
910 if (GuiTextInputBox(input_body, "send message", "connected",
911 "send", url_body_map[active_input_tab], BODY_BUFFER_LENGTH, &temp) == 1)
912 Trigger_Async_Send();
913 }
910 EndScissorMode(); 914 EndScissorMode();
911 915
912 if (input_body_scroll_offset < 0) { 916 if (input_body_scroll_offset < 0) {
913 float scrollbar_height = 10; 917 float scrollbar_height = 10;
914 float scrollbar_y = input_body.y - (input_body_scroll_offset / MAX_SCROLL_HEIGHT) * (input_body.height - scrollbar_height); 918 float scrollbar_y = input_body.y - (input_body_scroll_offset / MAX_SCROLL_HEIGHT) * (input_body.height - scrollbar_height);
935 scrolled_result.y += result_body_scroll_offset; 939 scrolled_result.y += result_body_scroll_offset;
936 scrolled_result.height = MAX_SCROLL_HEIGHT; 940 scrolled_result.height = MAX_SCROLL_HEIGHT;
937 GuiTextBoxMulti(scrolled_result, url_result_text, RESULT_BUFFER_LENGTH, false); 941 GuiTextBoxMulti(scrolled_result, url_result_text, RESULT_BUFFER_LENGTH, false);
938 EndScissorMode(); 942 EndScissorMode();
939 943
940 if (result_body_scroll_offset < 0) { 944 if (result_body_scroll_offset < 0)
945 {
941 float scrollbar_height = 10; 946 float scrollbar_height = 10;
942 float scrollbar_y = result_body.y - (result_body_scroll_offset / MAX_SCROLL_HEIGHT) * (result_body.height - scrollbar_height); 947 float scrollbar_y = result_body.y - (result_body_scroll_offset / MAX_SCROLL_HEIGHT) * (result_body.height - scrollbar_height);
943 Rectangle scrollbar = { 948 Rectangle scrollbar = {
944 result_body.x + result_body.width - 5, 949 result_body.x + result_body.width - 5,
945 scrollbar_y, 950 scrollbar_y,