rosenrot-browser

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

rosenrot3.c (16169B)


      1 #include <gdk/gdk.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <webkit2/webkit2.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 
     26 /* Utils */
     27 WebKitWebView* notebook_get_webview(GtkNotebook* notebook)
     28 {
     29     return WEBKIT_WEB_VIEW(gtk_notebook_get_nth_page(notebook, gtk_notebook_get_current_page(notebook)));
     30 }
     31 
     32 /* Load content*/
     33 void load_uri(WebKitWebView* view, const char* uri)
     34 {
     35     bool is_empty_uri = (strlen(uri) == 0);
     36     if (is_empty_uri) {
     37         webkit_web_view_load_uri(view, "");
     38         toggle_bar(notebook, _SEARCH);
     39         return; 
     40     } 
     41 
     42     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:");
     43     if (has_direct_uri_prefix){
     44         webkit_web_view_load_uri(view, uri);
     45         return;
     46     } 
     47 
     48     bool has_common_domain_extension = (strstr(uri, ".com") || strstr(uri, ".org"));
     49     if (has_common_domain_extension){
     50         char tmp[strlen("https://") + strlen(uri) + 1];
     51         snprintf(tmp, sizeof(tmp) + 1, "https://%s", uri);
     52         webkit_web_view_load_uri(view, tmp);
     53         return; 
     54     } 
     55 
     56     int l = SHORTCUT_N + strlen(uri) + 1;
     57     char uri_expanded[l];
     58     str_init(uri_expanded, l);
     59     int check = shortcut_expand(uri, uri_expanded);
     60     bool has_shortcut = (check == 2);
     61     if (has_shortcut){
     62         webkit_web_view_load_uri(view, uri_expanded);
     63         return;
     64     } 
     65 
     66     char tmp[strlen(uri) + strlen(SEARCH)];
     67     snprintf(tmp, sizeof(tmp), SEARCH, uri);
     68     webkit_web_view_load_uri(view, tmp);
     69 }
     70 
     71 /* Deal with new load or changed load */
     72 void redirect_if_annoying(WebKitWebView* view, const char* uri)
     73 {
     74     if (LIBRE_REDIRECT_ENABLED) {
     75         int l = LIBRE_N + strlen(uri) + 1;
     76         char uri_filtered[l];
     77         str_init(uri_filtered, l);
     78 
     79         int check = libre_redirect(uri, uri_filtered);
     80         if (check == 2) webkit_web_view_load_uri(view, uri_filtered);
     81     }
     82 }
     83 void set_custom_style(WebKitWebView* view)
     84 {
     85     if (custom_style_enabled) {
     86         char* style_js = malloc(STYLE_N + 1);
     87         read_style_js(style_js);
     88         webkit_web_view_evaluate_javascript(view, style_js, -1, NULL, "rosenrot-style-plugin", NULL, NULL, NULL);
     89         free(style_js);
     90     }
     91 }
     92 void handle_signal_load_changed(WebKitWebView* self, WebKitLoadEvent load_event,
     93     GtkNotebook* notebook)
     94 {
     95     switch (load_event) {
     96         // https://webkitgtk.org/reference/webkit2gtk/2.5.1/WebKitWebView.html
     97         case WEBKIT_LOAD_STARTED:
     98         case WEBKIT_LOAD_COMMITTED:
     99             set_custom_style(self);
    100         case WEBKIT_LOAD_REDIRECTED:
    101             redirect_if_annoying(self, webkit_web_view_get_uri(self));
    102             break;
    103         case WEBKIT_LOAD_FINISHED: {
    104             set_custom_style(self);
    105             /* Add gtk tab title */
    106             const char* webpage_title = webkit_web_view_get_title(self);
    107             const int max_length = 25;
    108             char tab_title[max_length + 1];
    109             if (webpage_title != NULL) {
    110                 for (int i = 0; i < (max_length); i++) {
    111                     tab_title[i] = webpage_title[i];
    112                     if (webpage_title[i] == '\0') {
    113                         break;
    114                     }
    115                 }
    116                 tab_title[max_length] = '\0';
    117             }
    118             gtk_notebook_set_tab_label_text(notebook, GTK_WIDGET(self),
    119                 webpage_title == NULL ? "—" : tab_title);
    120         }
    121     }
    122 }
    123 
    124 /* New tabs */
    125 WebKitWebView* create_new_webview()
    126 {
    127     char* style;
    128 
    129     WebKitSettings* settings = webkit_settings_new_with_settings(WEBKIT_DEFAULT_SETTINGS, NULL);
    130     if (CUSTOM_USER_AGENT) {
    131         webkit_settings_set_user_agent(
    132             settings,
    133             "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, "
    134             "like Gecko) Chrome/120.0.0.0 Safari/537.3");
    135             // https://www.useragents.me
    136     }
    137     WebKitWebContext* web_context = webkit_web_context_new_with_website_data_manager(webkit_website_data_manager_new(DATA_MANAGER_OPTS, NULL));
    138     WebKitUserContentManager* contentmanager = webkit_user_content_manager_new();
    139 
    140     WebKitCookieManager* cookiemanager = webkit_web_context_get_cookie_manager(web_context);
    141     webkit_cookie_manager_set_persistent_storage(cookiemanager, DATA_DIR "/cookies.sqlite", WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
    142     webkit_cookie_manager_set_accept_policy(cookiemanager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
    143 
    144     if (g_file_get_contents("~/opt/rosenrot/style.css", &style, NULL, NULL)) {
    145         webkit_user_content_manager_add_style_sheet(
    146             contentmanager, webkit_user_style_sheet_new(style, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL));
    147     }
    148 
    149     WebKitWebView* view = g_object_new(WEBKIT_TYPE_WEB_VIEW, "settings", settings, "web-context", web_context, "user-content-manager", contentmanager, NULL);
    150 
    151     return view;
    152 }
    153 GtkWidget* handle_signal_create_new_tab(WebKitWebView* self,
    154     WebKitNavigationAction* navigation_action,
    155     GtkNotebook* notebook)
    156 {
    157     if (num_tabs < MAX_NUM_TABS || num_tabs == 0) {
    158         WebKitURIRequest* uri_request = webkit_navigation_action_get_request(navigation_action);
    159         const char* uri = webkit_uri_request_get_uri(uri_request);
    160         printf("Creating new window: %s\n", uri);
    161         notebook_create_new_tab(notebook, uri);
    162         gtk_notebook_set_show_tabs(notebook, true);
    163     } else {
    164         webkit_web_view_evaluate_javascript(self, "alert('Too many tabs, not opening a new one')", -1, NULL, "rosenrot-alert-numtabs", NULL, NULL, NULL);
    165     }
    166     return ABORT_REQUEST_ON_CURRENT_TAB;
    167 }
    168 
    169 void notebook_create_new_tab(GtkNotebook* notebook, const char* uri)
    170 {
    171     if (num_tabs < MAX_NUM_TABS || MAX_NUM_TABS == 0) {
    172         WebKitWebView* view = create_new_webview();
    173 
    174         g_signal_connect(view, "load_changed", G_CALLBACK(handle_signal_load_changed), notebook);
    175         g_signal_connect(view, "create", G_CALLBACK(handle_signal_create_new_tab), notebook);
    176 
    177         int n = gtk_notebook_append_page(notebook, GTK_WIDGET(view), NULL);
    178         gtk_notebook_set_tab_reorderable(notebook, GTK_WIDGET(view), true);
    179         gtk_widget_show_all(GTK_WIDGET(window));
    180         gtk_widget_hide(GTK_WIDGET(bar.widget));
    181         load_uri(view, (uri) ? uri : HOME);
    182 
    183         set_custom_style(view);
    184 
    185         gtk_notebook_set_current_page(notebook, n);
    186         gtk_notebook_set_tab_label_text(notebook, GTK_WIDGET(view), "-");
    187         webkit_web_view_set_zoom_level(view, ZOOM_START_LEVEL);
    188         num_tabs += 1;
    189     } else {
    190         webkit_web_view_evaluate_javascript(notebook_get_webview(notebook), "alert('Too many tabs, not opening a new one')",
    191             -1, NULL, "rosenrot-alert-numtabs", NULL, NULL, NULL);
    192     }
    193 }
    194 
    195 /* Top bar */
    196 void toggle_bar(GtkNotebook* notebook, Bar_entry_mode mode)
    197 {
    198     bar.entry_mode = mode;
    199     switch (bar.entry_mode) {
    200         case _SEARCH: {
    201             const char* url = webkit_web_view_get_uri(notebook_get_webview(notebook));
    202             gtk_entry_set_placeholder_text(bar.line, "Search");
    203             gtk_entry_buffer_set_text(bar.line_text, url, strlen(url));
    204             gtk_widget_show(GTK_WIDGET(bar.widget));
    205             gtk_window_set_focus(window, GTK_WIDGET(bar.line));
    206             break;
    207         }
    208         case _FIND: {
    209             const char* search_text = webkit_find_controller_get_search_text(
    210                 webkit_web_view_get_find_controller(notebook_get_webview(notebook)));
    211 
    212             if (search_text != NULL)
    213                 gtk_entry_buffer_set_text(bar.line_text, search_text, strlen(search_text));
    214 
    215             gtk_entry_set_placeholder_text(bar.line, "Find");
    216             gtk_widget_show(GTK_WIDGET(bar.widget));
    217             gtk_window_set_focus(window, GTK_WIDGET(bar.line));
    218             break;
    219         }
    220         case _HIDDEN:
    221             gtk_widget_hide(GTK_WIDGET(bar.widget));
    222     }
    223 }
    224 
    225 // Handle what happens when the user is on the bar and presses enter
    226 void handle_signal_bar_press_enter(GtkEntry* self, GtkNotebook* notebook)
    227 {
    228     if (bar.entry_mode == _SEARCH)
    229         load_uri(notebook_get_webview(notebook), gtk_entry_buffer_get_text(bar.line_text));
    230     else if (bar.entry_mode == _FIND)
    231         webkit_find_controller_search(
    232             webkit_web_view_get_find_controller(notebook_get_webview(notebook)),
    233             gtk_entry_buffer_get_text(bar.line_text),
    234             WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND,
    235             G_MAXUINT);
    236 
    237     gtk_widget_hide(GTK_WIDGET(bar.widget));
    238 }
    239 
    240 /* Shortcuts */
    241 int handle_shortcut(func id, GtkNotebook* notebook)
    242 {
    243     static double zoom = ZOOM_START_LEVEL;
    244     static bool is_fullscreen = 0;
    245 
    246     WebKitWebView* view = notebook_get_webview(notebook);
    247 
    248     switch (id) {
    249         case goback:
    250             webkit_web_view_go_back(view);
    251             break;
    252         case goforward:
    253             webkit_web_view_go_forward(view);
    254             break;
    255 
    256         case toggle_custom_style: /* Ctrl s + Ctrl Shift R to reload */
    257             custom_style_enabled ^= 1; 
    258             // fallthrough
    259         case refresh:
    260             webkit_web_view_reload(view);
    261             break;
    262         case refresh_force:
    263             webkit_web_view_reload_bypass_cache(view);
    264             break;
    265 
    266         case back_to_home:
    267             load_uri(view, HOME);
    268             break;
    269 
    270         case zoomin:
    271             webkit_web_view_set_zoom_level(view,
    272                 (zoom += ZOOM_STEPSIZE));
    273             break;
    274         case zoomout:
    275             webkit_web_view_set_zoom_level(view,
    276                 (zoom -= ZOOM_STEPSIZE));
    277             break;
    278         case zoom_reset:
    279             webkit_web_view_set_zoom_level(view,
    280                 (zoom = ZOOM_START_LEVEL));
    281             break;
    282 
    283         case prev_tab:; // declarations aren't statements
    284             // https://stackoverflow.com/questions/92396/why-cant-variables-be-declared-in-a-switch-statement
    285             int n = gtk_notebook_get_n_pages(notebook);
    286             int k = gtk_notebook_get_current_page(notebook);
    287             int l = (n + k - 1) % n;
    288             gtk_notebook_set_current_page(notebook, l);
    289             break;
    290         case next_tab:;
    291             int m = gtk_notebook_get_n_pages(notebook);
    292             int i = gtk_notebook_get_current_page(notebook);
    293             int j = (i + 1) % m;
    294             gtk_notebook_set_current_page(notebook, j);
    295             break;
    296         case close_tab:
    297             num_tabs -= 1;
    298             switch(num_tabs){
    299                 case 0:
    300                     exit(0);
    301                     break;
    302                 case 1:
    303                     gtk_notebook_set_show_tabs(notebook, false);
    304                     // fallthrough
    305                 default:
    306                     gtk_notebook_remove_page(notebook, gtk_notebook_get_current_page(notebook));
    307             }
    308             break;
    309         case toggle_fullscreen:
    310             if (is_fullscreen)
    311                 gtk_window_unfullscreen(window);
    312             else
    313                 gtk_window_fullscreen(window);
    314             is_fullscreen = !is_fullscreen;
    315             break;
    316         case show_searchbar:
    317             toggle_bar(notebook, _SEARCH);
    318             break;
    319         case show_finder:
    320             toggle_bar(notebook, _FIND);
    321             break;
    322 
    323         case finder_next:
    324             webkit_find_controller_search_next(webkit_web_view_get_find_controller(view));
    325             break;
    326         case finder_prev:
    327             webkit_find_controller_search_previous(webkit_web_view_get_find_controller(view));
    328             break;
    329 
    330         case new_tab:
    331             notebook_create_new_tab(notebook, NULL);
    332             gtk_notebook_set_show_tabs(notebook, true);
    333             toggle_bar(notebook, _SEARCH);
    334             break;
    335 
    336         case hide_bar:
    337             toggle_bar(notebook, _HIDDEN);
    338             break;
    339 
    340         case halve_window:
    341             gtk_window_resize(window, FULL_WIDTH/2, HEIGHT);
    342             break;
    343         case rebig_window:
    344             gtk_window_resize(window, FULL_WIDTH, HEIGHT);
    345             break;
    346 
    347         case prettify: {
    348             if (READABILITY_ENABLED) {
    349                 char* readability_js = malloc(READABILITY_N + 1);
    350                 read_readability_js(readability_js);
    351                 webkit_web_view_evaluate_javascript(view, readability_js, -1, NULL, "rosenrot-readability-plugin", NULL, NULL, NULL);
    352                 free(readability_js);
    353             }
    354             break;
    355         }
    356     }
    357 
    358     return 1;
    359 }
    360 // Listen to key presses and call shortcuts if needed.
    361 int handle_signal_keypress(void* self, GdkEvent* event, GtkNotebook* notebook)
    362 {
    363     (void)self;
    364 
    365     guint event_keyval = 0;
    366     gdk_event_get_keyval(event, &event_keyval);
    367     GdkModifierType event_state = 0;
    368     gdk_event_get_state(event, &event_state);
    369 
    370     if (0) {
    371         printf("Keypress state: %d\n", event_state);
    372         printf("Keypress value: %d\n", event_keyval);
    373     }
    374 
    375     for (int i = 0; i < sizeof(shortcut) / sizeof(shortcut[0]); i++)
    376         if ((event_state & shortcut[i].mod || shortcut[i].mod == 0x0) && event_keyval == shortcut[i].key)
    377             return handle_shortcut(shortcut[i].id, notebook);
    378     /*
    379     If I wanted to bind button presses, like the extra button in the mouse,
    380     I would have to bind the button-press-event signal instead.
    381     Some links in case I go down that road:
    382     - https://docs.gtk.org/gtk3/signal.Widget.button-press-event.html
    383     - https://docs.gtk.org/gdk3/union.Event.html
    384     - https://docs.gtk.org/gdk3/struct.EventButton.html
    385     */
    386     return 0;
    387 }
    388 
    389 int main(int argc, char** argv)
    390 {
    391     /* Initialize GTK in general */
    392     gtk_init(NULL, NULL); // https://docs.gtk.org/gtk3/func.init.html
    393     g_object_set(gtk_settings_get_default(), GTK_SETTINGS_CONFIG_H, NULL); // https://docs.gtk.org/gobject/method.Object.set.html
    394     GtkCssProvider* css = gtk_css_provider_new();
    395     gtk_css_provider_load_from_path(css, "/opt/rosenrot/style-gtk3.css", NULL);
    396     gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css), 800); 
    397 
    398     /* Initialize GTK objects. These are declared as static globals at the top of this file */
    399 
    400     // Window
    401     window = GTK_WINDOW(gtk_window_new(0));
    402     gtk_window_set_default_size(window, WIDTH, HEIGHT);
    403     // Notebook
    404     notebook = GTK_NOTEBOOK(gtk_notebook_new());
    405     gtk_notebook_set_show_tabs(notebook, false);
    406     gtk_notebook_set_show_border(notebook, false);
    407     gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(notebook));
    408 
    409     // Bar
    410     bar.line_text = GTK_ENTRY_BUFFER(gtk_entry_buffer_new("", 0));
    411     bar.line = GTK_ENTRY(gtk_entry_new_with_buffer(bar.line_text));
    412     gtk_entry_set_alignment(bar.line, 0.48);
    413     gtk_widget_set_size_request(GTK_WIDGET(bar.line), BAR_WIDTH, -1);
    414 
    415     bar.widget = GTK_HEADER_BAR(gtk_header_bar_new());
    416     gtk_header_bar_set_custom_title(bar.widget, GTK_WIDGET(bar.line));
    417     gtk_window_set_titlebar(window, GTK_WIDGET(bar.widget));
    418 
    419     // Signals
    420     g_signal_connect(window, "key-press-event", G_CALLBACK(handle_signal_keypress), notebook);
    421     g_signal_connect(window, "destroy", G_CALLBACK(exit), notebook);
    422     g_signal_connect(bar.line, "activate", G_CALLBACK(handle_signal_bar_press_enter), notebook);
    423 
    424 
    425     /* Load first tab */
    426     char* first_uri = argc > 1 ? argv[1] : HOME;
    427     notebook_create_new_tab(notebook, first_uri);
    428 
    429     /* Show to user */
    430     gtk_widget_show_all(GTK_WIDGET(window));
    431     if (argc != 0) gtk_widget_hide(GTK_WIDGET(bar.widget));
    432 
    433     /* Deal with more tabs */
    434     if (argc > 2) {
    435         gtk_notebook_set_show_tabs(notebook, true);
    436         for (int i = 2; i < argc; i++) {
    437             notebook_create_new_tab(notebook, argv[i]);
    438         }
    439     }
    440 
    441     gtk_main(); 
    442 }