fix CID 1596595: Resource leaks, and CID 1596594: (CHECKED_RETURN)
[claws.git] / src / plugins / python / python-shell.c
1 /*
2  * Copyright (c) 2008-2009  Christian Hammond
3  * Copyright (c) 2008-2009  David Trowbridge
4  * Copyright (C) 2021 the Claws Mail Team
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a
7  * copy of this software and associated documentation files (the "Software"),
8  * to deal in the Software without restriction, including without limitation
9  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10  * and/or sell copies of the Software, and to permit persons to whom the
11  * Software is furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included
14  * in all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24
25 #include "config.h"
26
27 #include <gdk/gdkkeysyms.h>
28 #include <string.h>
29
30 #include "python-hooks.h"
31 #include "python-shell.h"
32
33 #define MAX_HISTORY_LENGTH 20
34
35 #define PARASITE_PYTHON_SHELL_GET_PRIVATE(obj) \
36     (G_TYPE_INSTANCE_GET_PRIVATE((obj), PARASITE_TYPE_PYTHON_SHELL, \
37                                  ParasitePythonShellPrivate))
38
39 typedef struct
40 {
41     GtkWidget *textview;
42
43     GtkTextMark *scroll_mark;
44     GtkTextMark *line_start_mark;
45
46     GQueue *history;
47     GList *cur_history_item;
48
49     GString *pending_command;
50     gboolean in_block;
51
52 } ParasitePythonShellPrivate;
53
54 enum
55 {
56     LAST_SIGNAL
57 };
58
59
60 /* Widget functions */
61 static void parasite_python_shell_finalize(GObject *obj);
62
63 /* Python integration */
64 static void parasite_python_shell_write_prompt(GtkWidget *python_shell);
65 static char *parasite_python_shell_get_input(GtkWidget *python_shell);
66
67 /* Callbacks */
68 static gboolean parasite_python_shell_key_press_cb(GtkWidget *textview,
69                                                    GdkEventKey *event,
70                                                    GtkWidget *python_shell);
71
72
73 static GtkVBoxClass *parent_class = NULL;
74 //static guint signals[LAST_SIGNAL] = {0};
75
76 G_DEFINE_TYPE_WITH_CODE(ParasitePythonShell, parasite_python_shell,
77     GTK_TYPE_VBOX, G_ADD_PRIVATE(ParasitePythonShell))
78
79
80 static void
81 parasite_python_shell_class_init(ParasitePythonShellClass *klass)
82 {
83     GObjectClass *object_class = G_OBJECT_CLASS(klass);
84
85     parent_class = g_type_class_peek_parent(klass);
86
87     object_class->finalize = parasite_python_shell_finalize;
88 }
89
90 static void
91 parasite_python_shell_init(ParasitePythonShell *python_shell)
92 {
93     ParasitePythonShellPrivate *priv =
94         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
95     GtkWidget *swin;
96     GtkTextBuffer *buffer;
97     GtkTextIter iter;
98     PangoFontDescription *font_desc;
99
100     priv->history = g_queue_new();
101
102     gtk_box_set_spacing(GTK_BOX(python_shell), 6);
103
104     swin = gtk_scrolled_window_new(NULL, NULL);
105     gtk_widget_show(swin);
106     gtk_box_pack_start(GTK_BOX(python_shell), swin, TRUE, TRUE, 0);
107     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
108                                    GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
109     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin),
110                                         GTK_SHADOW_IN);
111
112     priv->textview = gtk_text_view_new();
113     gtk_widget_show(priv->textview);
114     gtk_container_add(GTK_CONTAINER(swin), priv->textview);
115     gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(priv->textview), TRUE);
116     gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(priv->textview), 3);
117     gtk_text_view_set_left_margin(GTK_TEXT_VIEW(priv->textview), 3);
118     gtk_text_view_set_right_margin(GTK_TEXT_VIEW(priv->textview), 3);
119
120     g_signal_connect(G_OBJECT(priv->textview), "key_press_event",
121                      G_CALLBACK(parasite_python_shell_key_press_cb),
122                      python_shell);
123
124     /* Make the textview monospaced */
125     font_desc = pango_font_description_from_string("monospace");
126     pango_font_description_set_size(font_desc, 10 * PANGO_SCALE);
127     gtk_widget_override_font(priv->textview, font_desc);
128     pango_font_description_free(font_desc);
129
130     /* Create the end-of-buffer mark */
131     buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
132     gtk_text_buffer_get_end_iter(buffer, &iter);
133     priv->scroll_mark = gtk_text_buffer_create_mark(buffer, "scroll_mark",
134                                                     &iter, FALSE);
135
136     /* Create the beginning-of-line mark */
137     priv->line_start_mark = gtk_text_buffer_create_mark(buffer,
138                                                         "line_start_mark",
139                                                         &iter, TRUE);
140
141     /* Register some tags */
142     gtk_text_buffer_create_tag(buffer, "stdout", NULL);
143     gtk_text_buffer_create_tag(buffer, "stderr",
144                                "foreground", "red",
145                                "paragraph-background", "#FFFFE0",
146                                NULL);
147     gtk_text_buffer_create_tag(buffer, "prompt",
148                                "foreground", "blue",
149                                NULL);
150
151     parasite_python_shell_write_prompt(GTK_WIDGET(python_shell));
152 }
153
154 static void
155 parasite_python_shell_finalize(GObject *python_shell)
156 {
157     ParasitePythonShellPrivate *priv =
158         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
159
160     g_queue_free(priv->history);
161 }
162
163 static void
164 parasite_python_shell_log_stdout(const char *text, gpointer python_shell)
165 {
166     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
167                                       text, "stdout");
168 }
169
170 static void
171 parasite_python_shell_log_stderr(const char *text, gpointer python_shell)
172 {
173     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
174                                       text, "stderr");
175 }
176
177 static void
178 parasite_python_shell_write_prompt(GtkWidget *python_shell)
179 {
180     ParasitePythonShellPrivate *priv =
181         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
182     GtkTextBuffer *buffer =
183         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
184     GtkTextIter iter;
185     const char *prompt = (priv->pending_command == NULL ? ">>> " : "... ");
186
187     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
188                                       prompt, "prompt");
189
190     gtk_text_buffer_get_end_iter(buffer, &iter);
191     gtk_text_buffer_move_mark(buffer, priv->line_start_mark, &iter);
192 }
193
194 static void
195 parasite_python_shell_process_line(GtkWidget *python_shell)
196 {
197     ParasitePythonShellPrivate *priv =
198         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
199
200     char *command = parasite_python_shell_get_input(python_shell);
201     char last_char;
202
203     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
204                                       "\n", NULL);
205
206     if (*command != '\0')
207     {
208         /* Save this command in the history. */
209         g_queue_push_head(priv->history, command);
210         priv->cur_history_item = NULL;
211
212         if (g_queue_get_length(priv->history) > MAX_HISTORY_LENGTH)
213             g_free(g_queue_pop_tail(priv->history));
214     }
215
216     last_char = command[MAX(0, (gint)(strlen(command) - 1))];
217
218     if (last_char == ':' || last_char == '\\' ||
219         (priv->in_block && g_ascii_isspace(command[0])))
220     {
221         printf("in block.. %c, %d, %d\n",
222                last_char, priv->in_block,
223                g_ascii_isspace(command[0]));
224         /* This is a multi-line expression */
225         if (priv->pending_command == NULL)
226             priv->pending_command = g_string_new(command);
227         else
228             g_string_append(priv->pending_command, command);
229
230         g_string_append_c(priv->pending_command, '\n');
231
232         if (last_char == ':')
233             priv->in_block = TRUE;
234     }
235     else
236     {
237         if (priv->pending_command != NULL)
238         {
239             g_string_append(priv->pending_command, command);
240             g_string_append_c(priv->pending_command, '\n');
241
242             /* We're not actually leaking this. It's in the history. */
243             command = g_string_free(priv->pending_command, FALSE);
244         }
245         parasite_python_run(command,
246                             parasite_python_shell_log_stdout,
247                             parasite_python_shell_log_stderr,
248                             python_shell);
249         if (priv->pending_command != NULL)
250         {
251             /* Now do the cleanup. */
252             g_free(command);
253             priv->pending_command = NULL;
254             priv->in_block = FALSE;
255         }
256     }
257     parasite_python_shell_write_prompt(python_shell);
258 }
259
260 static void
261 parasite_python_shell_replace_input(GtkWidget *python_shell,
262                                     const char *text)
263 {
264     ParasitePythonShellPrivate *priv =
265         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
266
267     GtkTextBuffer *buffer =
268         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
269     GtkTextIter start_iter;
270     GtkTextIter end_iter;
271
272     gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
273                                      priv->line_start_mark);
274     gtk_text_buffer_get_end_iter(buffer, &end_iter);
275
276     gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
277     gtk_text_buffer_insert(buffer, &end_iter, text, -1);
278 }
279
280 static char *
281 parasite_python_shell_get_input(GtkWidget *python_shell)
282 {
283     ParasitePythonShellPrivate *priv =
284         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
285     GtkTextBuffer *buffer =
286         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
287     GtkTextIter start_iter;
288     GtkTextIter end_iter;
289
290     gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
291                                      priv->line_start_mark);
292     gtk_text_buffer_get_end_iter(buffer, &end_iter);
293
294     return gtk_text_buffer_get_text(buffer, &start_iter, &end_iter, FALSE);
295 }
296
297 static const char *
298 parasite_python_shell_get_history_back(GtkWidget *python_shell)
299 {
300     ParasitePythonShellPrivate *priv =
301         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
302
303     if (priv->cur_history_item == NULL)
304     {
305         priv->cur_history_item = g_queue_peek_head_link(priv->history);
306
307         if (priv->cur_history_item == NULL)
308             return "";
309     }
310     else if (priv->cur_history_item->next != NULL)
311         priv->cur_history_item = priv->cur_history_item->next;
312
313     return (const char *)priv->cur_history_item->data;
314 }
315
316 static const char *
317 parasite_python_shell_get_history_forward(GtkWidget *python_shell)
318 {
319     ParasitePythonShellPrivate *priv =
320         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
321
322     if (priv->cur_history_item == NULL || priv->cur_history_item->prev == NULL)
323     {
324         priv->cur_history_item = NULL;
325         return "";
326     }
327
328     priv->cur_history_item = priv->cur_history_item->prev;
329
330     return (const char *)priv->cur_history_item->data;
331 }
332
333 static gboolean
334 parasite_python_shell_key_press_cb(GtkWidget *textview,
335                                    GdkEventKey *event,
336                                    GtkWidget *python_shell)
337 {
338     if (event && (event->keyval == GDK_KEY_KP_Enter ||
339         event->keyval == GDK_KEY_Return))
340     {
341         parasite_python_shell_process_line(python_shell);
342         return TRUE;
343     } else if (event && event->keyval == GDK_KEY_Up) {
344         parasite_python_shell_replace_input(python_shell,
345             parasite_python_shell_get_history_back(python_shell));
346         return TRUE;
347     } else if (event && event->keyval == GDK_KEY_Down) {
348         parasite_python_shell_replace_input(python_shell,
349             parasite_python_shell_get_history_forward(python_shell));
350         return TRUE;
351     } else if (event && event->string != NULL) {
352         ParasitePythonShellPrivate *priv =
353             PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
354         GtkTextBuffer *buffer =
355             gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
356         GtkTextMark *insert_mark = gtk_text_buffer_get_insert(buffer);
357         GtkTextMark *selection_mark =
358             gtk_text_buffer_get_selection_bound(buffer);
359         GtkTextIter insert_iter;
360         GtkTextIter selection_iter;
361         GtkTextIter start_iter;
362         gint cmp_start_insert;
363         gint cmp_start_select;
364         gint cmp_insert_select;
365
366         gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
367                                          priv->line_start_mark);
368         gtk_text_buffer_get_iter_at_mark(buffer, &insert_iter, insert_mark);
369         gtk_text_buffer_get_iter_at_mark(buffer, &selection_iter,
370                                          selection_mark);
371
372         cmp_start_insert = gtk_text_iter_compare(&start_iter, &insert_iter);
373         cmp_start_select = gtk_text_iter_compare(&start_iter, &selection_iter);
374         cmp_insert_select = gtk_text_iter_compare(&insert_iter,
375                                                   &selection_iter);
376
377         if (cmp_start_insert == 0 && cmp_start_select == 0 &&
378             event && (event->keyval == GDK_KEY_BackSpace ||
379              event->keyval == GDK_KEY_Left)) {
380             return TRUE;
381         }
382         if (cmp_start_insert <= 0 && cmp_start_select <= 0)
383             return FALSE;
384         else if (cmp_start_insert > 0 && cmp_start_select > 0)
385             gtk_text_buffer_place_cursor(buffer, &start_iter);
386         else if (cmp_insert_select < 0)
387             gtk_text_buffer_move_mark(buffer, insert_mark, &start_iter);
388         else if (cmp_insert_select > 0)
389             gtk_text_buffer_move_mark(buffer, selection_mark, &start_iter);
390     }
391
392     return FALSE;
393 }
394
395 GtkWidget *
396 parasite_python_shell_new(void)
397 {
398     return g_object_new(PARASITE_TYPE_PYTHON_SHELL, NULL);
399 }
400
401 void
402 parasite_python_shell_append_text(ParasitePythonShell *python_shell,
403                                   const char *str,
404                                   const char *tag)
405 {
406     ParasitePythonShellPrivate *priv =
407         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
408
409     GtkTextIter end;
410     GtkTextBuffer *buffer =
411         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
412     GtkTextMark *mark = gtk_text_buffer_get_insert(buffer);
413
414     gtk_text_buffer_get_end_iter(buffer, &end);
415     gtk_text_buffer_move_mark(buffer, mark, &end);
416     gtk_text_buffer_insert_with_tags_by_name(buffer, &end, str, -1, tag, NULL);
417     gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(priv->textview), mark,
418                                  0, TRUE, 0, 1);
419 }
420
421 void
422 parasite_python_shell_focus(ParasitePythonShell *python_shell)
423 {
424    gtk_widget_grab_focus(PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell)->textview);
425 }
426
427 // vim: set et ts=4: