2 * GTK implementation of a GUI password/passphrase prompt.
13 #if !GTK_CHECK_VERSION(3,0,0)
14 #include <gdk/gdkkeysyms.h>
18 #include "gtkcompat.h"
23 #define N_DRAWING_AREAS 3
25 struct drawing_area_ctx {
27 #ifndef DRAW_DEFAULT_CAIRO
30 int width, height, current;
34 GtkWidget *dialog, *promptlabel;
35 struct drawing_area_ctx drawingareas[N_DRAWING_AREAS];
37 #if GTK_CHECK_VERSION(2,0,0)
40 #ifndef DRAW_DEFAULT_CAIRO
45 int passlen, passsize;
46 #if GTK_CHECK_VERSION(3,20,0)
47 GdkSeat *seat; /* for gdk_seat_grab */
48 #elif GTK_CHECK_VERSION(3,0,0)
49 GdkDevice *keyboard; /* for gdk_device_grab */
53 static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
56 new_active = rand() % (N_DRAWING_AREAS - 1);
57 if (new_active >= ctx->active_area)
59 ctx->drawingareas[ctx->active_area].current = 0;
60 gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
61 ctx->drawingareas[new_active].current = 1;
62 gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
63 ctx->active_area = new_active;
66 static int last_char_len(struct askpass_ctx *ctx)
69 * GTK always encodes in UTF-8, so we can do this in a fixed way.
72 assert(ctx->passlen > 0);
74 while ((unsigned)((unsigned char)ctx->passphrase[i] - 0x80) < 0x40) {
79 return ctx->passlen - i;
82 static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str)
84 int len = strlen(str);
85 if (ctx->passlen + len >= ctx->passsize) {
86 /* Take some care with buffer expansion, because there are
87 * pieces of passphrase in the old buffer so we should ensure
88 * realloc doesn't leave a copy lying around in the address
90 int oldsize = ctx->passsize;
93 ctx->passsize = (ctx->passlen + len) * 5 / 4 + 1024;
94 newbuf = snewn(ctx->passsize, char);
95 memcpy(newbuf, ctx->passphrase, oldsize);
96 smemclr(ctx->passphrase, oldsize);
97 sfree(ctx->passphrase);
98 ctx->passphrase = newbuf;
100 strcpy(ctx->passphrase + ctx->passlen, str);
102 visually_acknowledge_keypress(ctx);
105 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
107 struct askpass_ctx *ctx = (struct askpass_ctx *)data;
109 if (event->keyval == GDK_KEY_Return &&
110 event->type == GDK_KEY_PRESS) {
112 } else if (event->keyval == GDK_KEY_Escape &&
113 event->type == GDK_KEY_PRESS) {
114 smemclr(ctx->passphrase, ctx->passsize);
115 ctx->passphrase = NULL;
118 #if GTK_CHECK_VERSION(2,0,0)
119 if (gtk_im_context_filter_keypress(ctx->imc, event))
123 if (event->type == GDK_KEY_PRESS) {
124 if (!strcmp(event->string, "\x15")) {
125 /* Ctrl-U. Wipe out the whole line */
127 visually_acknowledge_keypress(ctx);
128 } else if (!strcmp(event->string, "\x17")) {
129 /* Ctrl-W. Delete back to the last space->nonspace
130 * boundary. We interpret 'space' in a really simple
131 * way (mimicking terminal drivers), and don't attempt
132 * to second-guess exciting Unicode space
134 while (ctx->passlen > 0) {
136 ctx->passlen -= last_char_len(ctx);
137 deleted = ctx->passphrase[ctx->passlen];
138 prior = (ctx->passlen == 0 ? ' ' :
139 ctx->passphrase[ctx->passlen-1]);
140 if (!g_ascii_isspace(deleted) && g_ascii_isspace(prior))
143 visually_acknowledge_keypress(ctx);
144 } else if (event->keyval == GDK_KEY_BackSpace) {
145 /* Backspace. Delete one character. */
146 if (ctx->passlen > 0)
147 ctx->passlen -= last_char_len(ctx);
148 visually_acknowledge_keypress(ctx);
149 #if !GTK_CHECK_VERSION(2,0,0)
150 } else if (event->string[0]) {
151 add_text_to_passphrase(ctx, event->string);
159 #if GTK_CHECK_VERSION(2,0,0)
160 static void input_method_commit_event(GtkIMContext *imc, gchar *str,
163 struct askpass_ctx *ctx = (struct askpass_ctx *)data;
164 add_text_to_passphrase(ctx, str);
168 static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
171 struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
172 ctx->width = event->width;
173 ctx->height = event->height;
174 gtk_widget_queue_draw(widget);
178 #ifdef DRAW_DEFAULT_CAIRO
179 static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
181 cairo_set_source_rgb(cr, 1-ctx->current, 1-ctx->current, 1-ctx->current);
185 static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
187 GdkGC *gc = gdk_gc_new(win);
188 gdk_gc_set_foreground(gc, &ctx->cols[ctx->current]);
189 gdk_draw_rectangle(win, gc, TRUE, 0, 0, ctx->width, ctx->height);
194 #if GTK_CHECK_VERSION(3,0,0)
195 static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
197 struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
198 askpass_redraw_cairo(cr, ctx);
202 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
205 struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
207 #ifdef DRAW_DEFAULT_CAIRO
208 cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(ctx->area));
209 askpass_redraw_cairo(cr, ctx);
212 askpass_redraw_gdk(gtk_widget_get_window(ctx->area), ctx);
219 static int try_grab_keyboard(struct askpass_ctx *ctx)
223 #if GTK_CHECK_VERSION(3,20,0)
225 * Grabbing the keyboard in GTK 3.20 requires the new notion of
230 seat = gdk_display_get_default_seat
231 (gtk_widget_get_display(ctx->dialog));
236 ret = gdk_seat_grab(seat, gtk_widget_get_window(ctx->dialog),
237 GDK_SEAT_CAPABILITY_KEYBOARD,
238 TRUE, NULL, NULL, NULL, NULL);
239 #elif GTK_CHECK_VERSION(3,0,0)
241 * And it has to be done differently again prior to GTK 3.20.
243 GdkDeviceManager *dm;
244 GdkDevice *pointer, *keyboard;
246 dm = gdk_display_get_device_manager
247 (gtk_widget_get_display(ctx->dialog));
251 pointer = gdk_device_manager_get_client_pointer(dm);
254 keyboard = gdk_device_get_associated_device(pointer);
257 if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
260 ctx->keyboard = keyboard;
261 ret = gdk_device_grab(ctx->keyboard,
262 gtk_widget_get_window(ctx->dialog),
265 GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
270 * It's much simpler in GTK 1 and 2!
272 ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
273 FALSE, GDK_CURRENT_TIME);
276 return ret == GDK_GRAB_SUCCESS;
279 typedef int (try_grab_fn_t)(struct askpass_ctx *ctx);
281 static int repeatedly_try_grab(struct askpass_ctx *ctx, try_grab_fn_t fn)
284 * Repeatedly try to grab some aspect of the X server. We have to
285 * do this rather than just trying once, because there is at least
286 * one important situation in which the grab may fail the first
287 * time: any user who is launching an add-key operation off some
288 * kind of window manager hotkey will almost by definition be
289 * running this script with a keyboard grab already active, namely
290 * the one-key grab that the WM (or whatever) uses to detect
291 * presses of the hotkey. So at the very least we have to give the
292 * user time to release that key.
294 const useconds_t ms_limit = 5*1000000; /* try for 5 seconds */
295 const useconds_t ms_step = 1000000/8; /* at 1/8 second intervals */
298 for (ms = 0; ms < ms_limit; ms += ms_step) {
306 static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
307 const char *window_title,
308 const char *prompt_text)
314 ctx->passsize = 2048;
315 ctx->passphrase = snewn(ctx->passsize, char);
320 ctx->dialog = our_dialog_new();
321 gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title);
322 gtk_window_set_position(GTK_WINDOW(ctx->dialog), GTK_WIN_POS_CENTER);
323 ctx->promptlabel = gtk_label_new(prompt_text);
324 align_label_left(GTK_LABEL(ctx->promptlabel));
325 gtk_widget_show(ctx->promptlabel);
326 gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), TRUE);
327 #if GTK_CHECK_VERSION(3,0,0)
328 gtk_label_set_width_chars(GTK_LABEL(ctx->promptlabel), 48);
330 our_dialog_add_to_content_area(GTK_WINDOW(ctx->dialog),
331 ctx->promptlabel, TRUE, TRUE, 0);
332 #if GTK_CHECK_VERSION(2,0,0)
333 ctx->imc = gtk_im_multicontext_new();
335 #ifndef DRAW_DEFAULT_CAIRO
338 ctx->colmap = gdk_colormap_get_system();
339 ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
340 ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
341 gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
342 FALSE, TRUE, success);
343 if (!success[0] | !success[1])
344 return "unable to allocate colours";
348 action_area = our_dialog_make_action_hbox(GTK_WINDOW(ctx->dialog));
350 for (i = 0; i < N_DRAWING_AREAS; i++) {
351 ctx->drawingareas[i].area = gtk_drawing_area_new();
352 #ifndef DRAW_DEFAULT_CAIRO
353 ctx->drawingareas[i].cols = ctx->cols;
355 ctx->drawingareas[i].current = 0;
356 ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
357 /* It would be nice to choose this size in some more
358 * context-sensitive way, like measuring the size of some
359 * piece of template text. */
360 gtk_widget_set_size_request(ctx->drawingareas[i].area, 32, 32);
361 gtk_box_pack_end(action_area, ctx->drawingareas[i].area,
363 g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
365 G_CALLBACK(configure_area),
366 &ctx->drawingareas[i]);
367 #if GTK_CHECK_VERSION(3,0,0)
368 g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
370 G_CALLBACK(draw_area),
371 &ctx->drawingareas[i]);
373 g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
375 G_CALLBACK(expose_area),
376 &ctx->drawingareas[i]);
379 #if GTK_CHECK_VERSION(3,0,0)
380 g_object_set(G_OBJECT(ctx->drawingareas[i].area),
381 "margin-bottom", 8, (const char *)NULL);
384 gtk_widget_show(ctx->drawingareas[i].area);
386 ctx->active_area = rand() % N_DRAWING_AREAS;
387 ctx->drawingareas[ctx->active_area].current = 1;
390 * Arrange to receive key events. We don't really need to worry
391 * from a UI perspective about which widget gets the events, as
392 * long as we know which it is so we can catch them. So we'll pick
393 * the prompt label at random, and we'll use gtk_grab_add to
394 * ensure key events go to it.
396 gtk_widget_set_sensitive(ctx->promptlabel, TRUE);
398 #if GTK_CHECK_VERSION(2,0,0)
399 gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), TRUE);
403 * Actually show the window, and wait for it to be shown.
405 gtk_widget_show_now(ctx->dialog);
408 * Now that the window is displayed, make it grab the input focus.
410 gtk_grab_add(ctx->promptlabel);
411 if (!repeatedly_try_grab(ctx, try_grab_keyboard))
412 return "unable to grab keyboard";
415 * And now that we've got the keyboard grab, connect up our
418 #if GTK_CHECK_VERSION(2,0,0)
419 g_signal_connect(G_OBJECT(ctx->imc), "commit",
420 G_CALLBACK(input_method_commit_event), ctx);
422 g_signal_connect(G_OBJECT(ctx->promptlabel), "key_press_event",
423 G_CALLBACK(key_event), ctx);
424 g_signal_connect(G_OBJECT(ctx->promptlabel), "key_release_event",
425 G_CALLBACK(key_event), ctx);
426 #if GTK_CHECK_VERSION(2,0,0)
427 gtk_im_context_set_client_window(ctx->imc,
428 gtk_widget_get_window(ctx->dialog));
434 static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
436 #if GTK_CHECK_VERSION(3,20,0)
437 gdk_seat_ungrab(ctx->seat);
438 #elif GTK_CHECK_VERSION(3,0,0)
439 gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
441 gdk_keyboard_ungrab(GDK_CURRENT_TIME);
443 gtk_grab_remove(ctx->promptlabel);
445 if (ctx->passphrase) {
446 assert(ctx->passlen < ctx->passsize);
447 ctx->passphrase[ctx->passlen] = '\0';
450 gtk_widget_destroy(ctx->dialog);
453 static int setup_gtk(const char *display)
455 static int gtk_initialised = FALSE;
458 char **argv = real_argv;
465 argv[argc++] = dupstr("dummy");
466 argv[argc++] = dupprintf("--display=%s", display);
468 ret = gtk_init_check(&argc, &argv);
472 gtk_initialised = ret;
476 char *gtk_askpass_main(const char *display, const char *wintitle,
477 const char *prompt, int *success)
479 struct askpass_ctx actx, *ctx = &actx;
482 /* In case gtk_init hasn't been called yet by the program */
483 if (!setup_gtk(display)) {
485 return dupstr("unable to initialise GTK");
488 if ((err = gtk_askpass_setup(ctx, wintitle, prompt)) != NULL) {
490 return dupprintf("%s", err);
493 gtk_askpass_cleanup(ctx);
495 if (ctx->passphrase) {
497 return ctx->passphrase;
500 return dupstr("passphrase input cancelled");
505 void modalfatalbox(const char *p, ...)
508 fprintf(stderr, "FATAL ERROR: ");
510 vfprintf(stderr, p, ap);
516 int main(int argc, char **argv)
518 int success, exitcode;
521 gtk_init(&argc, &argv);
525 ret = dupprintf("usage: %s <prompt text>", argv[0]);
528 ret = gtk_askpass_main(NULL, "Enter passphrase", argv[1], &success);
541 smemclr(ret, strlen(ret));