Added SILC Thread Queue API
[crypto.git] / apps / silcer / src / gtkspell.c
1 /* gtkspell - a spell-checking addon for GtkText
2  * Copyright (c) 2000 Evan Martin.
3  * vim: ts=4 sw=4
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  * 
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
17  */
18
19 #include <gtk/gtk.h>
20
21 #include <sys/types.h>
22 #include <sys/wait.h>
23 #include <sys/time.h>
24 #include <unistd.h>   
25 #include <stdio.h>    
26 #include <signal.h>
27 #include <ctype.h>
28 #include <string.h>
29 #include <stdlib.h>
30 #include <errno.h>
31
32 #define GTKSPELL_USE_GNOME
33
34 #ifdef GTKSPELL_USE_GNOME
35 #include <gnome.h>
36 #endif /* GTKSPELL_USE_GNOME */
37
38 #include "gtkspell.h"
39
40 /* TODO:
41  * handle dictionary changes
42  * asynchronous lookups
43  */
44
45 /* size of the text buffer used in various word-processing routines. */
46 #define BUFSIZE 1024
47 /* number of suggestions to display on each menu. */
48 #define MENUCOUNT 10
49 #define BUGEMAIL "gtkspell-devel@lists.sourceforge.net"
50
51 /* because we keep only one copy of the spell program running, 
52  * all ispell-related variables can be static.  
53  */
54 static pid_t spell_pid = -1;
55 static int fd_write[2] = {0}, fd_read[2] = {0};
56 static int signal_set_up = 0;
57
58 /* FIXME? */
59 static GdkColor highlight = { 0, 255*256, 0, 0 };
60
61 static void entry_insert_cb(GtkText *gtktext, 
62                 gchar *newtext, guint len, guint *ppos, gpointer d);
63 static void set_up_signal();
64
65 int gtkspell_running() {
66         return (spell_pid > 0);
67 }
68
69 static void error_print(const char *fmt, ...) {
70         va_list ap;
71         va_start(ap, fmt);
72         fprintf(stderr, "gtkspell: ");
73         vfprintf(stderr, fmt, ap);
74         va_end(ap);
75 }
76
77 /* functions to interface with pipe */
78 static void writetext(char *text) {
79         write(fd_write[1], text, strlen(text));
80 }
81 static int readpipe(char *buf, int bufsize) {
82         int len;
83         len = read(fd_read[0], buf, bufsize-1);
84         if (len < 0) {
85                 error_print("read: %s\n", strerror(errno));
86                 return -1;
87         } else if (len == 0) {
88                 error_print("pipe closed.\n");
89                 return -1;
90         } else if (len == bufsize-1) {
91                 error_print("buffer overflowed?\n");
92         }
93
94         buf[len] = 0;
95         return len;
96 }
97 static int readline(char *buf) {
98         return readpipe(buf, BUFSIZE);
99 }
100
101 static int readresponse(char *buf) {
102         int len;
103         len = readpipe(buf, BUFSIZE);
104
105         /* all ispell responses of any reasonable length should end in \n\n.
106          * depending on the speed of the spell checker, this may require more
107          * reading. */
108         if (len >= 2 && (buf[len-1] != '\n' || buf[len-2] != '\n')) {
109                 len += readpipe(buf+len, BUFSIZE-len);
110         }
111
112         /* now we can remove all of the the trailing newlines. */
113         while (len > 0 && buf[len-1] == '\n')
114                 buf[--len] = 0;
115
116         return len;
117 }
118
119
120 void gtkspell_stop() {
121         if (gtkspell_running()) {
122                 kill(spell_pid, SIGHUP); 
123                 spell_pid = 0;
124                 close(fd_read[0]);
125                 close(fd_write[1]);
126         }
127 }
128
129 int gtkspell_start(char *path, char * args[]) {
130         int fd_error[2];
131         char buf[BUFSIZE];
132
133         if (gtkspell_running()) {
134                 error_print("gtkspell_start called while already running.\n");
135                 gtkspell_stop();
136         }
137
138         if (!signal_set_up) {
139                 set_up_signal();
140                 signal_set_up = 1;
141         }
142
143         pipe(fd_write);
144         pipe(fd_read);
145         pipe(fd_error);
146
147         spell_pid = fork();
148         if (spell_pid < 0) {
149                 error_print("fork: %s\n", strerror(errno));
150                 return -1;
151         } else if (spell_pid == 0) {
152                 dup2(fd_write[0], 0);
153                 close(fd_write[0]);
154                 close(fd_write[1]);
155
156                 dup2(fd_read[1], 1);
157                 close(fd_read[0]);
158                 close(fd_read[1]);
159
160                 dup2(fd_error[1], 2);
161                 close(fd_error[0]);
162                 close(fd_error[1]);
163
164                 if (path == NULL) {
165                         if (execvp(args[0], args) < 0) 
166                                 error_print("execvp('%s'): %s\n", args[0], strerror(errno));
167                 } else {
168                         if (execv(path, args) < 0) 
169                                 error_print("execv('%s'): %s\n", path, strerror(errno));
170                 }
171                 /* if we get here, we failed.
172                  * send some text on the pipe to indicate status.
173                  */
174                 write(0, "!", 1); /* stdout _is_ the pipe. */
175
176                 _exit(0);
177         } else {
178                 /* there are at least two ways to fail:
179                  * - the exec() can fail
180                  * - the exec() can succeed, but the program can dump the help screen
181                  * we must check for both.
182                  */
183                 fd_set rfds;
184                 struct timeval tv;
185                 
186                 close(fd_write[0]);
187                 close(fd_read[1]);
188
189                 FD_ZERO(&rfds);
190                 FD_SET(fd_error[0], &rfds);
191                 FD_SET(fd_read[0], &rfds);
192                 tv.tv_sec = 2;
193                 tv.tv_usec = 0;
194
195                 if (select(MAX(fd_error[0], fd_read[0])+1, 
196                                         &rfds, NULL, NULL, &tv) < 0) {
197                         /* FIXME: is this needed? */
198                         error_print("Timed out waiting for spell command.\n");
199                         gtkspell_stop();
200                         return -1;
201                 }
202
203                 if (FD_ISSET(fd_error[0], &rfds)) { /* stderr readable? */
204                         error_print("Spell command printed on stderr -- probably failed.\n");
205                         gtkspell_stop();
206                         return -1;
207                 }
208
209                 /* we're done with stderr, now. */
210                 close(fd_error[0]);
211                 close(fd_error[1]);
212
213                 /* otherwise, fd_read[0] is set. */
214                 readline(buf);
215
216                 /* ispell should print something like this:
217                  * @(#) International Ispell Version 3.1.20 10/10/95
218                  * if it doesn't, it's an error. */
219                 if (buf[0] != '@') {
220                         gtkspell_stop();
221                         return -1;
222                 }
223         }
224
225         /* put ispell into terse mode.  
226          * this makes it not respond on correctly spelled words. */
227         sprintf(buf, "!\n");
228         writetext(buf);
229         return 0;
230 }
231
232 static GList* misspelled_suggest(char *word) {
233         char buf[BUFSIZE];
234         char *newword;
235         GList *l = NULL;
236         int count;
237
238         sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
239         writetext(buf);
240         readresponse(buf);
241
242         switch (buf[0]) { /* first char is ispell command. */
243                 case 0: /* no response: word is ok. */
244                         return NULL;
245                 case '&': /* misspelled, with suggestions */
246                         /* & <orig> <count> <ofs>: <miss>, <miss>, <guess>, ... */
247                         strtok(buf, " "); /* & */
248                         newword = strtok(NULL, " "); /* orig */
249                         l = g_list_append(l, g_strdup(newword));
250                         newword = strtok(NULL, " "); /* count */
251                         count = atoi(newword);
252                         strtok(NULL, " "); /* ofs: */
253
254                         while ((newword = strtok(NULL, ",")) != NULL) {
255                                 int len = strlen(newword);
256                                 if (newword[len-1] == ' ' || newword[len-1] == '\n') 
257                                         newword[len-1] = 0;
258                                 if (count == 0) {
259                                         g_list_append(l, NULL); /* signal the "suggestions" */
260                                 }
261                                 /* add it to the list, skipping the initial space. */
262                                 l = g_list_append(l, 
263                                                 g_strdup(newword[0] == ' ' ? newword+1 : newword));
264
265                                 count--;
266                         }
267                         return l;
268
269                 case '#': /* misspelled, no suggestions */
270                 case '?': /* ispell is guessing. */
271                         /* # <orig> <ofs> */
272                         strtok(buf, " "); /* & */
273                         newword = strtok(NULL, " "); /* orig */
274                         l = g_list_append(l, g_strdup(newword));
275                         return l;
276                 default:
277                         error_print("Unsupported spell command '%c'.\n"
278                                         "This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
279                         error_print("Input [%s]\nOutput [%s]\n", word, buf);
280
281         }
282         return NULL;
283 }
284
285 static int misspelled_test(char *word) {
286         char buf[BUFSIZE];
287         sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
288         writetext(buf);
289         readresponse(buf);
290
291         if (buf[0] == 0) {
292                 return 0;
293         } else if (buf[0] == '&' || buf[0] == '#' || buf[0] == '?') {
294                 return 1;
295         }
296         
297         error_print("Unsupported spell command '%c'.\n"
298                         "This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
299         error_print("Input [%s]\nOutput [%s]\n", word, buf);
300         return -1;
301 }
302
303 static gboolean iswordsep(char c) {
304         return !isalpha(c) && c != '\'';
305 }
306
307 static gboolean get_word_from_pos(GtkText* gtktext, int pos, char* buf, 
308                 int *pstart, int *pend) {
309         gint start, end;
310
311         if (iswordsep(GTK_TEXT_INDEX(gtktext, pos))) return FALSE;
312
313         for (start = pos; start >= 0; --start) {
314                 if (iswordsep(GTK_TEXT_INDEX(gtktext, start))) break;
315         }
316         start++;
317
318         for (end = pos; end <= gtk_text_get_length(gtktext); end++) {
319                 if (iswordsep(GTK_TEXT_INDEX(gtktext, end))) break;
320         }
321
322         if (buf) {
323                 for (pos = start; pos < end; pos++) 
324                         buf[pos-start] = GTK_TEXT_INDEX(gtktext, pos);
325                 buf[pos-start] = 0;
326         }
327
328         if (pstart) *pstart = start;
329         if (pend) *pend = end;
330
331         return TRUE;
332 }
333
334 static gboolean get_curword(GtkText* gtktext, char* buf, 
335                 int *pstart, int *pend) {
336         int pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
337         return get_word_from_pos(gtktext, pos, buf, pstart, pend);
338 }
339
340 static void change_color(GtkText *gtktext, 
341                 int start, int end, GdkColor *color) {
342         char *newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
343         gtk_text_freeze(gtktext);
344         gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
345                         GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
346         
347         gtk_text_set_point(gtktext, start);
348         gtk_text_forward_delete(gtktext, end-start);
349
350         if (newtext && end-start > 0)
351                 gtk_text_insert(gtktext, NULL, color, NULL, newtext, end-start);
352
353         gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
354                         GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
355         gtk_text_thaw(gtktext);
356         g_free(newtext);
357 }
358
359 static gboolean check_at(GtkText *gtktext, int from_pos) {
360         int start, end;
361         char buf[BUFSIZE];
362
363         if (!get_word_from_pos(gtktext, from_pos, buf, &start, &end)) {
364                 return FALSE;
365         }
366
367         if (misspelled_test(buf)) {
368                 if (highlight.pixel == 0) {
369                         /* add an entry for the highlight in the color map. */
370                         GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
371                         gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);;
372                 }
373                 change_color(gtktext, start, end, &highlight);
374                 return TRUE;
375         } else { 
376                 change_color(gtktext, start, end, 
377                                 &(GTK_WIDGET(gtktext)->style->fg[0]));
378                 return FALSE;
379         }
380 }
381
382 void gtkspell_check_all(GtkText *gtktext) {
383         guint origpos;
384         guint pos = 0;
385         guint len;
386         float adj_value;
387         
388         if (!gtkspell_running()) return;
389         
390         len = gtk_text_get_length(gtktext);
391
392         adj_value = gtktext->vadj->value;
393         gtk_text_freeze(gtktext);
394         origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
395         while (pos < len) {
396                 while (pos < len && iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
397                         pos++;
398                 while (pos < len && !iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
399                         pos++;
400                 if (pos > 0)
401                         check_at(gtktext, pos-1);
402         }
403         gtk_text_thaw(gtktext);
404         gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
405 }
406
407 static void entry_insert_cb(GtkText *gtktext, 
408                 gchar *newtext, guint len, guint *ppos, gpointer d) {
409         int origpos;
410
411         if (!gtkspell_running()) return;
412
413         gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
414                                                                          GTK_SIGNAL_FUNC(entry_insert_cb),
415                                                                          NULL);
416         gtk_text_insert(GTK_TEXT(gtktext), NULL,
417                         &(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
418         gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
419                                                                          GTK_SIGNAL_FUNC(entry_insert_cb),
420                                                                          NULL);
421         gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
422         *ppos += len;
423
424         origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
425
426         if (iswordsep(newtext[0])) {
427                 /* did we just end a word? */
428                 if (*ppos >= 2) check_at(gtktext, *ppos-2);
429
430                 /* did we just split a word? */
431                 if (*ppos < gtk_text_get_length(gtktext))
432                         check_at(gtktext, *ppos+1);
433         } else {
434                 /* check as they type, *except* if they're typing at the end (the most
435                  * common case.
436                  */
437                 if (*ppos < gtk_text_get_length(gtktext) && 
438                                 !iswordsep(GTK_TEXT_INDEX(gtktext, *ppos)))
439                         check_at(gtktext, *ppos-1);
440         }
441
442         gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
443         gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
444 }
445
446 static void entry_delete_cb(GtkText *gtktext,
447                 gint start, gint end, gpointer d) {
448         int origpos;
449
450         if (!gtkspell_running()) return;
451
452         origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
453         check_at(gtktext, start-1);
454         gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
455         gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
456         /* this is to *UNDO* the selection, in case they were holding shift
457          * while hitting backspace. */
458 }
459
460 static void replace_word(GtkWidget *w, gpointer d) {
461         int start, end;
462         char *newword;
463         char buf[BUFSIZE];
464
465         /* we don't save their position, 
466          * because the cursor is moved by the click. */
467
468         gtk_text_freeze(GTK_TEXT(d));
469
470         gtk_label_get(GTK_LABEL(GTK_BIN(w)->child), &newword);
471         get_curword(GTK_TEXT(d), buf, &start, &end);
472
473         gtk_text_set_point(GTK_TEXT(d), end);
474         gtk_text_backward_delete(GTK_TEXT(d), end-start);
475         gtk_text_insert(GTK_TEXT(d), NULL, NULL, NULL, newword, strlen(newword));
476
477         gtk_text_thaw(GTK_TEXT(d));
478 }
479
480 static GtkMenu *make_menu(GList *l, GtkText *gtktext) {
481         GtkWidget *menu, *item;
482         char *caption;
483         menu = gtk_menu_new(); {
484                 caption = g_strdup_printf("Not in dictionary: %s", (char*)l->data);
485                 item = gtk_menu_item_new_with_label(caption);
486                 /* I'd like to make it so this item is never selectable, like
487                  * the menu titles in the GNOME panel... unfortunately, the GNOME
488                  * panel creates their own custom widget to do this! */
489                 gtk_widget_show(item);
490                 gtk_menu_append(GTK_MENU(menu), item);
491
492                 item = gtk_menu_item_new();
493                 gtk_widget_show(item);
494                 gtk_menu_append(GTK_MENU(menu), item);
495
496                 l = l->next;
497                 if (l == NULL) {
498                         item = gtk_menu_item_new_with_label("(no suggestions)");
499                         gtk_widget_show(item);
500                         gtk_menu_append(GTK_MENU(menu), item);
501                 } else {
502                         GtkWidget *curmenu = menu;
503                         int count = 0;
504                         do {
505                                 if (l->data == NULL && l->next != NULL) {
506                                         count = 0;
507                                         curmenu = gtk_menu_new();
508                                         item = gtk_menu_item_new_with_label("Other Possibilities...");
509                                         gtk_widget_show(item);
510                                         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
511                                         gtk_menu_append(GTK_MENU(curmenu), item);
512                                         l = l->next;
513                                 } else if (count > MENUCOUNT) {
514                                         count -= MENUCOUNT;
515                                         item = gtk_menu_item_new_with_label("More...");
516                                         gtk_widget_show(item);
517                                         gtk_menu_append(GTK_MENU(curmenu), item);
518                                         curmenu = gtk_menu_new();
519                                         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
520                                 }
521                                 item = gtk_menu_item_new_with_label((char*)l->data);
522                                 gtk_signal_connect(GTK_OBJECT(item), "activate",
523                                                 GTK_SIGNAL_FUNC(replace_word), gtktext);
524                                 gtk_widget_show(item);
525                                 gtk_menu_append(GTK_MENU(curmenu), item);
526                                 count++;
527                         } while ((l = l->next) != NULL);
528                 }
529         }
530         return GTK_MENU(menu);
531 }
532
533 static void popup_menu(GtkText *gtktext, GdkEventButton *eb) {
534         char buf[BUFSIZE];
535         GList *list, *l;
536
537         get_curword(gtktext, buf, NULL, NULL);
538
539         list = misspelled_suggest(buf);
540         if (list != NULL) {
541                 gtk_menu_popup(make_menu(list, gtktext), NULL, NULL, NULL, NULL,
542                                 eb->button, eb->time);
543                 for (l = list; l != NULL; l = l->next)
544                         g_free(l->data);
545                 g_list_free(list);
546         }
547 }
548
549 /* ok, this is pretty wacky:
550  * we need to let the right-mouse-click go through, so it moves the cursor, 
551  * but we *can't* let it go through, because GtkText interprets rightclicks as
552  * weird selection modifiers.
553  *
554  * so what do we do?  forge rightclicks as leftclicks, then popup the menu. 
555  * HACK HACK HACK. 
556  */
557 static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d) {
558         GdkEventButton *eb;
559         gboolean retval;
560
561         if (!gtkspell_running()) return FALSE;
562
563         if (e->type != GDK_BUTTON_PRESS) return FALSE;
564         eb = (GdkEventButton*) e;
565
566         if (eb->button != 3) return FALSE;
567
568         /* forge the leftclick */
569         eb->button = 1;
570
571         gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
572                         GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
573         gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
574                         e, &retval);
575         gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
576                         GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
577         gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");
578
579         /* now do the menu wackiness */
580         popup_menu(gtktext, eb);
581         return TRUE;
582 }
583
584 void gtkspell_uncheck_all(GtkText *gtktext) {
585         int origpos;
586         char *text;
587         float adj_value;
588
589         adj_value = gtktext->vadj->value;
590         gtk_text_freeze(gtktext);
591         origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
592         text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
593         gtk_text_set_point(gtktext, 0);
594         gtk_text_forward_delete(gtktext, gtk_text_get_length(gtktext));
595         gtk_text_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
596         gtk_text_thaw(gtktext);
597
598         gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
599         gtk_adjustment_set_value(gtktext->vadj, adj_value);
600 }
601
602 void gtkspell_attach(GtkText *gtktext) {
603         gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
604                 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
605         gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
606                 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
607         gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
608                         GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
609 }
610
611 void gtkspell_detach(GtkText *gtktext) {
612         gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
613                 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
614         gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
615                 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
616         gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext), 
617                         GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
618
619         gtkspell_uncheck_all(gtktext);
620 }
621
622 static void sigchld(int param) {
623         if (gtkspell_running() &&
624                 (waitpid(spell_pid, NULL, WNOHANG) == spell_pid)) {
625                 spell_pid = 0;
626         } else {
627                 /* a default SIGCHLD handler.
628                  * what else to do here? */
629                 waitpid(-1, NULL, WNOHANG);
630         }
631 }
632
633 static void set_up_signal() {
634         struct sigaction sigact;
635         memset(&sigact, 0, sizeof(struct sigaction));
636
637         sigact.sa_handler = sigchld;
638         sigaction(SIGCHLD, &sigact, NULL);
639 }