]> git.sur5r.net Git - i3/i3/blob - i3bar/src/xcb.c
Make hide_on_modifier configurable
[i3/i3] / i3bar / src / xcb.c
1 /*
2  * i3bar - an xcb-based status- and ws-bar for i3
3  *
4  * © 2010 Axel Wagner and contributors
5  *
6  * See file LICNSE for license information
7  *
8  * src/xcb.c: Communicating with X
9  *
10  */
11 #include <xcb/xcb.h>
12 #include <xcb/xproto.h>
13 #include <xcb/xcb_event.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <fcntl.h>
18 #include <string.h>
19 #include <i3/ipc.h>
20 #include <ev.h>
21
22 #include <X11/Xlib.h>
23 #include <X11/XKBlib.h>
24 #include <X11/extensions/XKB.h>
25
26 #include "common.h"
27
28 /* We save the Atoms in an easy to access array, indexed by an enum */
29 #define NUM_ATOMS 3
30
31 enum {
32     #define ATOM_DO(name) name,
33     #include "xcb_atoms.def"
34 };
35
36 xcb_intern_atom_cookie_t atom_cookies[NUM_ATOMS];
37 xcb_atom_t               atoms[NUM_ATOMS];
38
39 /* Variables, that are the same for all functions at all times */
40 xcb_connection_t *xcb_connection;
41 xcb_screen_t     *xcb_screens;
42 xcb_window_t     xcb_root;
43 xcb_font_t       xcb_font;
44
45 Display          *xkb_dpy;
46 int              xkb_event_base;
47 int              mod_pressed;
48
49 /* Event-Watchers, to interact with the user */
50 ev_prepare *xcb_prep;
51 ev_check   *xcb_chk;
52 ev_io      *xcb_io;
53 ev_io      *xkb_io;
54
55 /*
56  * Converts a colorstring to a colorpixel as expected from xcb_change_gc.
57  * s is assumed to be in the format "rrggbb"
58  *
59  */
60 uint32_t get_colorpixel(const char *s) {
61     char strings[3][3] = { { s[0], s[1], '\0'} ,
62                            { s[2], s[3], '\0'} ,
63                            { s[4], s[5], '\0'} };
64     uint8_t r = strtol(strings[0], NULL, 16);
65     uint8_t g = strtol(strings[1], NULL, 16);
66     uint8_t b = strtol(strings[2], NULL, 16);
67     return (r << 16 | g << 8 | b);
68 }
69
70 /*
71  * Hides all bars (unmaps them)
72  *
73  */
74 void hide_bars() {
75     if (!config.hide_on_modifier) {
76         return;
77     }
78
79     i3_output *walk;
80     SLIST_FOREACH(walk, outputs, slist) {
81         xcb_unmap_window(xcb_connection, walk->bar);
82     }
83     stop_child();
84 }
85
86 /*
87  * Unhides all bars (maps them)
88  *
89  */
90 void unhide_bars() {
91     if (!config.hide_on_modifier) {
92         return;
93     }
94
95     i3_output           *walk;
96     xcb_void_cookie_t   cookie;
97     xcb_generic_error_t *err;
98     uint32_t            mask;
99     uint32_t            values[5];
100
101     cont_child();
102
103     SLIST_FOREACH(walk, outputs, slist) {
104         if (walk->bar == XCB_NONE) {
105             continue;
106         }
107         mask = XCB_CONFIG_WINDOW_X |
108                XCB_CONFIG_WINDOW_Y |
109                XCB_CONFIG_WINDOW_WIDTH |
110                XCB_CONFIG_WINDOW_HEIGHT |
111                XCB_CONFIG_WINDOW_STACK_MODE;
112         values[0] = walk->rect.x;
113         values[1] = walk->rect.y + walk->rect.h - font_height - 6;
114         values[2] = walk->rect.w;
115         values[3] = font_height + 6;
116         values[4] = XCB_STACK_MODE_ABOVE;
117         printf("Reconfiguring Window for output %s to %d,%d\n", walk->name, values[0], values[1]);
118         cookie = xcb_configure_window_checked(xcb_connection,
119                                               walk->bar,
120                                               mask,
121                                               values);
122
123         if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
124             printf("ERROR: Could not reconfigure window. XCB-errorcode: %d\n", err->error_code);
125             exit(EXIT_FAILURE);
126         }
127         xcb_map_window(xcb_connection, walk->bar);
128     }
129 }
130
131 /*
132  * Handle a button-press-event (i.c. a mouse click on one of our bars).
133  * We determine, wether the click occured on a ws-button or if the scroll-
134  * wheel was used and change the workspace appropriately
135  *
136  */
137 void handle_button(xcb_button_press_event_t *event) {
138     i3_ws *cur_ws;
139
140     /* Determine, which bar was clicked */
141     i3_output *walk;
142     xcb_window_t bar = event->event;
143     SLIST_FOREACH(walk, outputs, slist) {
144         if (walk->bar == bar) {
145             break;
146         }
147     }
148
149     if (walk == NULL) {
150         printf("Unknown Bar klicked!\n");
151         return;
152     }
153
154     /* TODO: Move this to exern get_ws_for_output() */
155     TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
156         if (cur_ws->visible) {
157             break;
158         }
159     }
160
161     if (cur_ws == NULL) {
162         printf("No Workspace active?\n");
163         return;
164     }
165
166     int32_t x = event->event_x;
167
168     printf("Got Button %d\n", event->detail);
169
170     switch (event->detail) {
171         case 1:
172             /* Left Mousbutton. We determine, which button was clicked
173              * and set cur_ws accordingly */
174             TAILQ_FOREACH(cur_ws, walk->workspaces, tailq) {
175                 printf("x = %d\n", x);
176                 if (x < cur_ws->name_width + 10) {
177                     break;
178                 }
179                 x -= cur_ws->name_width + 10;
180             }
181             if (cur_ws == NULL) {
182                 return;
183             }
184             break;
185         case 4:
186             /* Mouse wheel down. We select the next ws */
187             if (cur_ws == TAILQ_FIRST(walk->workspaces)) {
188                 cur_ws = TAILQ_LAST(walk->workspaces, ws_head);
189             } else {
190                 cur_ws = TAILQ_PREV(cur_ws, ws_head, tailq);
191             }
192             break;
193         case 5:
194             /* Mouse wheel up. We select the previos ws */
195             if (cur_ws == TAILQ_LAST(walk->workspaces, ws_head)) {
196                 cur_ws = TAILQ_FIRST(walk->workspaces);
197             } else {
198                 cur_ws = TAILQ_NEXT(cur_ws, tailq);
199             }
200             break;
201     }
202
203     char buffer[50];
204     snprintf(buffer, 50, "%d", cur_ws->num);
205     i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, buffer);
206 }
207
208 /*
209  * This function is called immediately bevor the main loop locks. We flush xcb
210  * then (and only then)
211  *
212  */
213 void xcb_prep_cb(struct ev_loop *loop, ev_prepare *watcher, int revenst) {
214     xcb_flush(xcb_connection);
215 }
216
217 /*
218  * This function is called immediately after the main loop locks, so when one
219  * of the watchers registered an event.
220  * We check wether an X-Event arrived and handle it.
221  *
222  */
223 void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) {
224     xcb_generic_event_t *event;
225     while ((event = xcb_poll_for_event(xcb_connection)) == NULL) {
226         return;
227     }
228
229     switch (event->response_type & ~0x80) {
230         case XCB_EXPOSE:
231             /* Expose-events happen, when the window needs to be redrawn */
232             draw_bars();
233             break;
234         case XCB_BUTTON_PRESS:
235             /* Button-press-events are mouse-buttons clicked on one of our bars */
236             handle_button((xcb_button_press_event_t*) event);
237             break;
238     }
239     FREE(event);
240 }
241
242 /*
243  * Dummy Callback. We only need this, so that the Prepare- and Check-Watchers
244  * are triggered
245  *
246  */
247 void xcb_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
248 }
249
250 /*
251  * We need to bind to the modifier per XKB. Sadly, XCB does not implement this
252  *
253  */
254 void xkb_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) {
255     XkbEvent ev;
256     int modstate;
257
258     printf("Got XKB-Event!\n");
259
260     while (XPending(xkb_dpy)) {
261         XNextEvent(xkb_dpy, (XEvent*)&ev);
262
263         if (ev.type != xkb_event_base) {
264             printf("ERROR: No Xkb-Event!\n");
265             continue;
266         }
267
268         if (ev.any.xkb_type != XkbStateNotify) {
269             printf("ERROR: No State Notify!\n");
270             continue;
271         }
272
273         unsigned int mods = ev.state.mods;
274         modstate = mods & Mod4Mask;
275     }
276
277     if (modstate != mod_pressed) {
278         if (modstate == 0) {
279             printf("Mod4 got released!\n");
280             hide_bars();
281         } else {
282             printf("Mod4 got pressed!\n");
283             unhide_bars();
284         }
285         mod_pressed = modstate;
286     }
287 }
288
289 /*
290  * Calculate the rendered width of a string with the configured font.
291  * The string has to be encoded in ucs2 and glyph_len has to be the length
292  * of the string (in width)
293  *
294  */
295 int get_string_width(xcb_char2b_t *string, int glyph_len) {
296     xcb_query_text_extents_cookie_t cookie;
297     xcb_query_text_extents_reply_t *reply;
298     xcb_generic_error_t *error = NULL;
299     int width;
300
301     cookie = xcb_query_text_extents(xcb_connection, xcb_font, glyph_len, string);
302     reply = xcb_query_text_extents_reply(xcb_connection, cookie, &error);
303     if (error != NULL) {
304         printf("ERROR: Could not get text extents! XCB-errorcode: %d\n", error->error_code);
305         exit(EXIT_FAILURE);
306     }
307
308     width = reply->overall_width;
309     free(reply);
310     return width;
311 }
312
313 /*
314  * Initialize xcb and use the specified fontname for text-rendering
315  *
316  */
317 void init_xcb(char *fontname) {
318     /* FIXME: xcb_connect leaks Memory */
319     xcb_connection = xcb_connect(NULL, NULL);
320     if (xcb_connection_has_error(xcb_connection)) {
321         printf("Cannot open display\n");
322         exit(EXIT_FAILURE);
323     }
324     printf("Connected to xcb\n");
325
326     /* We have to request the atoms we need */
327     #define ATOM_DO(name) atom_cookies[name] = xcb_intern_atom(xcb_connection, 0, strlen(#name), #name);
328     #include "xcb_atoms.def"
329
330     xcb_screens = xcb_setup_roots_iterator(xcb_get_setup(xcb_connection)).data;
331     xcb_root = xcb_screens->root;
332
333     /* We load and allocate the font */
334     xcb_font = xcb_generate_id(xcb_connection);
335     xcb_void_cookie_t open_font_cookie;
336     open_font_cookie = xcb_open_font_checked(xcb_connection,
337                                              xcb_font,
338                                              strlen(fontname),
339                                              fontname);
340
341     xcb_generic_error_t *err = xcb_request_check(xcb_connection,
342                                                  open_font_cookie);
343
344     if (err != NULL) {
345         printf("ERROR: Could not open font! XCB-Error-Code: %d\n", err->error_code);
346         exit(EXIT_FAILURE);
347     }
348
349     /* We also need the fontheight to configure our bars accordingly */
350     xcb_list_fonts_with_info_cookie_t font_info_cookie;
351     font_info_cookie = xcb_list_fonts_with_info(xcb_connection,
352                                                 1,
353                                                 strlen(fontname),
354                                                 fontname);
355
356     if (config.hide_on_modifier) {
357         int xkb_major, xkb_minor, xkb_errbase, xkb_err;
358         xkb_major = XkbMajorVersion;
359         xkb_minor = XkbMinorVersion;
360
361         xkb_dpy = XkbOpenDisplay(":0",
362                                  &xkb_event_base,
363                                  &xkb_errbase,
364                                  &xkb_major,
365                                  &xkb_minor,
366                                  &xkb_err);
367
368         if (xkb_dpy == NULL) {
369             printf("ERROR: No XKB!\n");
370             exit(EXIT_FAILURE);
371         }
372
373         if (fcntl(ConnectionNumber(xkb_dpy), F_SETFD, FD_CLOEXEC) == -1) {
374             fprintf(stderr, "Could not set FD_CLOEXEC on xkbdpy\n");
375             exit(EXIT_FAILURE);
376         }
377
378         int i1;
379         if (!XkbQueryExtension(xkb_dpy, &i1, &xkb_event_base, &xkb_errbase, &xkb_major, &xkb_minor)) {
380             printf("ERROR: XKB not supported by X-server!\n");
381             exit(EXIT_FAILURE);
382         }
383
384         if (!XkbSelectEvents(xkb_dpy, XkbUseCoreKbd, XkbStateNotifyMask, XkbStateNotifyMask)) {
385             printf("Could not grab Key!\n");
386             exit(EXIT_FAILURE);
387         }
388
389         xkb_io = malloc(sizeof(ev_io));
390         ev_io_init(xkb_io, &xkb_io_cb, ConnectionNumber(xkb_dpy), EV_READ);
391         ev_io_start(main_loop, xkb_io);
392         XFlush(xkb_dpy);
393     }
394
395     /* The varios Watchers to communicate with xcb */
396     xcb_io = malloc(sizeof(ev_io));
397     xcb_prep = malloc(sizeof(ev_prepare));
398     xcb_chk = malloc(sizeof(ev_check));
399
400     ev_io_init(xcb_io, &xcb_io_cb, xcb_get_file_descriptor(xcb_connection), EV_READ);
401     ev_prepare_init(xcb_prep, &xcb_prep_cb);
402     ev_check_init(xcb_chk, &xcb_chk_cb);
403
404     ev_io_start(main_loop, xcb_io);
405     ev_prepare_start(main_loop, xcb_prep);
406     ev_check_start(main_loop, xcb_chk);
407
408     /* Now we get the atoms and save them in a nice data-structure */
409     get_atoms();
410
411     /* Now we calculate the font-height */
412     xcb_list_fonts_with_info_reply_t *reply;
413     reply = xcb_list_fonts_with_info_reply(xcb_connection,
414                                            font_info_cookie,
415                                            NULL);
416     font_height = reply->font_ascent + reply->font_descent;
417     FREE(reply);
418
419     printf("Calculated Font-height: %d\n", font_height);
420 }
421
422 /*
423  * Cleanup the xcb-stuff.
424  * Called once, before the program terminates.
425  *
426  */
427 void clean_xcb() {
428     i3_output *walk;
429     SLIST_FOREACH(walk, outputs, slist) {
430         destroy_window(walk);
431     }
432     FREE_SLIST(outputs, i3_output);
433
434     xcb_disconnect(xcb_connection);
435
436     ev_check_stop(main_loop, xcb_chk);
437     ev_prepare_stop(main_loop, xcb_prep);
438     ev_io_stop(main_loop, xcb_io);
439
440     FREE(xcb_chk);
441     FREE(xcb_prep);
442     FREE(xcb_io);
443 }
444
445 /*
446  * Get the earlier requested atoms and save them in the prepared data-structure
447  *
448  */
449 void get_atoms() {
450     xcb_intern_atom_reply_t *reply;
451     #define ATOM_DO(name) reply = xcb_intern_atom_reply(xcb_connection, atom_cookies[name], NULL); \
452         if (reply == NULL) { \
453             printf("ERROR: Could not get atom %s\n", #name); \
454             exit(EXIT_FAILURE); \
455         } \
456         atoms[name] = reply->atom; \
457         free(reply);
458
459     #include "xcb_atoms.def"
460     printf("Got Atoms\n");
461 }
462
463 /*
464  * Destroy the bar of the specified output
465  *
466  */
467 void destroy_window(i3_output *output) {
468     if (output == NULL) {
469         return;
470     }
471     if (output->bar == XCB_NONE) {
472         return;
473     }
474     xcb_destroy_window(xcb_connection, output->bar);
475     output->bar = XCB_NONE;
476 }
477
478 /*
479  * Reconfigure all bars and create new for newly activated outputs
480  *
481  */
482 void reconfig_windows() {
483     uint32_t mask;
484     uint32_t values[5];
485
486     xcb_void_cookie_t   cookie;
487     xcb_generic_error_t *err;
488
489     i3_output *walk;
490     SLIST_FOREACH(walk, outputs, slist) {
491         if (!walk->active) {
492             /* If an output is not active, we destroy it's bar */
493             /* FIXME: Maybe we rather want to unmap? */
494             printf("Destroying window for output %s\n", walk->name);
495             destroy_window(walk);
496             continue;
497         }
498         if (walk->bar == XCB_NONE) {
499             printf("Creating Window for output %s\n", walk->name);
500
501             walk->bar = xcb_generate_id(xcb_connection);
502             mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
503             /* Black background */
504             values[0] = xcb_screens->black_pixel;
505             /* If hide_on_modifier is set, i3 is not supposed to manage our bar-windows */
506             values[1] = config.hide_on_modifier;
507             /* The events we want to receive */
508             values[2] = XCB_EVENT_MASK_EXPOSURE |
509                         XCB_EVENT_MASK_BUTTON_PRESS;
510             cookie = xcb_create_window_checked(xcb_connection,
511                                                xcb_screens->root_depth,
512                                                walk->bar,
513                                                xcb_root,
514                                                walk->rect.x, walk->rect.y,
515                                                walk->rect.w, font_height + 6,
516                                                1,
517                                                XCB_WINDOW_CLASS_INPUT_OUTPUT,
518                                                xcb_screens->root_visual,
519                                                mask,
520                                                values);
521             if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
522                 printf("ERROR: Could not create Window. XCB-errorcode: %d\n", err->error_code);
523                 exit(EXIT_FAILURE);
524             }
525
526             /* We want dock-windows (for now) */
527             xcb_change_property(xcb_connection,
528                                 XCB_PROP_MODE_REPLACE,
529                                 walk->bar,
530                                 atoms[_NET_WM_WINDOW_TYPE],
531                                 atoms[ATOM],
532                                 32,
533                                 1,
534                                 (unsigned char*) &atoms[_NET_WM_WINDOW_TYPE_DOCK]);
535             if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
536                 printf("ERROR: Could not set dock mode. XCB-errorcode: %d\n", err->error_code);
537                 exit(EXIT_FAILURE);
538             }
539
540             /* We also want a graphics-context (the "canvas" on which we draw) */
541             walk->bargc = xcb_generate_id(xcb_connection);
542             mask = XCB_GC_FONT;
543             values[0] = xcb_font;
544             cookie = xcb_create_gc_checked(xcb_connection,
545                                            walk->bargc,
546                                            walk->bar,
547                                            mask,
548                                            values);
549
550             if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
551                 printf("ERROR: Could not create graphical context. XCB-errorcode: %d\n", err->error_code);
552                 exit(EXIT_FAILURE);
553             }
554
555             /* We finally map the bar (display it on screen) */
556             cookie = xcb_map_window_checked(xcb_connection, walk->bar);
557
558             if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
559                 printf("ERROR: Could not map window. XCB-errorcode: %d\n", err->error_code);
560                 exit(EXIT_FAILURE);
561             }
562         } else {
563             /* We already have a bar, so we just reconfigure it */
564             mask = XCB_CONFIG_WINDOW_X |
565                    XCB_CONFIG_WINDOW_Y |
566                    XCB_CONFIG_WINDOW_WIDTH |
567                    XCB_CONFIG_WINDOW_HEIGHT |
568                    XCB_CONFIG_WINDOW_STACK_MODE;
569             values[0] = walk->rect.x;
570             values[1] = walk->rect.y + walk->rect.h - font_height - 6;
571             values[2] = walk->rect.w;
572             values[3] = font_height + 6;
573             values[4] = XCB_STACK_MODE_ABOVE;
574             printf("Reconfiguring Window for output %s to %d,%d\n", walk->name, values[0], values[1]);
575             cookie = xcb_configure_window_checked(xcb_connection,
576                                                   walk->bar,
577                                                   mask,
578                                                   values);
579
580             if ((err = xcb_request_check(xcb_connection, cookie)) != NULL) {
581                 printf("ERROR: Could not reconfigure window. XCB-errorcode: %d\n", err->error_code);
582                 exit(EXIT_FAILURE);
583             }
584         }
585     }
586 }
587
588 /*
589  * Render the bars, with buttons and statusline
590  *
591  */
592 void draw_bars() {
593     printf("Drawing Bars...\n");
594     int i = 0;
595     i3_output *outputs_walk;
596     SLIST_FOREACH(outputs_walk, outputs, slist) {
597         if (!outputs_walk->active) {
598             printf("Output %s inactive, skipping...\n", outputs_walk->name);
599             continue;
600         }
601         if (outputs_walk->bar == XCB_NONE) {
602             reconfig_windows();
603         }
604         uint32_t color = get_colorpixel("000000");
605         xcb_change_gc(xcb_connection,
606                       outputs_walk->bargc,
607                       XCB_GC_FOREGROUND,
608                       &color);
609         xcb_rectangle_t rect = { 0, 0, outputs_walk->rect.w, font_height + 6 };
610         xcb_poly_fill_rectangle(xcb_connection,
611                                 outputs_walk->bar,
612                                 outputs_walk->bargc,
613                                 1,
614                                 &rect);
615         if (statusline != NULL) {
616             printf("Printing statusline!\n");
617             xcb_change_gc(xcb_connection,
618                           outputs_walk->bargc,
619                           XCB_GC_BACKGROUND,
620                           &color);
621             color = get_colorpixel("FFFFFF");
622             xcb_change_gc(xcb_connection,
623                           outputs_walk->bargc,
624                           XCB_GC_FOREGROUND,
625                           &color);
626
627             int glyph_count;
628             xcb_char2b_t *text = (xcb_char2b_t*) convert_utf8_to_ucs2(statusline, &glyph_count);
629
630             xcb_void_cookie_t cookie;
631             cookie = xcb_image_text_16(xcb_connection,
632                                        glyph_count,
633                                        outputs_walk->bar,
634                                        outputs_walk->bargc,
635                                        outputs_walk->rect.w - get_string_width(text, glyph_count) - 4,
636                                        font_height + 1,
637                                        (xcb_char2b_t*) text);
638
639             xcb_generic_error_t *err = xcb_request_check(xcb_connection, cookie);
640
641             if (err != NULL) {
642                 printf("XCB-Error: %d\n", err->error_code);
643             }
644         }
645         i3_ws *ws_walk;
646         TAILQ_FOREACH(ws_walk, outputs_walk->workspaces, tailq) {
647             printf("Drawing Button for WS %s at x = %d\n", ws_walk->name, i);
648             uint32_t color = get_colorpixel("240000");
649             if (ws_walk->visible) {
650                 color = get_colorpixel("480000");
651             }
652             if (ws_walk->urgent) {
653                 printf("WS %s is urgent!\n", ws_walk->name);
654                 color = get_colorpixel("002400");
655                 /* The urgent-hint should get noticed, so we unhide the bars shortly */
656                 unhide_bars();
657             }
658             xcb_change_gc(xcb_connection,
659                           outputs_walk->bargc,
660                           XCB_GC_FOREGROUND,
661                           &color);
662             xcb_change_gc(xcb_connection,
663                           outputs_walk->bargc,
664                           XCB_GC_BACKGROUND,
665                           &color);
666             xcb_rectangle_t rect = { i + 1, 1, ws_walk->name_width + 8, font_height + 4 };
667             xcb_poly_fill_rectangle(xcb_connection,
668                                     outputs_walk->bar,
669                                     outputs_walk->bargc,
670                                     1,
671                                     &rect);
672             color = get_colorpixel("FFFFFF");
673             xcb_change_gc(xcb_connection,
674                           outputs_walk->bargc,
675                           XCB_GC_FOREGROUND,
676                           &color);
677             xcb_image_text_16(xcb_connection,
678                               ws_walk->name_glyphs,
679                               outputs_walk->bar,
680                               outputs_walk->bargc,
681                               i + 5, font_height + 1,
682                               ws_walk->ucs2_name);
683             i += 10 + ws_walk->name_width;
684         }
685
686         i = 0;
687     }
688 }