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 }