rosenrot-browser

A hackable browser based on Webkitgtk
Log | Files | Refs | README

rosenrot4.c (17339B)


      1 #include <gdk/gdk.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <webkit/webkit.h>
      5 
      6 #include "config.h"
      7 #include "plugins/plugins.h"
      8 
      9 /* Global variables */
     10 static GtkNotebook* notebook;
     11 static GtkWindow* window;
     12 typedef enum { _SEARCH, _FIND, _HIDDEN } Bar_entry_mode;
     13 static struct {
     14     GtkHeaderBar* widget;
     15     GtkEntry* line;
     16     GtkEntryBuffer* line_text;
     17     Bar_entry_mode entry_mode;
     18 } bar;
     19 static int num_tabs = 0;
     20 static int custom_style_enabled = 1;
     21 
     22 /* Forward declarations */
     23 void toggle_bar(GtkNotebook* notebook, Bar_entry_mode mode);
     24 void notebook_create_new_tab(GtkNotebook* notebook, const char* uri);
     25 static int handle_signal_keypress(void* self, int keyval, int keycode,
     26     GdkModifierType state, void* controller);
     27 
     28 /* Utils */
     29 WebKitWebView* notebook_get_webview(GtkNotebook* notebook) /* TODO: Think through whether to pass global variables or not */ 
     30 {
     31     WebKitWebView* view = WEBKIT_WEB_VIEW(gtk_notebook_get_nth_page(notebook, gtk_notebook_get_current_page(notebook)));
     32     NULLCHECK(view);
     33     return view;
     34 }
     35 
     36 /* Load content */
     37 void load_uri(WebKitWebView* view, const char* uri)
     38 {
     39     bool is_empty_uri = (strlen(uri) == 0);
     40     if (is_empty_uri) {
     41         webkit_web_view_load_uri(view, "");
     42         toggle_bar(notebook, _SEARCH);
     43         return;
     44     } 
     45 
     46     bool has_direct_uri_prefix = g_str_has_prefix(uri, "http://") || g_str_has_prefix(uri, "https://") || g_str_has_prefix(uri, "file://") || g_str_has_prefix(uri, "about:");
     47     if (has_direct_uri_prefix){
     48         webkit_web_view_load_uri(view, uri);
     49         return;
     50     } 
     51 
     52     bool has_common_domain_extension = (strstr(uri, ".com") || strstr(uri, ".org"));
     53     if (has_common_domain_extension){
     54         char tmp[strlen("https://") + strlen(uri) + 1];
     55         snprintf(tmp, sizeof(tmp) + 1, "https://%s", uri);
     56         webkit_web_view_load_uri(view, tmp);
     57         return;
     58     } 
     59 
     60     int l = SHORTCUT_N + strlen(uri) + 1;
     61     char uri_expanded[l];
     62     str_init(uri_expanded, l);
     63     int check = shortcut_expand(uri, uri_expanded);
     64     bool has_shortcut = (check == 2);
     65     if (has_shortcut){
     66         webkit_web_view_load_uri(view, uri_expanded);
     67         return;
     68     } 
     69 
     70     char tmp[strlen(uri) + strlen(SEARCH)];
     71     snprintf(tmp, sizeof(tmp), SEARCH, uri);
     72     webkit_web_view_load_uri(view, tmp);
     73 }
     74 
     75 /* Deal with new load or changed load */
     76 void redirect_if_annoying(WebKitWebView* view, const char* uri)
     77 {
     78     if (LIBRE_REDIRECT_ENABLED) {
     79         int l = LIBRE_N + strlen(uri) + 1;
     80         char uri_filtered[l];
     81         str_init(uri_filtered, l);
     82 
     83         int check = libre_redirect(uri, uri_filtered);
     84         if (check == 2) webkit_web_view_load_uri(view, uri_filtered);
     85     }
     86 }
     87 void set_custom_style(WebKitWebView* view)
     88 {
     89     if (custom_style_enabled) {
     90         char* style_js = malloc(STYLE_N + 1);
     91         read_style_js(style_js);
     92         webkit_web_view_evaluate_javascript(view, style_js, -1, NULL, "rosenrot-style-plugin", NULL, NULL, NULL);
     93         free(style_js);
     94     }
     95 }
     96 
     97 void handle_signal_load_changed(WebKitWebView* self, WebKitLoadEvent load_event,
     98     GtkNotebook* notebook)
     99 {
    100     switch (load_event) {
    101         // https://webkitgtk.org/reference/webkit2gtk/2.5.1/WebKitWebView.html
    102         case WEBKIT_LOAD_STARTED:
    103         case WEBKIT_LOAD_COMMITTED:
    104             set_custom_style(self);
    105         case WEBKIT_LOAD_REDIRECTED:
    106             redirect_if_annoying(self, webkit_web_view_get_uri(self));
    107             break;
    108         case WEBKIT_LOAD_FINISHED: {
    109             set_custom_style(self);
    110             /* Add gtk tab title */
    111             const char* webpage_title = webkit_web_view_get_title(self);
    112             const int max_length = 25;
    113             char tab_title[max_length + 1];
    114             if (webpage_title != NULL) {
    115                 for (int i = 0; i < (max_length); i++) {
    116                     tab_title[i] = webpage_title[i];
    117                     if (webpage_title[i] == '\0') {
    118                         break;
    119                     }
    120                 }
    121                 tab_title[max_length] = '\0';
    122             }
    123             gtk_notebook_set_tab_label_text(notebook, GTK_WIDGET(self),
    124                 webpage_title == NULL ? "—" : tab_title);
    125         }
    126     }
    127 }
    128 
    129 /* New tabs */
    130 WebKitWebView* create_new_webview()
    131 {
    132     WebKitSettings* settings = webkit_settings_new_with_settings(WEBKIT_DEFAULT_SETTINGS, NULL);
    133     if (CUSTOM_USER_AGENT) {
    134         webkit_settings_set_user_agent(
    135             settings,
    136             "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, "
    137             "like Gecko) Chrome/120.0.0.0 Safari/537.3");
    138             // https://www.useragents.me
    139     }
    140     WebKitNetworkSession* network_session = webkit_network_session_new(DATA_DIR, DATA_DIR);
    141     WebKitUserContentManager* contentmanager = webkit_user_content_manager_new();
    142     WebKitCookieManager* cookiemanager = webkit_network_session_get_cookie_manager(network_session);
    143     webkit_cookie_manager_set_persistent_storage(cookiemanager, DATA_DIR "/cookies.sqlite", WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
    144     webkit_cookie_manager_set_accept_policy(cookiemanager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
    145 
    146     WebKitWebView* view = g_object_new(WEBKIT_TYPE_WEB_VIEW, "settings", settings, "network-session", network_session, "user-content-manager", contentmanager, NULL);
    147     NULLCHECK(view);
    148 
    149     GtkEventController* event_controller = gtk_event_controller_key_new();
    150     g_signal_connect(event_controller, "key-pressed", G_CALLBACK(handle_signal_keypress), NULL);
    151     gtk_widget_add_controller(GTK_WIDGET(view), event_controller);
    152 
    153     return view;
    154 }
    155 
    156 GtkWidget* handle_signal_create_new_tab(WebKitWebView* self,
    157     WebKitNavigationAction* navigation_action,
    158     GtkNotebook* notebook)
    159 {
    160     NULLCHECK(self);
    161     NULLCHECK(notebook);
    162     if (num_tabs < MAX_NUM_TABS || num_tabs == 0) {
    163         WebKitURIRequest* uri_request = webkit_navigation_action_get_request(navigation_action);
    164         const char* uri = webkit_uri_request_get_uri(uri_request);
    165         webkit_web_view_stop_loading(self);
    166         printf("Creating new window: %s\n", uri);
    167         notebook_create_new_tab(notebook, uri);
    168         gtk_notebook_set_show_tabs(notebook, true);
    169     } else {
    170         webkit_web_view_evaluate_javascript(self, "alert('Too many tabs, not opening a new one')", -1, NULL, "rosenrot-alert-numtabs", NULL, NULL, NULL);
    171     }
    172     return ABORT_REQUEST_ON_CURRENT_TAB;
    173     // Could also return GTK_WIDGET(self), in which case the new uri would also be loaded in the current webview. This could be interesting if I wanted to e.g., open an alternative frontend in a new tab
    174 }
    175 
    176 void notebook_create_new_tab(GtkNotebook* notebook, const char* uri)
    177 {
    178     if (num_tabs < MAX_NUM_TABS || MAX_NUM_TABS == 0) {
    179         WebKitWebView* view = create_new_webview();
    180         NULLCHECK(view);
    181 
    182         g_signal_connect(view, "load_changed", G_CALLBACK(handle_signal_load_changed), notebook);
    183         g_signal_connect(view, "create", G_CALLBACK(handle_signal_create_new_tab), notebook);
    184 
    185         int n = gtk_notebook_append_page(notebook, GTK_WIDGET(view), NULL);
    186         gtk_notebook_set_tab_reorderable(notebook, GTK_WIDGET(view), true);
    187         NULLCHECK(window);
    188         NULLCHECK(bar.widget);
    189         gtk_widget_set_visible(GTK_WIDGET(window), 1);
    190         gtk_widget_set_visible(GTK_WIDGET(bar.widget), 0);
    191         load_uri(view, (uri) ? uri : HOME);
    192 
    193         set_custom_style(view);
    194 
    195         gtk_notebook_set_current_page(notebook, n);
    196         gtk_notebook_set_tab_label_text(notebook, GTK_WIDGET(view), "-");
    197         webkit_web_view_set_zoom_level(view, ZOOM_START_LEVEL);
    198         num_tabs += 1;
    199     } else {
    200         webkit_web_view_evaluate_javascript(notebook_get_webview(notebook), "alert('Too many tabs, not opening a new one')",
    201             -1, NULL, "rosenrot-alert-numtabs", NULL, NULL, NULL);
    202     }
    203 }
    204 
    205 /* Top bar */
    206 void toggle_bar(GtkNotebook* notebook, Bar_entry_mode mode)
    207 {
    208     bar.entry_mode = mode;
    209     switch (bar.entry_mode) {
    210         case _SEARCH: {
    211             const char* url = webkit_web_view_get_uri(notebook_get_webview(notebook));
    212             gtk_entry_set_placeholder_text(bar.line, "Search");
    213             gtk_entry_buffer_set_text(bar.line_text, url, strlen(url));
    214             gtk_widget_set_visible(GTK_WIDGET(bar.widget), 1);
    215             gtk_window_set_focus(window, GTK_WIDGET(bar.line));
    216             break;
    217         }
    218         case _FIND: {
    219             const char* search_text = webkit_find_controller_get_search_text(
    220                 webkit_web_view_get_find_controller(notebook_get_webview(notebook)));
    221 
    222             if (search_text != NULL)
    223                 gtk_entry_buffer_set_text(bar.line_text, search_text, strlen(search_text));
    224 
    225             gtk_entry_set_placeholder_text(bar.line, "Find");
    226             gtk_widget_set_visible(GTK_WIDGET(bar.widget), 1);
    227             gtk_window_set_focus(window, GTK_WIDGET(bar.line));
    228             break;
    229         }
    230         case _HIDDEN:
    231             gtk_widget_set_visible(GTK_WIDGET(bar.widget), 0);
    232     }
    233 }
    234 
    235 // Handle what happens when the user is on the bar and presses enter
    236 void handle_signal_bar_press_enter(GtkEntry* self, GtkNotebook* notebook) /* consider passing notebook as the data here? */
    237 {
    238     WebKitWebView* view = notebook_get_webview(notebook);
    239     if (bar.entry_mode == _SEARCH)
    240         load_uri(view, gtk_entry_buffer_get_text(bar.line_text));
    241     else if (bar.entry_mode == _FIND)
    242         webkit_find_controller_search(
    243             webkit_web_view_get_find_controller(view),
    244             gtk_entry_buffer_get_text(bar.line_text),
    245             WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND,
    246             G_MAXUINT);
    247 
    248     gtk_widget_set_visible(GTK_WIDGET(bar.widget), 0);
    249 }
    250 
    251 /* Shortcuts */
    252 int handle_shortcut(func id)
    253 {
    254     static double zoom = ZOOM_START_LEVEL;
    255     static bool is_fullscreen = 0;
    256 
    257     WebKitWebView* view = notebook_get_webview(notebook);
    258     NULLCHECK(notebook);
    259     NULLCHECK(view);
    260 
    261     switch (id) {
    262         case goback:
    263             webkit_web_view_go_back(view);
    264             break;
    265         case goforward:
    266             webkit_web_view_go_forward(view);
    267             break;
    268 
    269         case toggle_custom_style: 
    270             custom_style_enabled ^= 1; 
    271             // fallthrough
    272         case refresh:
    273             webkit_web_view_reload(view);
    274             break;
    275         case refresh_force:
    276             webkit_web_view_reload_bypass_cache(view);
    277             break;
    278 
    279         case back_to_home:
    280             load_uri(view, HOME);
    281             break;
    282 
    283         case zoomin:
    284             webkit_web_view_set_zoom_level(view,
    285                 (zoom += ZOOM_STEPSIZE));
    286             break;
    287         case zoomout:
    288             webkit_web_view_set_zoom_level(view,
    289                 (zoom -= ZOOM_STEPSIZE));
    290             break;
    291         case zoom_reset:
    292             webkit_web_view_set_zoom_level(view,
    293                 (zoom = ZOOM_START_LEVEL));
    294             break;
    295 
    296         case prev_tab:; // declarations aren't statements
    297             // https://stackoverflow.com/questions/92396/why-cant-variables-be-declared-in-a-switch-statement
    298             int n = gtk_notebook_get_n_pages(notebook);
    299             int k = gtk_notebook_get_current_page(notebook);
    300             int o = (n + k - 1) % n;
    301             gtk_notebook_set_current_page(notebook, o);
    302             break;
    303         case next_tab:;
    304             int m = gtk_notebook_get_n_pages(notebook);
    305             int l = gtk_notebook_get_current_page(notebook);
    306             int p = (l + 1) % m;
    307             gtk_notebook_set_current_page(notebook, p);
    308             break;
    309         case close_tab:
    310             num_tabs -= 1;
    311             switch(num_tabs){
    312                 case 0:
    313                     exit(0);
    314                     break;
    315                 case 1:
    316                     gtk_notebook_set_show_tabs(notebook, false);
    317                     // fallthrough
    318                 default:
    319                     gtk_notebook_remove_page(notebook, gtk_notebook_get_current_page(notebook));
    320             }
    321             break;
    322         case toggle_fullscreen:
    323             if (is_fullscreen)
    324                 gtk_window_unfullscreen(window);
    325             else
    326                 gtk_window_fullscreen(window);
    327             is_fullscreen = !is_fullscreen;
    328             break;
    329         case show_searchbar:
    330             toggle_bar(notebook, _SEARCH);
    331             break;
    332         case show_finder:
    333             toggle_bar(notebook, _FIND);
    334             break;
    335 
    336         case finder_next:
    337             webkit_find_controller_search_next(webkit_web_view_get_find_controller(view));
    338             break;
    339         case finder_prev:
    340             webkit_find_controller_search_previous(webkit_web_view_get_find_controller(view));
    341             break;
    342 
    343         case new_tab:
    344             notebook_create_new_tab(notebook, NULL);
    345             gtk_notebook_set_show_tabs(notebook, true);
    346             toggle_bar(notebook, _SEARCH);
    347             break;
    348 
    349         case hide_bar:
    350             gtk_widget_set_visible(GTK_WIDGET(bar.widget), 0);
    351             toggle_bar(notebook, _HIDDEN);
    352             break;
    353 
    354         case halve_window:
    355             gtk_window_set_default_size(window, FULL_WIDTH/2, HEIGHT);
    356             break;
    357         case rebig_window:
    358             gtk_window_set_default_size(window, FULL_WIDTH, HEIGHT);
    359             break;
    360 
    361         case prettify: {
    362             if (READABILITY_ENABLED) {
    363                 char* readability_js = malloc(READABILITY_N + 1);
    364                 read_readability_js(readability_js);
    365                 webkit_web_view_evaluate_javascript(view, readability_js, -1, NULL, "rosenrot-readability-plugin", NULL, NULL, NULL);
    366                 free(readability_js);
    367             }
    368             break;
    369         }
    370 
    371         case save_uri_to_txt: {
    372             const char* uri = webkit_web_view_get_uri(view);
    373             FILE *f = fopen("/opt/rosenrot/uris.txt", "a");
    374             if (f == NULL) {
    375                 printf("Error opening /opt/rosenrot/uris.txt");
    376             } else {
    377                 fprintf(f, "%s\n", uri);
    378                 fclose(f);
    379                 webkit_web_view_evaluate_javascript(view, "alert('Saved current uri to /opt/rosenrot/uris.txt')", -1, NULL, "rosenrot-alert-numtabs", NULL, NULL, NULL);
    380             }
    381         }
    382     }
    383 
    384     return 1;
    385 }
    386 
    387 /* Listen to keypresses */
    388 
    389 static int handle_signal_keypress(void* self, int keyval, int keycode,
    390     GdkModifierType state, void* controller)
    391 {
    392 
    393     if (0) {
    394         printf("New keypress\n");
    395         printf("Keypress state: %d\n", state);
    396         printf("Keypress value: %d\n", keyval);
    397     }
    398     for (int i = 0; i < sizeof(shortcut) / sizeof(shortcut[0]); i++) {
    399         if ((state & shortcut[i].mod || shortcut[i].mod == 0x0) && keyval == shortcut[i].key) {
    400             printf("New shortcut, with id: %d\n", shortcut[i].id);
    401             return handle_shortcut(shortcut[i].id);
    402         }
    403     }
    404 
    405     return 0;
    406 }
    407 
    408 int main(int argc, char** argv)
    409 {
    410     // Initialize GTK in general
    411     gtk_init();
    412     g_object_set(gtk_settings_get_default(), GTK_SETTINGS_CONFIG_H, NULL); 
    413     // https://docs.gtk.org/gobject/method.Object.set.html
    414     GtkCssProvider* css = gtk_css_provider_new();
    415     gtk_css_provider_load_from_path(css, "/opt/rosenrot/style-gtk4.css");
    416     gtk_style_context_add_provider_for_display(gdk_display_get_default(), GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_USER);
    417 
    418     // Create the main window
    419     window = GTK_WINDOW(gtk_window_new());
    420     gtk_window_set_default_size(window, WIDTH, HEIGHT);
    421 
    422     // Set up notebook
    423     notebook = GTK_NOTEBOOK(gtk_notebook_new());
    424     gtk_notebook_set_show_tabs(notebook, false);
    425     gtk_notebook_set_show_border(notebook, false);
    426     gtk_window_set_child(window, GTK_WIDGET(notebook));
    427 
    428     // Set up top bar
    429     bar.line_text = GTK_ENTRY_BUFFER(gtk_entry_buffer_new("", 0));
    430     bar.line = GTK_ENTRY(gtk_entry_new_with_buffer(bar.line_text));
    431     gtk_entry_set_alignment(bar.line, 0.5);
    432     gtk_widget_set_size_request(GTK_WIDGET(bar.line), BAR_WIDTH, -1);
    433 
    434     bar.widget = GTK_HEADER_BAR(gtk_header_bar_new());
    435     gtk_header_bar_set_title_widget(bar.widget, GTK_WIDGET(bar.line));
    436     gtk_window_set_titlebar(window, GTK_WIDGET(bar.widget));
    437 
    438     // Setup signals
    439     GtkEventController* event_controller = gtk_event_controller_key_new();
    440     g_signal_connect(event_controller, "key-pressed", G_CALLBACK(handle_signal_keypress), NULL);
    441     gtk_widget_add_controller(GTK_WIDGET(window), event_controller);
    442 
    443     g_signal_connect(bar.line, "activate", G_CALLBACK(handle_signal_bar_press_enter), notebook); 
    444     g_signal_connect(GTK_WIDGET(window), "destroy", G_CALLBACK(exit), notebook);
    445 
    446     // Load first tab
    447     char* first_uri = argc > 1 ? argv[1] : HOME;
    448     notebook_create_new_tab(notebook, first_uri);
    449 
    450     // Show to user 
    451     // The first two commands are redundant with notebook_create_new_tab
    452     gtk_window_present(window); 
    453     gtk_widget_set_visible(GTK_WIDGET(window), 1);
    454     if (argc != 0) gtk_widget_set_visible(GTK_WIDGET(bar.widget), 0);
    455 
    456     // Deal with more tabs, if any 
    457     if (argc > 2) {
    458         gtk_notebook_set_show_tabs(notebook, true);
    459         for (int i = 2; i < argc; i++) {
    460             notebook_create_new_tab(notebook, argv[i]);
    461         }
    462     }
    463 
    464     // Enter the main event loop, and wait for user interaction
    465     while (g_list_model_get_n_items(gtk_window_get_toplevels()) > 0 && num_tabs > 0)
    466         g_main_context_iteration(NULL, TRUE);
    467 
    468     return 0;
    469 }