/* Copyright (c) 2026 Arslaan Pathan This software is licensed under the ARPL. See LICENSE for details. */ #include #include #include #include #include /* forward declare BEFORE including config.h so that config.h can reference them */ typedef struct { GtkWidget *window; GtkWidget *notebook; GtkWidget *cmdbar; GtkWidget *indicator; WebKitWebContext *web_context; } Cinnamon; typedef struct { const char* key; void (*fptr)(Cinnamon*, void* arg); void* arg; } Keybind; typedef struct { const char* name; void (*fptr)(Cinnamon*, const char *args); } Command; #include "config.h" /* 0 = normal mode, 1 = insert, 2 = passthrough, 3 = hint */ static int mode = 0; bool cmdbar_focused = false; static char hint_buffer[16] = {0}; static int hint_mode_active = 0; void (*hint_callback)(Cinnamon* cinnamon, char hint_buffer[16]); /* some forward declarations */ void cancel_hint_mode(Cinnamon* cinnamon); static void parse_keybind(const char *key, guint *keyval, GdkModifierType *mods) { char name[32]; const char *start; const char *end; *mods = 0; if (key[0] != '<') { *keyval = gdk_keyval_from_name(key); return; } if (strstr(key, "S-")) *mods |= GDK_SHIFT_MASK; if (strstr(key, "C-")) *mods |= GDK_CONTROL_MASK; if (strstr(key, "A-")) *mods |= GDK_MOD1_MASK; /* everything after the last '-' and before '>' */ /* this means we can't have things like for example, but too bad so sad. cannot be bothered to implement that at the moment */ start = strrchr(key, '-') + 1; end = strchr(key, '>'); snprintf(name, sizeof(name), "%.*s", (int)(end - start), start); *keyval = gdk_keyval_from_name(name); } static void on_title_changed(WebKitWebView* webview, GParamSpec* pspec, GtkNotebook* notebook) { const char* title = webkit_web_view_get_title(webview); int page = gtk_notebook_page_num(notebook, GTK_WIDGET(webview)); GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); #ifdef TABS_SHOW_FAVICON cairo_surface_t* favicon = webkit_web_view_get_favicon(webview); if (favicon) { int w = cairo_image_surface_get_width(favicon); int h = cairo_image_surface_get_height(favicon); GdkPixbuf* pixbuf = gdk_pixbuf_get_from_surface(favicon, 0, 0, w, h); if (pixbuf) { GdkPixbuf* scaled = gdk_pixbuf_scale_simple(pixbuf, 16, 16, GDK_INTERP_BILINEAR); GtkWidget* image = gtk_image_new_from_pixbuf(scaled); gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); g_object_unref(scaled); g_object_unref(pixbuf); } } #endif #ifdef TABS_SHOW_TITLE GtkWidget* label = gtk_label_new(title ? title : "New Tab"); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); #endif gtk_widget_show_all(hbox); gtk_notebook_set_tab_label(notebook, GTK_WIDGET(webview), hbox); } static void on_favicon_changed(WebKitWebView* webview, GParamSpec* pspec, GtkNotebook* notebook) { on_title_changed(webview, pspec, notebook); } static gboolean on_cmdbar_activate(GtkEntry* entry, gpointer data) { Cinnamon* cinnamon = (Cinnamon*)data; const char *input = gtk_entry_get_text(entry); char cmd[64]; const char *args = ""; const char *space = strchr(input, ' '); if (space) { snprintf(cmd, sizeof(cmd), "%.*s", (int)(space - input), input); args = space + 1; } else { snprintf(cmd, sizeof(cmd), "%s", input); } /* commands array defined in config.h */ for (int i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { if (strcmp(commands[i].name, cmd) == 0) { commands[i].fptr(cinnamon, args); break; } } gtk_entry_set_text(entry, ""); gtk_widget_hide(cinnamon->cmdbar); cmdbar_focused = false; set_mode(cinnamon, 0); return TRUE; } static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) { Cinnamon* cinnamon = (Cinnamon*)data; GdkModifierType relevant = GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK; if (mode == 3) { if (event->keyval >= GDK_KEY_0 && event->keyval <= GDK_KEY_9) { char digit = '0' + (event->keyval - GDK_KEY_0); int len = strlen(hint_buffer); if (len < 15) { hint_buffer[len] = digit; hint_buffer[len+1] = '\0'; char buf[32]; snprintf(buf, sizeof(buf), "-- HINT MODE -- [%s]", hint_buffer); gtk_label_set_text(GTK_LABEL(cinnamon->indicator), buf); } return TRUE; } else if (event->keyval == GDK_KEY_BackSpace) { int len = strlen(hint_buffer); if (len > 0) { hint_buffer[len-1] = '\0'; char buf[32]; snprintf(buf, sizeof(buf), "-- HINT MODE -- [%s]", hint_buffer); gtk_label_set_text(GTK_LABEL(cinnamon->indicator), buf); } return TRUE; } else if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) { hint_callback(cinnamon, hint_buffer); cancel_hint_mode(cinnamon); return TRUE; } else if (event->keyval == GDK_KEY_Escape) { cancel_hint_mode(cinnamon); return TRUE; } } else if (mode == 1 || mode == 2) { if (mode == 1 && event->keyval == GDK_KEY_Escape) { set_mode(cinnamon, 0); if (cmdbar_focused) { gtk_widget_hide(cinnamon->cmdbar); cmdbar_focused = false; } } if (mode == 2 && event->keyval == GDK_KEY_Escape && (event->state & relevant) == GDK_SHIFT_MASK) { set_mode(cinnamon, 0); } } else { if (event->keyval == GDK_KEY_colon) { gtk_widget_show(cinnamon->cmdbar); gtk_widget_grab_focus(cinnamon->cmdbar); cmdbar_focused = true; set_mode(cinnamon, 1); return TRUE; } /* keybinds array defined in config.h */ for (int i = 0; i < sizeof(keybinds) / sizeof(keybinds[0]); i++) { guint keyval; GdkModifierType mods; parse_keybind(keybinds[i].key, &keyval, &mods); if (gdk_keyval_to_lower(event->keyval) == keyval && (event->state & relevant) == mods) { keybinds[i].fptr(cinnamon, keybinds[i].arg); return TRUE; } } #ifdef NO_SEND_UNBOUND_KEYBINDS /* make sure that outside of insert/passthrough, we still don't send unbound keybinds to the webview */ return TRUE; #endif } /* event not consumed pass to webview */ return FALSE; } void set_mode(Cinnamon* cinnamon, int new_mode) { mode = new_mode; switch (new_mode) { case 0: gtk_label_set_text(GTK_LABEL(cinnamon->indicator), "-- NORMAL --"); break; case 1: gtk_label_set_text(GTK_LABEL(cinnamon->indicator), "-- INSERT --"); break; case 2: gtk_label_set_text(GTK_LABEL(cinnamon->indicator), "-- PASSTHROUGH --"); break; case 3: gtk_label_set_text(GTK_LABEL(cinnamon->indicator), "-- HINT MODE -- [start typing...]"); break; } } void tabopen(Cinnamon* cinnamon) { GtkWidget *webview = webkit_web_view_new_with_context(cinnamon->web_context); gtk_notebook_append_page(GTK_NOTEBOOK(cinnamon->notebook), webview, gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4)); g_signal_connect(webview, "notify::title", G_CALLBACK(on_title_changed), cinnamon->notebook); g_signal_connect(webview, "notify::favicon", G_CALLBACK(on_favicon_changed), cinnamon->notebook); /* HOMEPAGE and USERAGENT defined in config.h */ webkit_web_view_set_settings(WEBKIT_WEB_VIEW(webview), g_object_new(WEBKIT_TYPE_SETTINGS, "user-agent", USERAGENT, NULL)); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), HOMEPAGE); gtk_widget_show_all(cinnamon->notebook); gtk_notebook_set_current_page(GTK_NOTEBOOK(cinnamon->notebook), -1); } void tabclose(Cinnamon* cinnamon) { int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(cinnamon->notebook)); /* dont close last tab */ if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(cinnamon->notebook)) > 1) { gtk_notebook_remove_page(GTK_NOTEBOOK(cinnamon->notebook), page); } else { #ifdef NO_QUIT_ON_CLOSE_LAST_TAB tabopen(cinnamon); gtk_notebook_remove_page(GTK_NOTEBOOK(cinnamon->notebook), page); return; #endif gtk_main_quit(); } } void inject_hint_mode(Cinnamon* cinnamon, void (*callback)(Cinnamon*, char hint_buffer[16])) { if (mode == 3) return; hint_callback = callback; GtkWidget *webview = gtk_notebook_get_nth_page(GTK_NOTEBOOK(cinnamon->notebook), gtk_notebook_get_current_page(GTK_NOTEBOOK(cinnamon->notebook))); memset(hint_buffer, 0, sizeof(hint_buffer)); set_mode(cinnamon, 3); const char* js = "(function() { if (window.__cinnamon_hints) return; window.__cinnamon_hints = true; window.__cinnamon_link_map = {}; let links = Array.from(document.querySelectorAll('a, button, [role=button], input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]')); links.forEach((el, idx) => { let num = (idx + 1).toString(); let rect = el.getBoundingClientRect(); let div = document.createElement('div'); div.textContent = num; div.className = '__cinnamon_hint'; div.style.position = 'fixed'; div.style.left = rect.left + 'px'; div.style.top = (rect.top - 20) + 'px'; div.style.backgroundColor = '#fcd24d'; div.style.border = '1px solid #1e1e2f'; div.style.padding = '2px 4px'; div.style.fontSize = '12px'; div.style.fontWeight = 'bold'; div.style.zIndex = '999999'; div.style.borderRadius = '3px'; div.style.color = '#1e1e2f'; document.body.appendChild(div); window.__cinnamon_link_map[num] = el; }); })();"; webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(webview), js, NULL, NULL, NULL); } void cancel_hint_mode(Cinnamon* cinnamon) { GtkWidget *webview = gtk_notebook_get_nth_page(GTK_NOTEBOOK(cinnamon->notebook), gtk_notebook_get_current_page(GTK_NOTEBOOK(cinnamon->notebook))); const char* js = "(function() { document.querySelectorAll('.__cinnamon_hint').forEach(el => el.remove()); delete window.__cinnamon_hints; delete window.__cinnamon_link_map; })();"; webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(webview), js, NULL, NULL, NULL); set_mode(cinnamon, 0); memset(hint_buffer, 0, sizeof(hint_buffer)); } int main(int argc, char *argv[]) { #ifndef CINNAMON_ENABLED return 1; #endif gtk_init(&argc, &argv); Cinnamon cinnamon = {0}; /* Create a new window and set default size */ cinnamon.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* Size constants defined in config.h */ gtk_window_set_default_size(GTK_WINDOW(cinnamon.window), WINDOW_WIDTH, WINDOW_HEIGHT); g_signal_connect(cinnamon.window, "destroy", G_CALLBACK(gtk_main_quit), NULL); /* Create a vertical GtkBox for layout stuff */ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(cinnamon.window), vbox); cinnamon.notebook = gtk_notebook_new(); gtk_notebook_set_scrollable(GTK_NOTEBOOK(cinnamon.notebook), TRUE); // gtk_container_add(GTK_CONTAINER(cinnamon.window), cinnamon.notebook); gtk_box_pack_start(GTK_BOX(vbox), cinnamon.notebook, TRUE, TRUE, 0); /* initialize web context to save cookies */ WebKitWebsiteDataManager *data_manager = webkit_website_data_manager_new("base-data-directory", g_build_filename(g_get_user_data_dir(), "cinnamon", NULL),"base-cache-directory", g_build_filename(g_get_user_cache_dir(), "cinnamon", NULL), NULL); cinnamon.web_context = webkit_web_context_new_with_website_data_manager(data_manager); WebKitCookieManager *cookie_manager = webkit_web_context_get_cookie_manager(cinnamon.web_context); webkit_cookie_manager_set_persistent_storage(cookie_manager, g_build_filename(g_get_user_data_dir(), "cinnamon", "cookies.sqlite", NULL), WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE); /* favicon db */ webkit_web_context_set_favicon_database_directory(cinnamon.web_context, g_build_filename(g_get_user_cache_dir(), "cinnamon", "favicons", NULL)); /* Create initial tab */ tabopen(&cinnamon); /* Indicator label */ cinnamon.indicator = gtk_label_new("-- NORMAL --"); gtk_box_pack_end(GTK_BOX(vbox), cinnamon.indicator, FALSE, FALSE, 0); /* Create cmdbar */ cinnamon.cmdbar = gtk_entry_new(); gtk_box_pack_end(GTK_BOX(vbox), cinnamon.cmdbar, FALSE, FALSE, 0); /* handle keypresses */ g_signal_connect(cinnamon.window, "key-press-event", G_CALLBACK(on_key_press), &cinnamon); /* handle commands from cmdbar */ g_signal_connect(cinnamon.cmdbar, "activate", G_CALLBACK(on_cmdbar_activate), &cinnamon); gtk_widget_show_all(cinnamon.window); gtk_widget_hide(cinnamon.cmdbar); #ifdef HIDE_TAB_BAR gtk_notebook_set_show_tabs(GTK_NOTEBOOK(cinnamon.notebook), FALSE); #endif user_start(&cinnamon); gtk_main(); return 0; }