2026-08-12 18:48:46 +08:00
|
|
|
/* See LICENSE file for copyright and license details */
|
|
|
|
|
|
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
|
#include <webkit/webkit.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
GtkWidget *window;
|
|
|
|
|
GtkWidget *header;
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
GtkWidget *url_entry;
|
|
|
|
|
#endif
|
|
|
|
|
GtkWidget *web_view;
|
|
|
|
|
GtkWidget *progress;
|
2026-08-13 16:22:36 +08:00
|
|
|
GtkWidget *find_bar;
|
|
|
|
|
GtkWidget *find_entry;
|
|
|
|
|
GtkWidget *find_count_label;
|
|
|
|
|
gboolean find_open;
|
2026-08-12 18:48:46 +08:00
|
|
|
} PageApp;
|
|
|
|
|
|
|
|
|
|
static void update_title(PageApp *app) {
|
|
|
|
|
const char *title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
gtk_window_set_title(GTK_WINDOW(app->window), title && *title ? title : "page");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
static void update_url_bar(PageApp *app) {
|
|
|
|
|
const char *uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
if (uri)
|
|
|
|
|
gtk_editable_set_text(GTK_EDITABLE(app->url_entry), uri);
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
static void update_url_bar(PageApp *app) {
|
|
|
|
|
(void)app; /* no bar to update when compiled without one (duh) */
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static gchar *normalize_input(const char *input) {
|
|
|
|
|
/* provided it looks like it has a scheme or a dot, treat as URL.
|
|
|
|
|
* otherwise, treat as a query */
|
|
|
|
|
if (strstr(input, "://") != NULL)
|
|
|
|
|
return g_strdup(input);
|
|
|
|
|
|
|
|
|
|
if (strchr(input, ' ') == NULL && strchr(input, '.') != NULL)
|
|
|
|
|
return g_strdup_printf("https://%s", input);
|
|
|
|
|
|
|
|
|
|
gchar *escaped = g_uri_escape_string(input, NULL, FALSE);
|
|
|
|
|
gchar *query = g_strconcat(SEARCH_URL_PREFIX, escaped, NULL);
|
|
|
|
|
g_free(escaped);
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
static void on_url_activate(GtkEntry *entry, gpointer user_data) {
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
const char *text = gtk_editable_get_text(GTK_EDITABLE(entry));
|
|
|
|
|
if (!text || !*text)
|
|
|
|
|
return;
|
|
|
|
|
gchar *uri = normalize_input(text);
|
|
|
|
|
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(app->web_view), uri);
|
|
|
|
|
g_free(uri);
|
|
|
|
|
gtk_widget_grab_focus(app->web_view);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent event, gpointer user_data) {
|
|
|
|
|
(void)web_view;
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
switch (event) {
|
|
|
|
|
case WEBKIT_LOAD_STARTED:
|
|
|
|
|
case WEBKIT_LOAD_REDIRECTED:
|
|
|
|
|
gtk_widget_set_visible(app->progress, TRUE);
|
|
|
|
|
break;
|
|
|
|
|
case WEBKIT_LOAD_COMMITTED:
|
|
|
|
|
update_url_bar(app);
|
|
|
|
|
break;
|
|
|
|
|
case WEBKIT_LOAD_FINISHED:
|
|
|
|
|
gtk_widget_set_visible(app->progress, FALSE);
|
|
|
|
|
update_title(app);
|
|
|
|
|
update_url_bar(app);
|
2026-08-15 00:33:25 +08:00
|
|
|
webkit_web_view_set_zoom_level(web_view, ZOOM_DEFAULT);
|
2026-08-12 18:48:46 +08:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_progress_changed(WebKitWebView *web_view, GParamSpec *pspec, gpointer user_data) {
|
|
|
|
|
(void)pspec;
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
gdouble p = webkit_web_view_get_estimated_load_progress(web_view);
|
|
|
|
|
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(app->progress), p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_title_changed(WebKitWebView *web_view, GParamSpec *pspec, gpointer user_data) {
|
|
|
|
|
(void)web_view;
|
|
|
|
|
(void)pspec;
|
|
|
|
|
update_title((PageApp *)user_data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void zoom_by(PageApp *app, double delta) {
|
|
|
|
|
gdouble z = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(app->web_view), z + delta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void zoom_reset(PageApp *app) {
|
2026-08-12 19:05:30 +08:00
|
|
|
webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(app->web_view), ZOOM_DEFAULT);
|
2026-08-12 18:48:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void toggle_fullscreen(PageApp *app) {
|
|
|
|
|
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(app->window));
|
|
|
|
|
GdkToplevelState tstate = gdk_toplevel_get_state(GDK_TOPLEVEL(surface));
|
|
|
|
|
if (tstate & GDK_TOPLEVEL_STATE_FULLSCREEN)
|
|
|
|
|
gtk_window_unfullscreen(GTK_WINDOW(app->window));
|
|
|
|
|
else
|
|
|
|
|
gtk_window_fullscreen(GTK_WINDOW(app->window));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* scroll the loaded page's content by 'delta' pixels (negative = up).
|
|
|
|
|
* WebKitWebView's own GtkScrollable plumbing controls the widget's
|
|
|
|
|
* position in page's layout, not the document inside it, so this goes
|
|
|
|
|
* through JS instead (the same thing a page's own scroll code would do) */
|
|
|
|
|
static void scroll_by(PageApp *app, int delta) {
|
|
|
|
|
gchar *script = g_strdup_printf("window.scrollBy(0, %d);", delta);
|
|
|
|
|
webkit_web_view_evaluate_javascript(WEBKIT_WEB_VIEW(app->web_view),
|
|
|
|
|
script, -1, NULL, NULL, NULL, NULL, NULL);
|
|
|
|
|
g_free(script);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 16:22:36 +08:00
|
|
|
#define FIND_OPTIONS (WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND)
|
|
|
|
|
#define FIND_MAX_MATCHES 1000
|
|
|
|
|
|
|
|
|
|
static WebKitFindController *find_controller(PageApp *app) {
|
|
|
|
|
return webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void find_show(PageApp *app) {
|
|
|
|
|
app->find_open = TRUE;
|
|
|
|
|
gtk_widget_set_visible(app->find_bar, TRUE);
|
|
|
|
|
gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), TRUE);
|
|
|
|
|
gtk_widget_grab_focus(app->find_entry);
|
|
|
|
|
const char *text = gtk_editable_get_text(GTK_EDITABLE(app->find_entry));
|
|
|
|
|
if (text && *text)
|
|
|
|
|
gtk_editable_select_region(GTK_EDITABLE(app->find_entry), 0, -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void find_hide(PageApp *app) {
|
|
|
|
|
app->find_open = FALSE;
|
|
|
|
|
webkit_find_controller_search_finish(find_controller(app));
|
|
|
|
|
gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), FALSE);
|
|
|
|
|
gtk_label_set_text(GTK_LABEL(app->find_count_label), "");
|
|
|
|
|
gtk_widget_grab_focus(app->web_view);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static gboolean find_bar_visible(PageApp *app) {
|
|
|
|
|
return gtk_revealer_get_reveal_child(GTK_REVEALER(app->find_bar));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void find_run(PageApp *app, WebKitFindOptions extra_opts) {
|
|
|
|
|
const char *text = gtk_editable_get_text(GTK_EDITABLE(app->find_entry));
|
|
|
|
|
if (!text || !*text) {
|
|
|
|
|
webkit_find_controller_search_finish(find_controller(app));
|
|
|
|
|
gtk_label_set_text(GTK_LABEL(app->find_count_label), "");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
webkit_find_controller_search(find_controller(app), text,
|
|
|
|
|
FIND_OPTIONS | extra_opts, FIND_MAX_MATCHES);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_find_entry_changed(GtkEditable *entry, gpointer user_data) {
|
|
|
|
|
(void)entry;
|
|
|
|
|
find_run((PageApp *)user_data, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_find_entry_activate(GtkEntry *entry, gpointer user_data) {
|
|
|
|
|
(void)entry;
|
|
|
|
|
find_run((PageApp *)user_data, WEBKIT_FIND_OPTIONS_NONE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_find_next_clicked(GtkButton *btn, gpointer user_data) {
|
|
|
|
|
(void)btn;
|
|
|
|
|
webkit_find_controller_search_next(find_controller((PageApp *)user_data));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_find_prev_clicked(GtkButton *btn, gpointer user_data) {
|
|
|
|
|
(void)btn;
|
|
|
|
|
webkit_find_controller_search_previous(find_controller((PageApp *)user_data));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_find_close_clicked(GtkButton *btn, gpointer user_data) {
|
|
|
|
|
(void)btn;
|
|
|
|
|
find_hide((PageApp *)user_data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_found_text(WebKitFindController *ctrl, guint match_count, gpointer user_data) {
|
|
|
|
|
(void)ctrl;
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
if (!app->find_open)
|
|
|
|
|
return;
|
|
|
|
|
gchar *label = match_count == G_MAXUINT
|
|
|
|
|
? g_strdup_printf("%d+ matches", FIND_MAX_MATCHES)
|
|
|
|
|
: g_strdup_printf("%u match%s", match_count, match_count == 1 ? "" : "es");
|
|
|
|
|
gtk_label_set_text(GTK_LABEL(app->find_count_label), label);
|
|
|
|
|
g_free(label);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_failed_to_find_text(WebKitFindController *ctrl, gpointer user_data) {
|
|
|
|
|
(void)ctrl;
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
if (!app->find_open)
|
|
|
|
|
return;
|
|
|
|
|
gtk_label_set_text(GTK_LABEL(app->find_count_label), "no matches");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* downloads and such */
|
|
|
|
|
|
|
|
|
|
static gchar *unique_destination_path(const char *dir, const char *filename) {
|
|
|
|
|
gchar *path = g_build_filename(dir, filename, NULL);
|
|
|
|
|
if (!g_file_test(path, G_FILE_TEST_EXISTS))
|
|
|
|
|
return path;
|
|
|
|
|
|
|
|
|
|
gchar *base = g_strdup(filename);
|
|
|
|
|
gchar *ext = strrchr(base, '.');
|
|
|
|
|
gchar *stem = (ext && ext != base) ? g_strndup(base, ext - base) : g_strdup(base);
|
|
|
|
|
const char *suffix = (ext && ext != base) ? ext : "";
|
|
|
|
|
|
|
|
|
|
gchar *candidate = NULL;
|
|
|
|
|
int n;
|
|
|
|
|
for (n = 1; n <= 999; n++) {
|
|
|
|
|
g_free(candidate);
|
|
|
|
|
candidate = g_strdup_printf("%s (%d)%s", stem, n, suffix);
|
|
|
|
|
g_free(path);
|
|
|
|
|
path = g_build_filename(dir, candidate, NULL);
|
|
|
|
|
if (!g_file_test(path, G_FILE_TEST_EXISTS))
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (n > 999) {
|
|
|
|
|
g_free(path);
|
|
|
|
|
gchar *unique_suffix = g_strdup_printf("-%" G_GINT64_FORMAT "%s",
|
|
|
|
|
g_get_real_time(), suffix);
|
|
|
|
|
gchar *unique_name = g_strconcat(stem, unique_suffix, NULL);
|
|
|
|
|
path = g_build_filename(dir, unique_name, NULL);
|
|
|
|
|
g_free(unique_suffix);
|
|
|
|
|
g_free(unique_name);
|
|
|
|
|
}
|
|
|
|
|
g_free(candidate);
|
|
|
|
|
g_free(base);
|
|
|
|
|
g_free(stem);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static gboolean on_decide_destination(WebKitDownload *download, const gchar *suggested_filename,
|
|
|
|
|
gpointer user_data) {
|
|
|
|
|
(void)user_data;
|
|
|
|
|
|
|
|
|
|
const char *xdg_downloads = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
|
|
|
|
|
gchar *dir_owned = xdg_downloads ? NULL : g_build_filename(g_get_home_dir(), "Downloads", NULL);
|
|
|
|
|
const char *dir = xdg_downloads ? xdg_downloads : dir_owned;
|
|
|
|
|
g_mkdir_with_parents(dir, 0700);
|
|
|
|
|
|
|
|
|
|
const char *raw = (suggested_filename && *suggested_filename) ? suggested_filename : "download";
|
|
|
|
|
gchar *safe_name = g_path_get_basename(raw);
|
|
|
|
|
if (!*safe_name || g_strcmp0(safe_name, ".") == 0 || g_strcmp0(safe_name, "/") == 0) {
|
|
|
|
|
g_free(safe_name);
|
|
|
|
|
safe_name = g_strdup("download");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gchar *dest_path = unique_destination_path(dir, safe_name);
|
|
|
|
|
|
|
|
|
|
if (g_path_is_absolute(dest_path))
|
|
|
|
|
webkit_download_set_destination(download, dest_path);
|
|
|
|
|
else
|
|
|
|
|
webkit_download_cancel(download);
|
|
|
|
|
|
|
|
|
|
g_free(safe_name);
|
|
|
|
|
g_free(dest_path);
|
|
|
|
|
g_free(dir_owned);
|
|
|
|
|
return TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static gboolean on_download_started(WebKitNetworkSession *session, WebKitDownload *download,
|
|
|
|
|
gpointer user_data) {
|
|
|
|
|
(void)session;
|
|
|
|
|
(void)user_data;
|
|
|
|
|
g_signal_connect(download, "decide-destination", G_CALLBACK(on_decide_destination), NULL);
|
|
|
|
|
return TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 18:48:46 +08:00
|
|
|
/* run the behaviour bound to a PageAction */
|
|
|
|
|
static void dispatch_action(PageApp *app, PageAction action) {
|
|
|
|
|
switch (action) {
|
|
|
|
|
case ACTION_FOCUS_URL:
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
gtk_widget_grab_focus(app->url_entry);
|
|
|
|
|
gtk_editable_select_region(GTK_EDITABLE(app->url_entry), 0, -1);
|
|
|
|
|
#endif
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_QUIT:
|
|
|
|
|
gtk_window_close(GTK_WINDOW(app->window));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_RELOAD:
|
|
|
|
|
webkit_web_view_reload(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_RELOAD_HARD:
|
|
|
|
|
webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_BACK:
|
|
|
|
|
webkit_web_view_go_back(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_FORWARD:
|
|
|
|
|
webkit_web_view_go_forward(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_ZOOM_IN:
|
|
|
|
|
zoom_by(app, ZOOM_STEP);
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_ZOOM_OUT:
|
|
|
|
|
zoom_by(app, -ZOOM_STEP);
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_ZOOM_RESET:
|
|
|
|
|
zoom_reset(app);
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_FULLSCREEN:
|
|
|
|
|
toggle_fullscreen(app);
|
|
|
|
|
break;
|
2026-08-13 16:22:36 +08:00
|
|
|
case ACTION_ESCAPE: {
|
|
|
|
|
gboolean did_something = FALSE;
|
|
|
|
|
if (find_bar_visible(app)) {
|
|
|
|
|
find_hide(app);
|
|
|
|
|
did_something = TRUE;
|
|
|
|
|
}
|
2026-08-12 18:48:46 +08:00
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
if (gtk_widget_has_focus(app->url_entry)) {
|
|
|
|
|
update_url_bar(app);
|
|
|
|
|
gtk_widget_grab_focus(app->web_view);
|
2026-08-13 16:22:36 +08:00
|
|
|
did_something = TRUE;
|
2026-08-12 18:48:46 +08:00
|
|
|
}
|
|
|
|
|
#endif
|
2026-08-13 16:22:36 +08:00
|
|
|
if (!did_something)
|
|
|
|
|
webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(app->web_view));
|
2026-08-12 18:48:46 +08:00
|
|
|
break;
|
2026-08-13 16:22:36 +08:00
|
|
|
}
|
2026-08-12 18:48:46 +08:00
|
|
|
case ACTION_SCROLL_UP:
|
|
|
|
|
scroll_by(app, -SCROLL_STEP);
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_SCROLL_DOWN:
|
|
|
|
|
scroll_by(app, SCROLL_STEP);
|
|
|
|
|
break;
|
2026-08-13 16:22:36 +08:00
|
|
|
case ACTION_FIND:
|
|
|
|
|
find_show(app);
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_FIND_NEXT:
|
|
|
|
|
if (find_bar_visible(app))
|
|
|
|
|
webkit_find_controller_search_next(find_controller(app));
|
|
|
|
|
break;
|
|
|
|
|
case ACTION_FIND_PREV:
|
|
|
|
|
if (find_bar_visible(app))
|
|
|
|
|
webkit_find_controller_search_previous(find_controller(app));
|
|
|
|
|
break;
|
2026-08-12 18:48:46 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* only the modifier bits actually bind against matter for matching
|
|
|
|
|
* (ignore lock keys, numlock, etc. that GDK may also report) */
|
|
|
|
|
#define RELEVANT_MODS (GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_ALT_MASK | GDK_SUPER_MASK)
|
|
|
|
|
|
|
|
|
|
static gboolean on_key_pressed(GtkEventControllerKey *ctrl, guint keyval,
|
|
|
|
|
guint keycode, GdkModifierType state,
|
|
|
|
|
gpointer user_data) {
|
|
|
|
|
(void)ctrl;
|
|
|
|
|
(void)keycode;
|
|
|
|
|
PageApp *app = user_data;
|
|
|
|
|
GdkModifierType mods = state & RELEVANT_MODS;
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < N_KEYBINDINGS; i++) {
|
|
|
|
|
if (KEYBINDINGS[i].keyval == keyval && KEYBINDINGS[i].mods == mods) {
|
|
|
|
|
dispatch_action(app, KEYBINDINGS[i].action);
|
|
|
|
|
return TRUE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static gchar *expand_home(const char *path) {
|
|
|
|
|
if (!path)
|
|
|
|
|
return NULL;
|
|
|
|
|
if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) {
|
|
|
|
|
const char *home = g_get_home_dir();
|
|
|
|
|
return g_build_filename(home, path + 1, NULL);
|
|
|
|
|
}
|
|
|
|
|
return g_strdup(path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static WebKitNetworkSession *build_network_session(void) {
|
|
|
|
|
if (!DATA_DIR)
|
|
|
|
|
return webkit_network_session_get_default();
|
|
|
|
|
|
|
|
|
|
gchar *data_dir = expand_home(DATA_DIR);
|
|
|
|
|
gchar *cache_dir = g_build_filename(data_dir, "cache", NULL);
|
|
|
|
|
|
|
|
|
|
g_mkdir_with_parents(data_dir, 0700);
|
|
|
|
|
g_mkdir_with_parents(cache_dir, 0700);
|
|
|
|
|
|
|
|
|
|
WebKitNetworkSession *session = webkit_network_session_new(data_dir, cache_dir);
|
|
|
|
|
|
|
|
|
|
gchar *cookie_file = g_build_filename(data_dir, "cookies.sqlite", NULL);
|
|
|
|
|
WebKitCookieManager *cookie_mgr = webkit_network_session_get_cookie_manager(session);
|
|
|
|
|
webkit_cookie_manager_set_persistent_storage(cookie_mgr, cookie_file,
|
|
|
|
|
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
|
|
|
|
g_free(cookie_file);
|
|
|
|
|
|
|
|
|
|
g_free(data_dir);
|
|
|
|
|
g_free(cache_dir);
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void activate(GtkApplication *gtk_app, gpointer user_data) {
|
|
|
|
|
const char *start_uri = user_data ? (const char *)user_data : DEFAULT_URI;
|
|
|
|
|
PageApp *app = g_new0(PageApp, 1);
|
|
|
|
|
|
|
|
|
|
app->window = gtk_application_window_new(gtk_app);
|
|
|
|
|
gtk_window_set_default_size(GTK_WINDOW(app->window), WINDOW_WIDTH, WINDOW_HEIGHT);
|
|
|
|
|
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
app->header = gtk_header_bar_new();
|
|
|
|
|
gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(app->header), TRUE);
|
|
|
|
|
|
|
|
|
|
app->url_entry = gtk_entry_new();
|
|
|
|
|
gtk_widget_set_hexpand(app->url_entry, TRUE);
|
|
|
|
|
gtk_editable_set_width_chars(GTK_EDITABLE(app->url_entry), 40);
|
|
|
|
|
g_signal_connect(app->url_entry, "activate", G_CALLBACK(on_url_activate), app);
|
|
|
|
|
gtk_header_bar_set_title_widget(GTK_HEADER_BAR(app->header), app->url_entry);
|
|
|
|
|
gtk_window_set_titlebar(GTK_WINDOW(app->window), app->header);
|
|
|
|
|
#else
|
|
|
|
|
app->header = NULL; /* no bar built (window uses GTK's default titlebar) */
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
|
|
|
|
|
|
|
|
app->progress = gtk_progress_bar_new();
|
|
|
|
|
gtk_widget_set_visible(app->progress, FALSE);
|
|
|
|
|
gtk_box_append(GTK_BOX(box), app->progress);
|
|
|
|
|
|
2026-08-13 16:22:36 +08:00
|
|
|
GtkWidget *find_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
|
|
|
|
gtk_widget_set_margin_start(find_row, 6);
|
|
|
|
|
gtk_widget_set_margin_end(find_row, 6);
|
|
|
|
|
gtk_widget_set_margin_top(find_row, 4);
|
|
|
|
|
gtk_widget_set_margin_bottom(find_row, 4);
|
|
|
|
|
|
|
|
|
|
app->find_entry = gtk_entry_new();
|
|
|
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(app->find_entry), "Find in page\u2026");
|
|
|
|
|
gtk_widget_set_hexpand(app->find_entry, TRUE);
|
|
|
|
|
|
|
|
|
|
app->find_count_label = gtk_label_new("");
|
|
|
|
|
gtk_widget_set_size_request(app->find_count_label, 90, -1);
|
|
|
|
|
|
|
|
|
|
GtkWidget *find_prev_btn = gtk_button_new_from_icon_name("go-up-symbolic");
|
|
|
|
|
GtkWidget *find_next_btn = gtk_button_new_from_icon_name("go-down-symbolic");
|
|
|
|
|
GtkWidget *find_close_btn = gtk_button_new_from_icon_name("window-close-symbolic");
|
|
|
|
|
gtk_widget_set_tooltip_text(find_prev_btn, "Previous match (Shift+F3)");
|
|
|
|
|
gtk_widget_set_tooltip_text(find_next_btn, "Next match (F3)");
|
|
|
|
|
|
|
|
|
|
gtk_box_append(GTK_BOX(find_row), app->find_entry);
|
|
|
|
|
gtk_box_append(GTK_BOX(find_row), app->find_count_label);
|
|
|
|
|
gtk_box_append(GTK_BOX(find_row), find_prev_btn);
|
|
|
|
|
gtk_box_append(GTK_BOX(find_row), find_next_btn);
|
|
|
|
|
gtk_box_append(GTK_BOX(find_row), find_close_btn);
|
|
|
|
|
|
|
|
|
|
app->find_bar = gtk_revealer_new();
|
|
|
|
|
gtk_revealer_set_transition_type(GTK_REVEALER(app->find_bar), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
|
|
|
|
gtk_revealer_set_child(GTK_REVEALER(app->find_bar), find_row);
|
|
|
|
|
gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), FALSE);
|
|
|
|
|
gtk_widget_set_visible(app->find_bar, FALSE);
|
|
|
|
|
gtk_box_append(GTK_BOX(box), app->find_bar);
|
|
|
|
|
|
|
|
|
|
g_signal_connect(app->find_entry, "changed", G_CALLBACK(on_find_entry_changed), app);
|
|
|
|
|
g_signal_connect(app->find_entry, "activate", G_CALLBACK(on_find_entry_activate), app);
|
|
|
|
|
g_signal_connect(find_prev_btn, "clicked", G_CALLBACK(on_find_prev_clicked), app);
|
|
|
|
|
g_signal_connect(find_next_btn, "clicked", G_CALLBACK(on_find_next_clicked), app);
|
|
|
|
|
g_signal_connect(find_close_btn, "clicked", G_CALLBACK(on_find_close_clicked), app);
|
|
|
|
|
|
2026-08-12 18:48:46 +08:00
|
|
|
/* cookies/cache/history/logins/whatever persist under DATA_DIR */
|
|
|
|
|
WebKitNetworkSession *session = build_network_session();
|
2026-08-13 16:22:36 +08:00
|
|
|
g_signal_connect(session, "download-started", G_CALLBACK(on_download_started), app);
|
2026-08-12 18:48:46 +08:00
|
|
|
app->web_view = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
|
|
|
|
|
"network-session", session,
|
|
|
|
|
NULL));
|
|
|
|
|
gtk_widget_set_vexpand(app->web_view, TRUE);
|
|
|
|
|
gtk_widget_set_hexpand(app->web_view, TRUE);
|
|
|
|
|
|
|
|
|
|
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
webkit_settings_set_enable_developer_extras(settings, ENABLE_DEV_TOOLS);
|
|
|
|
|
|
2026-08-12 19:05:30 +08:00
|
|
|
webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(app->web_view), ZOOM_DEFAULT);
|
|
|
|
|
|
2026-08-12 18:48:46 +08:00
|
|
|
g_signal_connect(app->web_view, "load-changed", G_CALLBACK(on_load_changed), app);
|
|
|
|
|
g_signal_connect(app->web_view, "notify::estimated-load-progress",
|
|
|
|
|
G_CALLBACK(on_progress_changed), app);
|
|
|
|
|
g_signal_connect(app->web_view, "notify::title", G_CALLBACK(on_title_changed), app);
|
|
|
|
|
|
2026-08-13 16:22:36 +08:00
|
|
|
WebKitFindController *fctrl = webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(app->web_view));
|
|
|
|
|
g_signal_connect(fctrl, "found-text", G_CALLBACK(on_found_text), app);
|
|
|
|
|
g_signal_connect(fctrl, "failed-to-find-text", G_CALLBACK(on_failed_to_find_text), app);
|
|
|
|
|
|
2026-08-12 18:48:46 +08:00
|
|
|
gtk_box_append(GTK_BOX(box), app->web_view);
|
|
|
|
|
gtk_window_set_child(GTK_WINDOW(app->window), box);
|
|
|
|
|
|
|
|
|
|
GtkEventController *key_ctrl = gtk_event_controller_key_new();
|
|
|
|
|
g_signal_connect(key_ctrl, "key-pressed", G_CALLBACK(on_key_pressed), app);
|
|
|
|
|
gtk_widget_add_controller(app->window, key_ctrl);
|
|
|
|
|
|
|
|
|
|
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(app->web_view), start_uri);
|
|
|
|
|
#if ENABLE_URL_BAR
|
|
|
|
|
gtk_editable_set_text(GTK_EDITABLE(app->url_entry), start_uri);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
gtk_widget_set_visible(app->window, TRUE);
|
|
|
|
|
gtk_widget_grab_focus(app->web_view);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void print_usage(const char *progname) {
|
|
|
|
|
g_print(
|
|
|
|
|
"Usage: %s [OPTIONS] [URL | SEARCH TERMS]\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"A minimal WebKitGTK viewer\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" With no argument, opens DEFAULT_URI (see config.h).\n"
|
|
|
|
|
" With a URL-like argument, opens it directly.\n"
|
|
|
|
|
" With anything else, searches via SEARCH_URL_PREFIX (see config.h).\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Options:\n"
|
|
|
|
|
" -h, --help Show this help and exit\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Keybindings, window size, data directory, and search engine are\n"
|
|
|
|
|
"all configured at compile time in config.h (thanks, captain obvious).\n",
|
|
|
|
|
progname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
|
|
|
if (g_strcmp0(argv[i], "-h") == 0 || g_strcmp0(argv[i], "--help") == 0) {
|
|
|
|
|
print_usage(argv[0]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gchar *start_uri = (argc > 1) ? normalize_input(argv[1]) : g_strdup(DEFAULT_URI);
|
|
|
|
|
|
2026-08-15 00:33:25 +08:00
|
|
|
GtkApplication *gtk_app = gtk_application_new("dev.page.viewer", G_APPLICATION_NON_UNIQUE);
|
2026-08-12 18:48:46 +08:00
|
|
|
g_signal_connect(gtk_app, "activate", G_CALLBACK(activate), start_uri);
|
|
|
|
|
|
|
|
|
|
int status = g_application_run(G_APPLICATION(gtk_app), 1, argv);
|
|
|
|
|
g_object_unref(gtk_app);
|
|
|
|
|
g_free(start_uri);
|
|
|
|
|
return status;
|
|
|
|
|
}
|