1 | #include <errno.h> |
2 | #include <stdio.h> |
3 | #include <stdlib.h> |
4 | #include <string.h> |
5 | |
6 | #include "config.h" |
7 | |
8 | #include <gtk/gtk.h> |
9 | #include <glib/gstdio.h> |
10 | |
11 | #ifdef HAVE_GIO_UNIX |
12 | #include <gio/gunixoutputstream.h> |
13 | #include <fcntl.h> |
14 | #endif |
15 | |
16 | |
17 | /* This is the guts of gtk_text_buffer_insert_markup, |
18 | * copied here so we can make an incremental version. |
19 | */ |
20 | static void |
21 | insert_tags_for_attributes (GtkTextBuffer *buffer, |
22 | PangoAttrIterator *iter, |
23 | GtkTextIter *start, |
24 | GtkTextIter *end) |
25 | { |
26 | GtkTextTagTable *table; |
27 | GSList *attrs, *l; |
28 | GtkTextTag *tag; |
29 | char name[256]; |
30 | float fg_alpha, bg_alpha; |
31 | |
32 | table = gtk_text_buffer_get_tag_table (buffer); |
33 | |
34 | #define LANGUAGE_ATTR(attr_name) \ |
35 | { \ |
36 | const char *language = pango_language_to_string (((PangoAttrLanguage*)attr)->value); \ |
37 | g_snprintf (name, 256, "language=%s", language); \ |
38 | tag = gtk_text_tag_table_lookup (table, name); \ |
39 | if (!tag) \ |
40 | { \ |
41 | tag = gtk_text_tag_new (name); \ |
42 | g_object_set (tag, #attr_name, language, NULL); \ |
43 | gtk_text_tag_table_add (table, tag); \ |
44 | g_object_unref (tag); \ |
45 | } \ |
46 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
47 | } |
48 | |
49 | #define STRING_ATTR(attr_name) \ |
50 | { \ |
51 | const char *string = ((PangoAttrString*)attr)->value; \ |
52 | g_snprintf (name, 256, #attr_name "=%s", string); \ |
53 | tag = gtk_text_tag_table_lookup (table, name); \ |
54 | if (!tag) \ |
55 | { \ |
56 | tag = gtk_text_tag_new (name); \ |
57 | g_object_set (tag, #attr_name, string, NULL); \ |
58 | gtk_text_tag_table_add (table, tag); \ |
59 | g_object_unref (tag); \ |
60 | } \ |
61 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
62 | } |
63 | |
64 | #define INT_ATTR(attr_name) \ |
65 | { \ |
66 | int value = ((PangoAttrInt*)attr)->value; \ |
67 | g_snprintf (name, 256, #attr_name "=%d", value); \ |
68 | tag = gtk_text_tag_table_lookup (table, name); \ |
69 | if (!tag) \ |
70 | { \ |
71 | tag = gtk_text_tag_new (name); \ |
72 | g_object_set (tag, #attr_name, value, NULL); \ |
73 | gtk_text_tag_table_add (table, tag); \ |
74 | g_object_unref (tag); \ |
75 | } \ |
76 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
77 | } |
78 | |
79 | #define FONT_ATTR(attr_name) \ |
80 | { \ |
81 | PangoFontDescription *desc = ((PangoAttrFontDesc*)attr)->desc; \ |
82 | char *str = pango_font_description_to_string (desc); \ |
83 | g_snprintf (name, 256, "font-desc=%s", str); \ |
84 | g_free (str); \ |
85 | tag = gtk_text_tag_table_lookup (table, name); \ |
86 | if (!tag) \ |
87 | { \ |
88 | tag = gtk_text_tag_new (name); \ |
89 | g_object_set (tag, #attr_name, desc, NULL); \ |
90 | gtk_text_tag_table_add (table, tag); \ |
91 | g_object_unref (tag); \ |
92 | } \ |
93 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
94 | } |
95 | |
96 | #define FLOAT_ATTR(attr_name) \ |
97 | { \ |
98 | float value = ((PangoAttrFloat*)attr)->value; \ |
99 | g_snprintf (name, 256, #attr_name "=%g", value); \ |
100 | tag = gtk_text_tag_table_lookup (table, name); \ |
101 | if (!tag) \ |
102 | { \ |
103 | tag = gtk_text_tag_new (name); \ |
104 | g_object_set (tag, #attr_name, value, NULL); \ |
105 | gtk_text_tag_table_add (table, tag); \ |
106 | g_object_unref (tag); \ |
107 | } \ |
108 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
109 | } |
110 | |
111 | #define RGBA_ATTR(attr_name, alpha_value) \ |
112 | { \ |
113 | PangoColor *color; \ |
114 | GdkRGBA rgba; \ |
115 | color = &((PangoAttrColor*)attr)->color; \ |
116 | rgba.red = color->red / 65535.; \ |
117 | rgba.green = color->green / 65535.; \ |
118 | rgba.blue = color->blue / 65535.; \ |
119 | rgba.alpha = alpha_value; \ |
120 | char *str = gdk_rgba_to_string (&rgba); \ |
121 | g_snprintf (name, 256, #attr_name "=%s", str); \ |
122 | g_free (str); \ |
123 | tag = gtk_text_tag_table_lookup (table, name); \ |
124 | if (!tag) \ |
125 | { \ |
126 | tag = gtk_text_tag_new (name); \ |
127 | g_object_set (tag, #attr_name, &rgba, NULL); \ |
128 | gtk_text_tag_table_add (table, tag); \ |
129 | g_object_unref (tag); \ |
130 | } \ |
131 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
132 | } |
133 | |
134 | #define VOID_ATTR(attr_name) \ |
135 | { \ |
136 | tag = gtk_text_tag_table_lookup (table, #attr_name); \ |
137 | if (!tag) \ |
138 | { \ |
139 | tag = gtk_text_tag_new (#attr_name); \ |
140 | g_object_set (tag, #attr_name, TRUE, NULL); \ |
141 | gtk_text_tag_table_add (table, tag); \ |
142 | g_object_unref (tag); \ |
143 | } \ |
144 | gtk_text_buffer_apply_tag (buffer, tag, start, end); \ |
145 | } |
146 | |
147 | fg_alpha = bg_alpha = 1.; |
148 | |
149 | attrs = pango_attr_iterator_get_attrs (iterator: iter); |
150 | for (l = attrs; l; l = l->next) |
151 | { |
152 | PangoAttribute *attr = l->data; |
153 | |
154 | switch ((int)attr->klass->type) |
155 | { |
156 | case PANGO_ATTR_FOREGROUND_ALPHA: |
157 | fg_alpha = ((PangoAttrInt*)attr)->value / 65535.; |
158 | break; |
159 | |
160 | case PANGO_ATTR_BACKGROUND_ALPHA: |
161 | bg_alpha = ((PangoAttrInt*)attr)->value / 65535.; |
162 | break; |
163 | |
164 | default: |
165 | break; |
166 | } |
167 | } |
168 | |
169 | for (l = attrs; l; l = l->next) |
170 | { |
171 | PangoAttribute *attr = l->data; |
172 | |
173 | switch (attr->klass->type) |
174 | { |
175 | case PANGO_ATTR_LANGUAGE: |
176 | LANGUAGE_ATTR (language); |
177 | break; |
178 | |
179 | case PANGO_ATTR_FAMILY: |
180 | STRING_ATTR (family); |
181 | break; |
182 | |
183 | case PANGO_ATTR_STYLE: |
184 | INT_ATTR (style); |
185 | break; |
186 | |
187 | case PANGO_ATTR_WEIGHT: |
188 | INT_ATTR (weight); |
189 | break; |
190 | |
191 | case PANGO_ATTR_VARIANT: |
192 | INT_ATTR (variant); |
193 | break; |
194 | |
195 | case PANGO_ATTR_STRETCH: |
196 | INT_ATTR (stretch); |
197 | break; |
198 | |
199 | case PANGO_ATTR_SIZE: |
200 | INT_ATTR (size); |
201 | break; |
202 | |
203 | case PANGO_ATTR_FONT_DESC: |
204 | FONT_ATTR (font-desc); |
205 | break; |
206 | |
207 | case PANGO_ATTR_FOREGROUND: |
208 | RGBA_ATTR (foreground_rgba, fg_alpha); |
209 | break; |
210 | |
211 | case PANGO_ATTR_BACKGROUND: |
212 | RGBA_ATTR (background_rgba, bg_alpha); |
213 | break; |
214 | |
215 | case PANGO_ATTR_UNDERLINE: |
216 | INT_ATTR (underline); |
217 | break; |
218 | |
219 | case PANGO_ATTR_UNDERLINE_COLOR: |
220 | RGBA_ATTR (underline_rgba, fg_alpha); |
221 | break; |
222 | |
223 | case PANGO_ATTR_OVERLINE: |
224 | INT_ATTR (overline); |
225 | break; |
226 | |
227 | case PANGO_ATTR_OVERLINE_COLOR: |
228 | RGBA_ATTR (overline_rgba, fg_alpha); |
229 | break; |
230 | |
231 | case PANGO_ATTR_STRIKETHROUGH: |
232 | INT_ATTR (strikethrough); |
233 | break; |
234 | |
235 | case PANGO_ATTR_STRIKETHROUGH_COLOR: |
236 | RGBA_ATTR (strikethrough_rgba, fg_alpha); |
237 | break; |
238 | |
239 | case PANGO_ATTR_RISE: |
240 | INT_ATTR (rise); |
241 | break; |
242 | |
243 | case PANGO_ATTR_SCALE: |
244 | FLOAT_ATTR (scale); |
245 | break; |
246 | |
247 | case PANGO_ATTR_FALLBACK: |
248 | INT_ATTR (fallback); |
249 | break; |
250 | |
251 | case PANGO_ATTR_LETTER_SPACING: |
252 | INT_ATTR (letter_spacing); |
253 | break; |
254 | |
255 | case PANGO_ATTR_FONT_FEATURES: |
256 | STRING_ATTR (font_features); |
257 | break; |
258 | |
259 | case PANGO_ATTR_ALLOW_BREAKS: |
260 | INT_ATTR (allow_breaks); |
261 | break; |
262 | |
263 | case PANGO_ATTR_SHOW: |
264 | INT_ATTR (show_spaces); |
265 | break; |
266 | |
267 | case PANGO_ATTR_INSERT_HYPHENS: |
268 | INT_ATTR (insert_hyphens); |
269 | break; |
270 | |
271 | case PANGO_ATTR_LINE_HEIGHT: |
272 | FLOAT_ATTR (line_height); |
273 | break; |
274 | |
275 | case PANGO_ATTR_ABSOLUTE_LINE_HEIGHT: |
276 | break; |
277 | |
278 | case PANGO_ATTR_WORD: |
279 | VOID_ATTR (word); |
280 | break; |
281 | |
282 | case PANGO_ATTR_SENTENCE: |
283 | VOID_ATTR (sentence); |
284 | break; |
285 | |
286 | case PANGO_ATTR_BASELINE_SHIFT: |
287 | INT_ATTR (baseline_shift); |
288 | break; |
289 | |
290 | case PANGO_ATTR_FONT_SCALE: |
291 | INT_ATTR (font_scale); |
292 | break; |
293 | |
294 | case PANGO_ATTR_SHAPE: |
295 | case PANGO_ATTR_ABSOLUTE_SIZE: |
296 | case PANGO_ATTR_GRAVITY: |
297 | case PANGO_ATTR_GRAVITY_HINT: |
298 | case PANGO_ATTR_FOREGROUND_ALPHA: |
299 | case PANGO_ATTR_BACKGROUND_ALPHA: |
300 | break; |
301 | |
302 | case PANGO_ATTR_TEXT_TRANSFORM: |
303 | INT_ATTR (text_transform); |
304 | break; |
305 | |
306 | case PANGO_ATTR_INVALID: |
307 | default: |
308 | g_assert_not_reached (); |
309 | break; |
310 | } |
311 | } |
312 | |
313 | g_slist_free_full (list: attrs, free_func: (GDestroyNotify)pango_attribute_destroy); |
314 | |
315 | #undef LANGUAGE_ATTR |
316 | #undef STRING_ATTR |
317 | #undef INT_ATTR |
318 | #undef FONT_ATTR |
319 | #undef FLOAT_ATTR |
320 | #undef RGBA_ATTR |
321 | } |
322 | |
323 | typedef struct |
324 | { |
325 | GMarkupParseContext *parser; |
326 | char *markup; |
327 | gsize pos; |
328 | gsize len; |
329 | GtkTextBuffer *buffer; |
330 | GtkTextIter iter; |
331 | GtkTextMark *mark; |
332 | PangoAttrList *attributes; |
333 | char *text; |
334 | PangoAttrIterator *attr; |
335 | } MarkupData; |
336 | |
337 | static void |
338 | free_markup_data (MarkupData *mdata) |
339 | { |
340 | g_free (mem: mdata->markup); |
341 | g_clear_pointer (&mdata->parser, g_markup_parse_context_free); |
342 | gtk_text_buffer_delete_mark (buffer: mdata->buffer, mark: mdata->mark); |
343 | g_clear_pointer (&mdata->attr, pango_attr_iterator_destroy); |
344 | g_clear_pointer (&mdata->attributes, pango_attr_list_unref); |
345 | g_free (mem: mdata->text); |
346 | g_object_unref (object: mdata->buffer); |
347 | g_free (mem: mdata); |
348 | } |
349 | |
350 | static gboolean |
351 | insert_markup_idle (gpointer data) |
352 | { |
353 | MarkupData *mdata = data; |
354 | gint64 begin; |
355 | |
356 | begin = g_get_monotonic_time (); |
357 | |
358 | do |
359 | { |
360 | int start, end; |
361 | int start_offset; |
362 | GtkTextIter start_iter; |
363 | |
364 | if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND) |
365 | { |
366 | g_idle_add (function: insert_markup_idle, data); |
367 | return G_SOURCE_REMOVE; |
368 | } |
369 | |
370 | pango_attr_iterator_range (iterator: mdata->attr, start: &start, end: &end); |
371 | |
372 | if (end == G_MAXINT) /* last chunk */ |
373 | end = start - 1; /* resulting in -1 to be passed to _insert */ |
374 | |
375 | start_offset = gtk_text_iter_get_offset (iter: &mdata->iter); |
376 | gtk_text_buffer_insert (buffer: mdata->buffer, iter: &mdata->iter, text: mdata->text + start, len: end - start); |
377 | gtk_text_buffer_get_iter_at_offset (buffer: mdata->buffer, iter: &start_iter, char_offset: start_offset); |
378 | |
379 | insert_tags_for_attributes (buffer: mdata->buffer, iter: mdata->attr, start: &start_iter, end: &mdata->iter); |
380 | |
381 | gtk_text_buffer_get_iter_at_mark (buffer: mdata->buffer, iter: &mdata->iter, mark: mdata->mark); |
382 | } |
383 | while (pango_attr_iterator_next (iterator: mdata->attr)); |
384 | |
385 | free_markup_data (mdata); |
386 | return G_SOURCE_REMOVE; |
387 | } |
388 | |
389 | static gboolean |
390 | parse_markup_idle (gpointer data) |
391 | { |
392 | MarkupData *mdata = data; |
393 | gint64 begin; |
394 | GError *error = NULL; |
395 | |
396 | begin = g_get_monotonic_time (); |
397 | |
398 | do { |
399 | if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND) |
400 | { |
401 | g_idle_add (function: parse_markup_idle, data); |
402 | return G_SOURCE_REMOVE; |
403 | } |
404 | |
405 | if (!g_markup_parse_context_parse (context: mdata->parser, |
406 | text: mdata->markup + mdata->pos, |
407 | MIN (4096, mdata->len - mdata->pos), |
408 | error: &error)) |
409 | { |
410 | g_warning ("Invalid markup string: %s" , error->message); |
411 | g_error_free (error); |
412 | free_markup_data (mdata); |
413 | return G_SOURCE_REMOVE; |
414 | } |
415 | |
416 | mdata->pos += 4096; |
417 | } while (mdata->pos < mdata->len); |
418 | |
419 | if (!pango_markup_parser_finish (context: mdata->parser, |
420 | attr_list: &mdata->attributes, |
421 | text: &mdata->text, |
422 | NULL, |
423 | error: &error)) |
424 | { |
425 | g_warning ("Invalid markup string: %s" , error->message); |
426 | g_error_free (error); |
427 | free_markup_data (mdata); |
428 | return G_SOURCE_REMOVE; |
429 | } |
430 | |
431 | if (!mdata->attributes) |
432 | { |
433 | gtk_text_buffer_insert (buffer: mdata->buffer, iter: &mdata->iter, text: mdata->text, len: -1); |
434 | free_markup_data (mdata); |
435 | return G_SOURCE_REMOVE; |
436 | } |
437 | |
438 | mdata->attr = pango_attr_list_get_iterator (list: mdata->attributes); |
439 | insert_markup_idle (data); |
440 | |
441 | return G_SOURCE_REMOVE; |
442 | } |
443 | |
444 | /* Takes a ref on @buffer while it is operating, |
445 | * and consumes @markup. |
446 | */ |
447 | static void |
448 | insert_markup (GtkTextBuffer *buffer, |
449 | GtkTextIter *iter, |
450 | char *markup, |
451 | int len) |
452 | { |
453 | MarkupData *data; |
454 | |
455 | g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); |
456 | |
457 | data = g_new0 (MarkupData, 1); |
458 | |
459 | data->buffer = g_object_ref (buffer); |
460 | data->iter = *iter; |
461 | data->markup = markup; |
462 | data->len = len; |
463 | |
464 | data->parser = pango_markup_parser_new (accel_marker: 0); |
465 | data->pos = 0; |
466 | |
467 | /* create mark with right gravity */ |
468 | data->mark = gtk_text_buffer_create_mark (buffer, NULL, where: iter, FALSE); |
469 | |
470 | parse_markup_idle (data); |
471 | } |
472 | |
473 | static void |
474 | fontify_finish (GObject *source, |
475 | GAsyncResult *result, |
476 | gpointer data) |
477 | { |
478 | GSubprocess *subprocess = G_SUBPROCESS (source); |
479 | GtkTextBuffer *buffer = data; |
480 | GBytes *stdout_buf = NULL; |
481 | GBytes *stderr_buf = NULL; |
482 | GError *error = NULL; |
483 | |
484 | if (!g_subprocess_communicate_finish (subprocess, |
485 | result, |
486 | stdout_buf: &stdout_buf, |
487 | stderr_buf: &stderr_buf, |
488 | error: &error)) |
489 | { |
490 | g_clear_pointer (&stdout_buf, g_bytes_unref); |
491 | g_clear_pointer (&stderr_buf, g_bytes_unref); |
492 | |
493 | g_warning ("%s" , error->message); |
494 | g_clear_error (err: &error); |
495 | |
496 | g_object_unref (object: subprocess); |
497 | g_object_unref (object: buffer); |
498 | return; |
499 | } |
500 | |
501 | if (g_subprocess_get_exit_status (subprocess) != 0) |
502 | { |
503 | if (stderr_buf) |
504 | g_warning ("%s" , (char *)g_bytes_get_data (stderr_buf, NULL)); |
505 | g_clear_pointer (&stderr_buf, g_bytes_unref); |
506 | } |
507 | |
508 | g_object_unref (object: subprocess); |
509 | |
510 | g_clear_pointer (&stderr_buf, g_bytes_unref); |
511 | |
512 | if (stdout_buf) |
513 | { |
514 | char *markup; |
515 | gsize len; |
516 | char *p; |
517 | GtkTextIter start; |
518 | |
519 | gtk_text_buffer_set_text (buffer, text: "" , len: 0); |
520 | |
521 | /* highlight puts a span with font and size around its output, |
522 | * which we don't want. |
523 | */ |
524 | markup = g_bytes_unref_to_data (bytes: stdout_buf, size: &len); |
525 | for (p = markup + strlen (s: "<span " ); *p != '>'; p++) *p = ' '; |
526 | |
527 | gtk_text_buffer_get_start_iter (buffer, iter: &start); |
528 | insert_markup (buffer, iter: &start, markup, len); |
529 | } |
530 | |
531 | g_object_unref (object: buffer); |
532 | } |
533 | |
534 | void |
535 | fontify (const char *format, |
536 | GtkTextBuffer *source_buffer) |
537 | { |
538 | GSubprocess *subprocess; |
539 | char *format_arg; |
540 | GtkSettings *settings; |
541 | char *theme; |
542 | gboolean prefer_dark; |
543 | const char *style_arg; |
544 | char *text; |
545 | GtkTextIter start, end; |
546 | GBytes *bytes; |
547 | GError *error = NULL; |
548 | |
549 | settings = gtk_settings_get_default (); |
550 | g_object_get (object: settings, |
551 | first_property_name: "gtk-theme-name" , &theme, |
552 | "gtk-application-prefer-dark-theme" , &prefer_dark, |
553 | NULL); |
554 | |
555 | if (prefer_dark || strcmp (s1: theme, s2: "HighContrastInverse" ) == 0) |
556 | style_arg = "--style=edit-vim-dark" ; |
557 | else |
558 | style_arg = "--style=edit-kwrite" ; |
559 | |
560 | g_free (mem: theme); |
561 | |
562 | format_arg = g_strconcat (string1: "--syntax=" , format, NULL); |
563 | subprocess = g_subprocess_new (flags: G_SUBPROCESS_FLAGS_STDIN_PIPE | |
564 | G_SUBPROCESS_FLAGS_STDOUT_PIPE | |
565 | G_SUBPROCESS_FLAGS_STDERR_PIPE, |
566 | error: &error, |
567 | argv0: "highlight" , |
568 | format_arg, |
569 | "--out-format=pango" , |
570 | style_arg, |
571 | NULL); |
572 | g_free (mem: format_arg); |
573 | |
574 | if (!subprocess) |
575 | { |
576 | if (g_error_matches (error, G_SPAWN_ERROR, code: G_SPAWN_ERROR_NOENT)) |
577 | { |
578 | static gboolean warned = FALSE; |
579 | |
580 | if (!warned) |
581 | { |
582 | warned = TRUE; |
583 | g_message ("For syntax highlighting, install the “highlight” program" ); |
584 | } |
585 | } |
586 | else |
587 | g_warning ("%s" , error->message); |
588 | |
589 | g_clear_error (err: &error); |
590 | |
591 | return; |
592 | } |
593 | |
594 | gtk_text_buffer_get_bounds (buffer: source_buffer, start: &start, end: &end); |
595 | text = gtk_text_buffer_get_text (buffer: source_buffer, start: &start, end: &end, TRUE); |
596 | bytes = g_bytes_new_take (data: text, size: strlen (s: text)); |
597 | |
598 | #ifdef HAVE_GIO_UNIX |
599 | /* Work around https://gitlab.gnome.org/GNOME/glib/-/issues/2182 */ |
600 | if (G_IS_UNIX_OUTPUT_STREAM (g_subprocess_get_stdin_pipe (subprocess))) |
601 | { |
602 | GOutputStream *stdin_pipe = g_subprocess_get_stdin_pipe (subprocess); |
603 | int fd = g_unix_output_stream_get_fd (G_UNIX_OUTPUT_STREAM (stdin_pipe)); |
604 | fcntl (fd: fd, F_SETFL, O_NONBLOCK); |
605 | } |
606 | #endif |
607 | |
608 | g_subprocess_communicate_async (subprocess, |
609 | stdin_buf: bytes, |
610 | NULL, |
611 | callback: fontify_finish, |
612 | g_object_ref (source_buffer)); |
613 | g_bytes_unref (bytes); |
614 | } |
615 | |