/* * PubScript Native Linux Desktop Shell (GTK3 + WebKit2GTK 4.1) * * Spawns the embedded local PubScript Fastify + SQLite server on port 18081 * (storing user data in ~/.local/share/pubscript/pubscript.sqlite) and renders * the full PubScript studio inside a native GTK3 + WebKit2GTK 4.1 window with * persistent IndexedDB and localStorage in ~/.local/share/pubscript/webdata. */ #define _DEFAULT_SOURCE #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static pid_t g_server_pid = -1; static void stop_server(void) { if (g_server_pid > 0) { kill(g_server_pid, SIGTERM); for (int i = 0; i < 20; i++) { int status = 0; pid_t r = waitpid(g_server_pid, &status, WNOHANG); if (r == g_server_pid || r < 0) { g_server_pid = -1; return; } usleep(25000); } kill(g_server_pid, SIGKILL); waitpid(g_server_pid, NULL, 0); g_server_pid = -1; } } static void on_sigterm(int sig) { (void)sig; stop_server(); _exit(0); } static bool is_port_listening(int port) { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return false; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port); inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); bool ok = (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0); close(sock); return ok; } static void mkdir_p(const char *path) { char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s", path); size_t len = strlen(tmp); if (len == 0) return; if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; for (char *p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; } } mkdir(tmp, 0755); } static void start_embedded_server(int port, const char *data_dir, const char *app_root) { if (is_port_listening(port)) { return; } char db_path[2048]; snprintf(db_path, sizeof(db_path), "%s/pubscript.sqlite", data_dir); char web_dist[2048]; snprintf(web_dist, sizeof(web_dist), "%s/apps/web/dist", app_root); char server_entry[2048]; snprintf(server_entry, sizeof(server_entry), "%s/server/dist/server.mjs", app_root); char port_str[32]; snprintf(port_str, sizeof(port_str), "%d", port); char origin_str[128]; snprintf(origin_str, sizeof(origin_str), "http://127.0.0.1:%d", port); pid_t pid = fork(); if (pid == 0) { setenv("PORT", port_str, 1); setenv("PUBSCRIPT_DB", db_path, 1); setenv("PUBSCRIPT_WEB_DIST", web_dist, 1); setenv("PUBSCRIPT_ORIGIN", origin_str, 1); if (chdir(app_root) != 0) { /* ignore */ } int log_fd = open("/tmp/pubscript-desktop-server.log", O_CREAT | O_WRONLY | O_TRUNC, 0644); if (log_fd >= 0) { dup2(log_fd, STDOUT_FILENO); dup2(log_fd, STDERR_FILENO); close(log_fd); } execlp("node", "node", server_entry, (char *)NULL); _exit(127); } else if (pid > 0) { g_server_pid = pid; for (int i = 0; i < 60; i++) { if (is_port_listening(port)) { break; } usleep(50000); } } } static void on_window_destroy(GtkWidget *widget, gpointer data) { (void)widget; (void)data; stop_server(); gtk_main_quit(); } int main(int argc, char *argv[]) { const char *home = getenv("HOME"); if (!home || !*home) home = "/tmp"; char data_dir[1024]; snprintf(data_dir, sizeof(data_dir), "%s/.local/share/pubscript", home); mkdir_p(data_dir); char webkit_data_dir[2048]; snprintf(webkit_data_dir, sizeof(webkit_data_dir), "%s/webdata", data_dir); mkdir_p(webkit_data_dir); char webkit_cache_dir[2048]; snprintf(webkit_cache_dir, sizeof(webkit_cache_dir), "%s/webcache", data_dir); mkdir_p(webkit_cache_dir); const char *app_root = getenv("PUBSCRIPT_APP_ROOT"); if (!app_root || !*app_root) { app_root = "/opt/pubscript"; } int port = 18081; const char *env_port = getenv("PUBSCRIPT_DESKTOP_PORT"); if (env_port && *env_port) { int p = atoi(env_port); if (p > 0 && p < 65536) port = p; } signal(SIGINT, on_sigterm); signal(SIGTERM, on_sigterm); atexit(stop_server); start_embedded_server(port, data_dir, app_root); char url[256]; snprintf(url, sizeof(url), "http://127.0.0.1:%d/", port); for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--version") == 0) { printf("PubScript Desktop 0.1.0\n"); stop_server(); return 0; } if (strcmp(argv[i], "--headless-check") == 0) { bool up = is_port_listening(port); printf("{\"ok\":%s,\"url\":\"%s\",\"dataDir\":\"%s\"}\n", up ? "true" : "false", url, data_dir); stop_server(); return up ? 0 : 1; } } gtk_init(&argc, &argv); void *wk = dlopen("libwebkit2gtk-4.1.so.0", RTLD_LAZY | RTLD_GLOBAL); if (!wk) { wk = dlopen("libwebkit2gtk-4.0.so.37", RTLD_LAZY | RTLD_GLOBAL); } if (!wk) { fprintf(stderr, "WebKit2GTK not found (%s); falling back to xdg-open %s\n", dlerror(), url); char cmd[512]; snprintf(cmd, sizeof(cmd), "xdg-open '%s' >/dev/null 2>&1 &", url); int r = system(cmd); (void)r; return 0; } typedef void *(*fn_data_mgr_new)(const char *, const char *, const char *, const char *, void *); typedef void *(*fn_ctx_new_with_mgr)(void *); typedef GtkWidget *(*fn_webview_new_with_ctx)(void *); typedef GtkWidget *(*fn_webview_new)(void); typedef void *(*fn_webview_get_settings)(GtkWidget *); typedef void (*fn_webview_load_uri)(GtkWidget *, const char *); fn_data_mgr_new p_data_mgr_new = (fn_data_mgr_new)dlsym(wk, "webkit_website_data_manager_new"); fn_ctx_new_with_mgr p_ctx_new_with_mgr = (fn_ctx_new_with_mgr)dlsym(wk, "webkit_web_context_new_with_website_data_manager"); fn_webview_new_with_ctx p_webview_new_with_ctx = (fn_webview_new_with_ctx)dlsym(wk, "webkit_web_view_new_with_context"); fn_webview_new p_webview_new = (fn_webview_new)dlsym(wk, "webkit_web_view_new"); fn_webview_get_settings p_get_settings = (fn_webview_get_settings)dlsym(wk, "webkit_web_view_get_settings"); fn_webview_load_uri p_load_uri = (fn_webview_load_uri)dlsym(wk, "webkit_web_view_load_uri"); GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "PubScript — Writing Studio"); gtk_window_set_default_size(GTK_WINDOW(window), 1440, 900); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); const char *icon_path = "/usr/share/icons/hicolor/scalable/apps/pubscript.svg"; if (access(icon_path, R_OK) == 0) { gtk_window_set_icon_from_file(GTK_WINDOW(window), icon_path, NULL); } GtkWidget *webview = NULL; if (p_data_mgr_new && p_ctx_new_with_mgr && p_webview_new_with_ctx) { void *mgr = p_data_mgr_new( "base-data-directory", webkit_data_dir, "base-cache-directory", webkit_cache_dir, NULL ); void *ctx = p_ctx_new_with_mgr(mgr); webview = p_webview_new_with_ctx(ctx); } else if (p_webview_new) { webview = p_webview_new(); } if (!webview || !p_load_uri) { fprintf(stderr, "Failed to initialize WebKitWebView\n"); stop_server(); return 1; } if (p_get_settings) { void *settings = p_get_settings(webview); if (settings) { g_object_set( G_OBJECT(settings), "enable-developer-extras", TRUE, "enable-smooth-scrolling", TRUE, "javascript-can-access-clipboard", TRUE, "enable-webgl", TRUE, NULL ); } } gtk_container_add(GTK_CONTAINER(window), webview); g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL); p_load_uri(webview, url); gtk_widget_show_all(window); gtk_main(); stop_server(); return 0; }