# HG changeset patch # User June Park # Date 1767464445 28800 # Node ID 2301aeb7503ba98f8fb48f2200bf6c7683f15519 # Parent f6d2f2eaaf8401f6157423cff2409aa0d53e3301 [Hg Web] Super simple mercurial server. diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/BUILD --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/BUILD Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,36 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle") + +move_files_into_dir( + name = "compiled_ts", + srcs = [ + "//markdown_converter:markdown_to_html", + ], + dest = "src", +) + +filegroup( + name = "src_files", + srcs = glob(["src/**"]) + [":compiled_ts"], +) + +cc_binary( + name = "hg_web_server", + srcs = ["main.c"], + deps = ["//seobeo:seobeo_server"], + data = [":src_files"], + defines = ["REPO_ROOT=\\\"\"/home/mrjunejune/zenbu\"\\\""], +) + +bundle( + name = "hg_web_server_bundle", + binary = ":hg_web_server", +) + +cc_binary( + name = "hg_web_server_dev", + srcs = ["main.c"], + deps = ["//seobeo:seobeo_server_dev"], + data = [":src_files"], + defines = ["REPO_ROOT=\\\"\"/Users/mrjunejune/zenbu\"\\\""], +) diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/deploy.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/deploy.sh Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,13 @@ +#!/bin/bash +bazel build -c opt //hg-web:hg_web_server_bundle + +# Create +sudo cp -a bazel-bin/hg-web/hg_web_server_bundle /opt/hg_web_server_bundle_new +sudo chown -R hg_web_server:zenbu_team /opt/hg_web_server_bundle_new + +# Swap +sudo rm -rf /opt/hg_web_server_bundle_active +sudo mv /opt/hg_web_server_bundle_new /opt/hg_web_server_bundle_active + +sudo systemctl restart hg_web_server.service +echo "Deployment complete!" diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/main.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/main.c Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,279 @@ +#include "seobeo/seobeo.h" +#include "dowa/dowa.h" +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PATH 4096 + +// TODO: Move this to seobeo.... +// Asked AI to create this lol, probably should learn to decode it myself.. +static void url_decode(char *dst, const char *src) +{ + char a, b; + while (*src) { + if ((*src == '%') && + ((a = src[1]) && (b = src[2])) && + (isxdigit(a) && isxdigit(b))) { + if (a >= 'a') a -= 'a'-'A'; + if (a >= 'A') a -= ('A' - 10); + else a -= '0'; + if (b >= 'a') b -= 'a'-'A'; + if (b >= 'A') b -= ('A' - 10); + else b -= '0'; + *dst++ = 16*a+b; + src+=3; + } else if (*src == '+') { + *dst++ = ' '; + src++; + } else { + *dst++ = *src++; + } + } + *dst = '\0'; +} + +static int is_directory(const char *path) +{ + struct stat st; + if (stat(path, &st) != 0) return 0; + return S_ISDIR(st.st_mode); +} + +static int file_exists(const char *path) +{ + struct stat st; + return stat(path, &st) == 0; +} + +static char* sanitize_path(const char *input_path, Dowa_Arena *arena) +{ + if (!input_path || strlen(input_path) == 0) + { + char *empty = Dowa_Arena_Allocate(arena, 1); + empty[0] = '\0'; + return empty; + } + + size_t len = strlen(input_path); + char *result = Dowa_Arena_Allocate(arena, len + 1); + size_t j = 0; + + for (size_t i = 0; i < len; i++) + { + if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) { + if (i + 1 < len && input_path[i+1] == '.') { + // Skip ".." + i++; + continue; + } + // Skip "." + continue; + } + result[j++] = input_path[i]; + } + result[j] = '\0'; + + // Remove leading/trailing slashes + while (result[0] == '/') + memmove(result, result + 1, strlen(result)); + while (j > 0 && result[j-1] == '/') + result[--j] = '\0'; + + return result; +} + +Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); + const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; + + char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); + url_decode(decoded_path, rel_path); + + char *safe_path = sanitize_path(decoded_path, arena); + char full_path[MAX_PATH]; + if (strlen(safe_path) > 0) + snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); + else + snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT); + + if (!is_directory(full_path)) + { + char *error_json = Dowa_Arena_Allocate(arena, 256); + snprintf(error_json, 256, "{\"error\":\"Directory not found\"}"); + + Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); + return resp; + } + + DIR *dir = opendir(full_path); + if (!dir) + { + char *error_json = Dowa_Arena_Allocate(arena, 256); + snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}"); + + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); + return resp; + } + + char *json = Dowa_Arena_Allocate(arena, 1024 * 100); + strcpy(json, "{\"files\":["); + + struct dirent *entry; + int first = 1; + + while ((entry = readdir(dir)) != NULL) + { + if (entry->d_name[0] == '.') continue; + + char entry_path[MAX_PATH]; + snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name); + + int is_dir = is_directory(entry_path); + + char entry_rel_path[MAX_PATH]; + if (strlen(safe_path) > 0) + snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name); + else + snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name); + + if (!first) strcat(json, ","); + first = 0; + + char entry_json[MAX_PATH * 2]; + snprintf(entry_json, sizeof(entry_json), + "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}", + entry->d_name, + is_dir ? "directory" : "file", + entry_rel_path); + strcat(json, entry_json); + } + + closedir(dir); + strcat(json, "]}"); + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); + Dowa_HashMap_Push_Arena(resp, "body", json, arena); + + return resp; +} + +Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + + void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path"); + const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : ""; + char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1); + url_decode(decoded_path, rel_path); + char *safe_path = sanitize_path(decoded_path, arena); + + Seobeo_Log(SEOBEO_DEBUG, "rel_path: %s\n", rel_path); + Seobeo_Log(SEOBEO_DEBUG, "decoded_path: %s\n", decoded_path); + Seobeo_Log(SEOBEO_DEBUG, "safe path: %s\n", safe_path); + + + if (strlen(safe_path) == 0) + { + char *error = Dowa_Arena_Allocate(arena, 64); + strcpy(error, "File path required"); + + Dowa_HashMap_Push_Arena(resp, "status", "400", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", error, arena); + return resp; + } + + char full_path[MAX_PATH]; + snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); + FILE *file = fopen(full_path, "rb"); + if (!file) + { + char *error_msg = "File not found."; + Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); + return resp; + } + + fseek(file, 0, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + char *file_data = malloc(file_size + 1); + if (!file_data) + { + fclose(file); + char *error_msg = "Memory allocation failed"; + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); + return resp; + } + + fread(file_data, 1, file_size, file); + file_data[file_size] = '\0'; + fclose(file); + + char *body = Dowa_Arena_Allocate(arena, file_size + 1); + memcpy(body, file_data, file_size); + body[file_size] = '\0'; + free(file_data); + + if (!body) + { + char *error = Dowa_Arena_Allocate(arena, 64); + strcpy(error, "Cannot read file"); + + Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", error, arena); + return resp; + } + + const char *content_type = "text/plain"; + if (strstr(safe_path, ".md")) content_type = "text/markdown"; + else if (strstr(safe_path, ".html")) content_type = "text/html"; + else if (strstr(safe_path, ".css")) content_type = "text/css"; + else if (strstr(safe_path, ".js")) content_type = "application/javascript"; + else if (strstr(safe_path, ".json")) content_type = "application/json"; + + Dowa_HashMap_Push_Arena(resp, "status", "200", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); + Dowa_HashMap_Push_Arena(resp, "body", body, arena); + + return resp; +} + +Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) { + return ApiGetFile(req, arena); +} + +int main(void) { + Seobeo_Router_Init(); + + Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory); + Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile); + Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme); + + printf("Starting on Port 6970...\n"); + printf("Repository: %s\n", REPO_ROOT); + + int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4); + + Seobeo_Router_Destroy(); + + return result; +} diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/src/base.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/base.css Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,141 @@ +/* --- Colors ---*/ +:root { + --bg: #ffffff; + --fg: #1a1a1a; + --border: #e0e0e0; + --hover: #f5f5f5; + --accent: #0066cc; + --accent-hover: #0052a3; + --secondary: #6c757d; + --success: #28a745; + --warning: #ffc107; + --danger: #dc3545; + --code-bg: #f6f8fa; + --link: #0066cc; + --link-hover: #0052a3; +} + +.dark { + --bg: #0d1117; + --fg: #c9d1d9; + --border: #30363d; + --hover: #161b22; + --accent: #58a6ff; + --accent-hover: #79c0ff; + --secondary: #8b949e; + --success: #3fb950; + --warning: #d29922; + --danger: #f85149; + --code-bg: #161b22; + --link: #58a6ff; + --link-hover: #79c0ff; +} + +@media (prefers-color-scheme: dark) { + :root:not(.light-mode) { + --bg: #0d1117; + --fg: #c9d1d9; + --border: #30363d; + --hover: #161b22; + --accent: #58a6ff; + --accent-hover: #79c0ff; + --secondary: #8b949e; + --success: #3fb950; + --warning: #d29922; + --danger: #f85149; + --code-bg: #161b22; + --link: #58a6ff; + --link-hover: #79c0ff; + } +} + +/* --- Reset and Base Styles --- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + background: var(--bg); + color: var(--fg); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + line-height: 1.6; + background: var(--bg); + color: var(--fg); + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + color: var(--link-hover); + text-decoration: underline; +} + +h1, h2, h3, h4, h5, h6 { + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.25; +} + +h1 { font-size: 2rem; } +h2 { font-size: 1.75rem; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } +h5 { font-size: 1.1rem; } +h6 { font-size: 1rem; } + +p { + margin-bottom: 1rem; +} + +code { + background: var(--code-bg); + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Monaco', 'Courier New', monospace; + font-size: 0.9em; +} + +pre { + background: var(--code-bg); + padding: 1rem; + border-radius: 6px; + overflow-x: auto; + margin-bottom: 1rem; +} + +pre code { + background: none; + padding: 0; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + body { + font-size: 14px; + } + + main { + padding: 1rem; + } + + h1 { font-size: 1.75rem; } + h2 { font-size: 1.5rem; } + h3 { font-size: 1.25rem; } +} diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/src/index.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/index.css Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,179 @@ +.header { + border-bottom: 1px solid var(--border); + padding-bottom: 1rem; + margin-bottom: 2rem; +} + +.header h1 { + margin-bottom: 0.5rem; +} + +.header .description { + color: var(--secondary); + font-size: 0.95rem; +} + +.clone-info { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1rem; + margin-bottom: 2rem; +} + +.clone-info code { + background: none; + color: var(--fg); + font-size: 0.95rem; +} + +.breadcrumb { + margin-bottom: 1.5rem; + font-size: 0.95rem; +} + +.breadcrumb a { + color: var(--link); +} + +.breadcrumb a:hover { + text-decoration: underline; +} + +.breadcrumb span { + color: var(--secondary); + margin: 0 0.5rem; +} + +.file-list { + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; +} + +.file-item { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + transition: background-color 0.2s; +} + +.file-item:last-child { + border-bottom: none; +} + +.file-item:hover { + background: var(--hover); +} + +.file-item .icon { + margin-right: 0.75rem; + font-size: 1.2rem; + width: 20px; + text-align: center; +} + +.file-item .name { + flex: 1; + font-family: 'Monaco', 'Courier New', monospace; + font-size: 0.9rem; +} + +.file-item .name a { + color: var(--fg); +} + +.file-item .name a:hover { + color: var(--link); +} + +.file-item.directory .icon { + color: var(--accent); +} + +.file-item.file .icon { + color: var(--secondary); +} + +.readme-section { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border); +} + +.readme-section h2 { + margin-bottom: 1rem; + font-size: 1.5rem; +} + +.readme-content { + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.5rem; + background: var(--code-bg); +} + +.readme-content h1 { font-size: 1.75rem; margin-top: 1.5rem; } +.readme-content h2 { font-size: 1.5rem; margin-top: 1.25rem; } +.readme-content h3 { font-size: 1.25rem; margin-top: 1rem; } + +.readme-content h1:first-child, +.readme-content h2:first-child, +.readme-content h3:first-child { + margin-top: 0; +} + +.readme-content ul, +.readme-content ol { + margin-left: 2rem; + margin-bottom: 1rem; +} + +.readme-content li { + margin-bottom: 0.5rem; +} + +.readme-content img { + max-width: 100%; + height: auto; + border-radius: 6px; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--secondary); +} + +.error-message { + background: var(--danger); + color: white; + padding: 1rem; + border-radius: 6px; + margin-bottom: 1rem; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + main { + padding: 1rem; + } + + .file-item { + padding: 0.5rem 0.75rem; + } + + .file-item .name { + font-size: 0.85rem; + } + + .clone-info { + padding: 0.75rem; + overflow-x: auto; + } + + .readme-content { + padding: 1rem; + } +} diff -r f6d2f2eaaf84 -r 2301aeb7503b hg-web/src/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/index.html Sat Jan 03 10:20:45 2026 -0800 @@ -0,0 +1,175 @@ + + + + + + Zenbu Repository + + + + +
+
+

Zenbu Repository

+

Browse and clone this mercurial repository

+
+ +
+ Clone this repository:
+ hg clone http://zenbu.babocoder.com +
+ + + +
+ + + + +
+ + + + +