Mercurial
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, |