diff --git a/src/platform.h b/src/platform.h index 8ad5cee..767166f 100644 --- a/src/platform.h +++ b/src/platform.h @@ -94,6 +94,13 @@ struct platform { * is called. */ void (*commit)(); + + /* + * Display an error modal dialog to the user. + * This function should block until the user dismisses the dialog + * or a timeout occurs (recommended: 10 seconds). + */ + void (*show_error_modal)(const char *title, const char *message); }; void platform_run(int (*main) (struct platform *platform)); diff --git a/src/platform/linux/X/X.c b/src/platform/linux/X/X.c index d1c0dd3..0a89fea 100644 --- a/src/platform/linux/X/X.c +++ b/src/platform/linux/X/X.c @@ -201,7 +201,9 @@ void x_init(struct platform *platform) { dpy = XOpenDisplay(NULL); if (!dpy) { - fprintf(stderr, "Could not connect to X server\n"); + /* Since we can't show a modal without X11, just use fprintf */ + fprintf(stderr, "ERROR: Could not connect to X server\n"); + fprintf(stderr, "Please make sure X11 is running and DISPLAY is set correctly.\n"); exit(-1); } @@ -231,4 +233,104 @@ void x_init(struct platform *platform) platform->screen_get_dimensions = x_screen_get_dimensions; platform->screen_list = x_screen_list; platform->scroll = x_scroll; + platform->show_error_modal = x_show_error_modal; +} + +void x_show_error_modal(const char *title, const char *message) +{ + if (!dpy) + return; + + Window root = DefaultRootWindow(dpy); + int screen = DefaultScreen(dpy); + + /* Create a simple modal window */ + int win_w = 400; + int win_h = 150; + int scr_w = DisplayWidth(dpy, screen); + int scr_h = DisplayHeight(dpy, screen); + int x = (scr_w - win_w) / 2; + int y = (scr_h - win_h) / 2; + + Window win = XCreateSimpleWindow(dpy, root, x, y, win_w, win_h, 2, + BlackPixel(dpy, screen), + WhitePixel(dpy, screen)); + + /* Set window properties */ + XStoreName(dpy, win, title); + XSelectInput(dpy, win, ExposureMask | KeyPressMask | ButtonPressMask); + + /* Set window type to dialog */ + Atom wm_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + Atom wm_type_dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + XChangeProperty(dpy, win, wm_type, XA_ATOM, 32, PropModeReplace, + (unsigned char *)&wm_type_dialog, 1); + + /* Set it to be on top */ + Atom wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); + Atom wm_state_above = XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False); + XChangeProperty(dpy, win, wm_state, XA_ATOM, 32, PropModeReplace, + (unsigned char *)&wm_state_above, 1); + + XMapWindow(dpy, win); + XRaiseWindow(dpy, win); + XFlush(dpy); + + /* Draw the message */ + GC gc = XCreateGC(dpy, win, 0, NULL); + XSetForeground(dpy, gc, BlackPixel(dpy, screen)); + + /* Simple text rendering - split message into lines */ + int text_y = 30; + const char *line_start = message; + char line_buf[256]; + + while (*line_start) { + const char *line_end = strchr(line_start, '\n'); + int line_len; + + if (line_end) { + line_len = line_end - line_start; + if (line_len > 255) line_len = 255; + strncpy(line_buf, line_start, line_len); + line_buf[line_len] = '\0'; + line_start = line_end + 1; + } else { + strncpy(line_buf, line_start, 255); + line_buf[255] = '\0'; + line_start += strlen(line_start); + } + + XDrawString(dpy, win, gc, 10, text_y, line_buf, strlen(line_buf)); + text_y += 20; + + if (!*line_start) + break; + } + + XDrawString(dpy, win, gc, 10, win_h - 20, "Press any key or click to dismiss", 34); + XFlush(dpy); + + /* Wait for user input with 10 second timeout */ + struct timeval tv; + fd_set fds; + int x11_fd = ConnectionNumber(dpy); + + tv.tv_sec = 10; + tv.tv_usec = 0; + + FD_ZERO(&fds); + FD_SET(x11_fd, &fds); + + int ret = select(x11_fd + 1, &fds, NULL, NULL, &tv); + + if (ret > 0) { + /* Wait for any event */ + XEvent ev; + XNextEvent(dpy, &ev); + } + + XFreeGC(dpy, gc); + XDestroyWindow(dpy, win); + XFlush(dpy); } diff --git a/src/platform/linux/X/X.h b/src/platform/linux/X/X.h index 83202cd..8202106 100644 --- a/src/platform/linux/X/X.h +++ b/src/platform/linux/X/X.h @@ -105,6 +105,7 @@ void x_copy_selection(); void x_commit(); void x_monitor_file(const char *path); long x_get_mtime(const char *path); +void x_show_error_modal(const char *title, const char *message); extern struct monitored_file monitored_files[32]; extern size_t nr_monitored_files; diff --git a/src/platform/linux/X/input.c b/src/platform/linux/X/input.c index 1f7c09c..533bee4 100644 --- a/src/platform/linux/X/input.c +++ b/src/platform/linux/X/input.c @@ -141,9 +141,15 @@ static uint8_t process_xinput_event(XEvent *ev, int *state, int *mods) static const char *xerr_key = NULL; static int input_xerr(Display *dpy, XErrorEvent *ev) { - fprintf(stderr, - "ERROR: Failed to grab %s (ensure it isn't mapped by another application)\n", + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), + "Failed to grab key: %s\n\n" + "Make sure it isn't mapped by another application.", xerr_key); + + /* Show error modal */ + x_show_error_modal("Keyboard Grab Failed", error_msg); + return 0; } diff --git a/src/platform/linux/wayland/wayland.c b/src/platform/linux/wayland/wayland.c index d479bb6..fca8c8c 100644 --- a/src/platform/linux/wayland/wayland.c +++ b/src/platform/linux/wayland/wayland.c @@ -235,4 +235,93 @@ void wayland_init(struct platform *platform) platform->screen_get_dimensions = way_screen_get_dimensions; platform->screen_list = way_screen_list; platform->scroll = way_scroll; + platform->show_error_modal = way_show_error_modal; +} + +void way_show_error_modal(const char *title, const char *message) +{ + /* For Wayland, we'll create a simple overlay window with the error message */ + if (nr_screens == 0 || !wl.dpy) + return; + + struct screen *scr = &screens[0]; + int win_w = 400; + int win_h = 150; + int x = (scr->w - win_w) / 2; + int y = (scr->h - win_h) / 2; + + /* Create a surface for the error modal */ + struct surface *sfc = create_surface(scr, x, y, win_w, win_h, 1); + if (!sfc) + return; + + /* Get cairo context from screen */ + cairo_t *cr = scr->cr; + if (!cr) + return; + + /* Draw white background */ + cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); + cairo_rectangle(cr, x, y, win_w, win_h); + cairo_fill(cr); + + /* Draw border */ + cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); + cairo_set_line_width(cr, 2.0); + cairo_rectangle(cr, x, y, win_w, win_h); + cairo_stroke(cr); + + /* Draw title */ + cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); + cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(cr, 14.0); + cairo_move_to(cr, x + 10, y + 25); + cairo_show_text(cr, title); + + /* Draw message */ + cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, 12.0); + + int text_y = y + 50; + const char *line_start = message; + char line_buf[256]; + + while (*line_start) { + const char *line_end = strchr(line_start, '\n'); + int line_len; + + if (line_end) { + line_len = line_end - line_start; + if (line_len > 255) line_len = 255; + strncpy(line_buf, line_start, line_len); + line_buf[line_len] = '\0'; + line_start = line_end + 1; + } else { + strncpy(line_buf, line_start, 255); + line_buf[255] = '\0'; + line_start += strlen(line_start); + } + + cairo_move_to(cr, x + 10, text_y); + cairo_show_text(cr, line_buf); + text_y += 20; + + if (!*line_start) + break; + } + + /* Draw dismiss hint */ + cairo_move_to(cr, x + 10, y + win_h - 20); + cairo_show_text(cr, "Will auto-dismiss in 10 seconds..."); + + surface_show(sfc); + wl_display_flush(wl.dpy); + + /* Wait for 10 seconds */ + sleep(10); + + destroy_surface(sfc); + wl_display_flush(wl.dpy); } diff --git a/src/platform/linux/wayland/wayland.h b/src/platform/linux/wayland/wayland.h index 022fc72..16a7575 100644 --- a/src/platform/linux/wayland/wayland.h +++ b/src/platform/linux/wayland/wayland.h @@ -126,5 +126,6 @@ void way_copy_selection(); void way_commit(); void way_init(); void init_input(); +void way_show_error_modal(const char *title, const char *message); #endif diff --git a/src/platform/macos/input.m b/src/platform/macos/input.m index 9ec38ca..f17add1 100644 --- a/src/platform/macos/input.m +++ b/src/platform/macos/input.m @@ -499,9 +499,11 @@ void macos_init_input() if (!tap) { - fprintf(stderr, - "Failed to create event tap, make sure warpd is " - "whitelisted as an accessibility feature.\n"); + /* Show error modal before exiting */ + osx_show_error_modal("Accessibility Permission Required", + "Failed to create event tap.\n\n" + "Please make sure warpd is whitelisted as an\n" + "accessibility feature in System Settings."); exit(-1); } diff --git a/src/platform/macos/macos.h b/src/platform/macos/macos.h index a42397f..ac55e68 100644 --- a/src/platform/macos/macos.h +++ b/src/platform/macos/macos.h @@ -124,5 +124,6 @@ void osx_copy_selection(); void osx_monitor_file(const char *_path); void osx_input_interrupt(); void osx_commit(); +void osx_show_error_modal(const char *title, const char *message); #endif diff --git a/src/platform/macos/macos.m b/src/platform/macos/macos.m index 962813e..e887c17 100644 --- a/src/platform/macos/macos.m +++ b/src/platform/macos/macos.m @@ -186,6 +186,7 @@ void osx_commit() .screen_list = osx_screen_list, .scroll = osx_scroll, .monitor_file = osx_monitor_file, + .show_error_modal = osx_show_error_modal, }; main(&platform); @@ -208,3 +209,21 @@ void platform_run(int (*main)(struct platform *platform)) [NSApp run]; } + +void osx_show_error_modal(const char *title, const char *message) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithUTF8String:title]]; + [alert setInformativeText:[NSString stringWithUTF8String:message]]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert addButtonWithTitle:@"OK"]; + + /* Run the alert modally with a timeout */ + [alert runModal]; + }); + + /* Give the modal time to display */ + sleep(1); +} + diff --git a/src/platform/windows/windows.c b/src/platform/windows/windows.c index 8bc98b6..8512f9a 100755 --- a/src/platform/windows/windows.c +++ b/src/platform/windows/windows.c @@ -472,6 +472,13 @@ void platform_run(int (*main)(struct platform *platform)) platform.input_lookup_code = input_lookup_code; platform.input_lookup_name = input_lookup_name; platform.monitor_file = wn_monitor_file; + platform.show_error_modal = wn_show_error_modal; exit(main(&platform)); } + +void wn_show_error_modal(const char *title, const char *message) +{ + /* Use Windows MessageBox API for error display */ + MessageBoxA(NULL, message, title, MB_OK | MB_ICONERROR | MB_TOPMOST); +} diff --git a/src/platform/windows/windows.h b/src/platform/windows/windows.h index 9ff4c5c..a3ce691 100755 --- a/src/platform/windows/windows.h +++ b/src/platform/windows/windows.h @@ -22,5 +22,6 @@ void wn_screen_get_dimensions(struct screen *scr, int *xoff, int *yoff, int *w, void wn_screen_set_hints(struct screen *scr, struct hint *hints, size_t nhints); void wn_screen_set_hintinfo(COLORREF _hint_bgcol, COLORREF _hint_fgcol); void wn_monitor_file(const char *path); +void wn_show_error_modal(const char *title, const char *message); #endif diff --git a/src/warpd.c b/src/warpd.c index 3f14701..e39b9e5 100644 --- a/src/warpd.c +++ b/src/warpd.c @@ -18,6 +18,17 @@ uint64_t get_time_us() return ts.tv_nsec / 1E3 + ts.tv_sec * 1E6; } +void show_error_modal(const char *title, const char *message) +{ + /* Log to stderr as well for debugging/logging purposes */ + fprintf(stderr, "ERROR: %s: %s\n", title, message); + + /* Show modal if platform is initialized and supports it */ + if (platform && platform->show_error_modal) { + platform->show_error_modal(title, message); + } +} + const char *get_data_path(const char *file) { diff --git a/src/warpd.h b/src/warpd.h index 258c8d4..3173224 100644 --- a/src/warpd.h +++ b/src/warpd.h @@ -148,5 +148,7 @@ extern struct config_entry *config; int mode_loop(int initial_mode, int oneshot, int record_history); void daemon_loop(const char *config_path); +void show_error_modal(const char *title, const char *message); + extern struct platform *platform; #endif