1 /* pk-repl.c - A REPL ui for poke. */
2
3 /* Copyright (C) 2019, 2020, 2021 Jose E. Marchesi */
4
5 /* This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <config.h>
20
21 #include <signal.h>
22 #include <pthread.h>
23 #include <unistd.h>
24 #include <setjmp.h>
25 #include <stdlib.h>
26 #include "readline.h"
27 #if defined HAVE_READLINE_HISTORY_H
28 # include <readline/history.h>
29 #endif
30 #include "xalloc.h"
31 #include "xstrndup.h"
32
33 #include "poke.h"
34 #include "pk-cmd.h"
35 #if HAVE_HSERVER
36 # include "pk-hserver.h"
37 #endif
38 #include "pk-utils.h"
39 #include "pk-map.h"
40
41 /* The thread that contains the non-local entry point for reentering
42 the REPL. */
43 static pthread_t volatile ctrlc_thread;
44 /* The non-local entry point for reentering the REPL. */
45 static sigjmp_buf /*volatile*/ ctrlc_buf;
46 /* When nonzero, ctrlc_thread and ctrlc_buf contain valid values. */
47 static int volatile ctrlc_buf_valid;
48
49 /* This function is called repeatedly by the readline library, when
50 generating potential command line completions.
51
52 TEXT is the partial word to be completed. STATE is zero the first
53 time the function is called and a positive non-zero integer for
54 each subsequent call.
55
56 On each call, the function returns a potential completion. It
57 returns NULL to indicate that there are no more possibilities left. */
58 static char *
poke_completion_function(const char * text,int state)59 poke_completion_function (const char *text, int state)
60 {
61 /* First try to complete with "normal" commands. */
62 char *function_name = pk_completion_function (poke_compiler,
63 text, state);
64
65 /* Then try with dot-commands. */
66 if (function_name == NULL)
67 function_name = pk_cmd_get_next_match (text, strlen (text));
68
69 return function_name;
70 }
71
72 /* A trivial completion function. No completions are possible. */
73 static char *
null_completion_function(const char * x,int state)74 null_completion_function (const char *x, int state)
75 {
76 return NULL;
77 }
78
79 char * doc_completion_function (const char *x, int state);
80
81 #define SPACE_SUBSTITUTE '/'
82
83 /* Display the list of matches, replacing SPACE_SUBSTITUTE with
84 a space. */
85 static void
space_substitute_display_matches(char ** matches,int num_matches,int max_length)86 space_substitute_display_matches (char **matches, int num_matches,
87 int max_length)
88 {
89 for (int i = 0; i < num_matches + 1; ++i)
90 {
91 for (char *m = matches[i]; *m; ++m)
92 {
93 if (*m == SPACE_SUBSTITUTE)
94 *m = ' ';
95 }
96 }
97
98 rl_display_match_list (matches, num_matches, max_length);
99 }
100
101 /* Display the rl_line_buffer substituting
102 SPACE_SUBSTITUTE with a space. */
103 static void
space_substitute_redisplay(void)104 space_substitute_redisplay (void)
105 {
106 /* Take a copy of the line_buffer. */
107 char *olb = xstrdup (rl_line_buffer);
108
109 for (char *x = rl_line_buffer; *x ; x++)
110 {
111 if (*x == SPACE_SUBSTITUTE)
112 *x = ' ';
113 }
114
115 rl_redisplay ();
116
117 /* restore the line_buffer to its original state. */
118 strcpy (rl_line_buffer, olb);
119 free (olb);
120 }
121
122 /* Readline's getc callback.
123 Use this function to update the completer which
124 should be used.
125 */
126 static int
poke_getc(FILE * stream)127 poke_getc (FILE *stream)
128 {
129 char *line_to_point = xstrndup (rl_line_buffer, rl_point ? rl_point - 1 : 0);
130 char *tok = strtok (line_to_point, "\t ");
131 const struct pk_cmd *cmd = pk_cmd_find (tok);
132
133 free (line_to_point);
134
135 if (cmd == NULL)
136 rl_completion_entry_function = poke_completion_function;
137
138 if (rl_completion_entry_function == poke_completion_function)
139 {
140 if (cmd)
141 {
142 if (cmd->completer)
143 rl_completion_entry_function = cmd->completer;
144 else
145 rl_completion_entry_function = null_completion_function;
146 }
147 }
148
149 int c = rl_getc (stream);
150
151 /* Due to readline's apparent inability to change the word break
152 character in the middle of a line, we have to do some underhand
153 skullduggery here. Spaces are substituted with SPACE_SUBSTITUTE,
154 and then substituted back again in various callback functions. */
155 if (rl_completion_entry_function == doc_completion_function)
156 {
157 rl_completion_display_matches_hook = space_substitute_display_matches;
158 rl_redisplay_function = space_substitute_redisplay;
159
160 if (c == ' ')
161 c = SPACE_SUBSTITUTE;
162 }
163 else
164 {
165 rl_completion_display_matches_hook = NULL;
166 rl_redisplay_function = rl_redisplay;
167 }
168
169 return c;
170 }
171
172
173 static void
banner(void)174 banner (void)
175 {
176 if (!poke_quiet_p)
177 {
178 pk_print_version (1 /* hand_p */);
179 pk_puts ("\n");
180
181 #if HAVE_HSERVER
182 if (poke_hserver_p)
183 {
184 pk_printf ("hserver listening in port %d.\n",
185 pk_hserver_port ());
186 pk_puts ("\n");
187 }
188 #endif
189
190 #if HAVE_HSERVER
191 if (poke_hserver_p)
192 {
193 char *help_hyperlink
194 = pk_hserver_make_hyperlink ('e', ".help");
195
196 pk_puts (_("For help, type \""));
197 pk_term_hyperlink (help_hyperlink, NULL);
198 pk_puts (".help");
199 pk_term_end_hyperlink ();
200 pk_puts ("\".\n");
201 free (help_hyperlink);
202 }
203 else
204 #endif
205 pk_puts (_("For help, type \".help\".\n"));
206 pk_puts (_("Type \".exit\" to leave the program.\n"));
207 }
208
209 }
210
211 static _GL_ASYNC_SAFE void
poke_sigint_handler(int sig)212 poke_sigint_handler (int sig)
213 {
214 if (ctrlc_buf_valid)
215 {
216 /* Abort the current command, and return to the REPL. */
217 if (!pthread_equal (pthread_self (), ctrlc_thread))
218 {
219 /* We are not in the thread that established the jmp_buf for
220 returning to the REPL. Redirect the signal to that thread,
221 and decline responsibility for future deliveries of this
222 signal. */
223 sigset_t mask;
224 if (sigemptyset (&mask) == 0
225 && sigaddset (&mask, SIGINT) == 0)
226 pthread_sigmask (SIG_BLOCK, &mask, NULL);
227
228 pthread_kill (ctrlc_thread, SIGINT);
229 }
230 else
231 {
232 /* We are in the thread that established the jmp_buf for returning
233 to the REPL. Put the readline library into a sane state, then
234 unwind the stack. */
235 rl_free_line_state ();
236 rl_cleanup_after_signal ();
237 rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0;
238 printf ("\n");
239 siglongjmp (ctrlc_buf, 1);
240 }
241 }
242 else
243 {
244 /* The default behaviour for SIGINT is to terminate the process.
245 Let's do that here. */
246 struct sigaction sa;
247 sa.sa_handler = SIG_DFL;
248 sa.sa_flags = 0;
249 sigemptyset (&sa.sa_mask);
250 sigaction (SIGINT, &sa, NULL);
251 raise (SIGINT);
252 }
253 }
254
255 /* Return a copy of TEXT, with every instance of the space character
256 prepended with the backslash character. The caller is responsible
257 for freeing the result. */
258 static char *
escape_metacharacters(char * text,int match_type,char * qp)259 escape_metacharacters (char *text, int match_type, char *qp)
260 {
261 char *p = text;
262 char *r = xmalloc (strlen (text) * 2 + 1);
263 char *s = r;
264
265 while (*p)
266 {
267 char c = *p++;
268 if (c == ' ')
269 *r++ = '\\';
270 *r++ = c;
271 }
272 *r = '\0';
273
274 return s;
275 }
276
277 static char *
pk_prompt(void)278 pk_prompt (void)
279 {
280 char *prompt = "";
281
282 if (poke_prompt_maps_p)
283 {
284 pk_ios cur_ios;
285
286 cur_ios = pk_ios_cur (poke_compiler);
287 if (cur_ios)
288 {
289 pk_map maps
290 = pk_map_get_maps (pk_ios_get_id (cur_ios));
291
292 if (maps)
293 {
294 pk_map map;
295
296 prompt = pk_str_concat (prompt, "[", NULL);
297 for (map = maps; map; map = PK_MAP_CHAIN (map))
298 prompt = pk_str_concat (prompt, PK_MAP_NAME (map),
299 PK_MAP_CHAIN (map) != NULL ? "," : NULL,
300 NULL);
301 prompt = pk_str_concat (prompt, "]", NULL);
302 }
303 }
304 }
305
306 prompt = pk_str_concat (prompt, "(poke) ", NULL);
307 return prompt;
308 }
309
310 void
pk_repl(void)311 pk_repl (void)
312 {
313 banner ();
314
315 /* Arrange for the current line to be cancelled on SIGINT.
316 Since some library code is also interested in SIGINT
317 (GNU libtextstyle, via gnulib module fatal-signal), it is better
318 to install it once, rather than to install and uninstall it once
319 in each round of the REP loop below. */
320 ctrlc_buf_valid = 0;
321 struct sigaction sa;
322 sa.sa_handler = poke_sigint_handler;
323 sa.sa_flags = 0;
324 sigemptyset (&sa.sa_mask);
325 sigaction (SIGINT, &sa, NULL);
326
327 #if defined HAVE_READLINE_HISTORY_H
328 char *poke_history = NULL;
329 /* Load the user's history file ~/.poke_history, if it exists
330 in the HOME directory. */
331 char *homedir = getenv ("HOME");
332
333 if (homedir != NULL)
334 {
335 if (asprintf (&poke_history, "%s/.poke_history", homedir) != -1)
336 {
337 if (access (poke_history, R_OK) == 0)
338 read_history (poke_history);
339 }
340 else
341 poke_history = NULL;
342 }
343 #endif
344 rl_getc_function = poke_getc;
345 rl_completer_quote_characters = "\"";
346 rl_filename_quote_characters = " ";
347 rl_filename_quoting_function = escape_metacharacters;
348
349 ctrlc_thread = pthread_self ();
350 sigsetjmp (ctrlc_buf, 1);
351 ctrlc_buf_valid = 1;
352
353 while (!poke_exit_p)
354 {
355 char *prompt;
356 char *line;
357
358 pk_term_flush ();
359 rl_completion_entry_function = poke_completion_function;
360 prompt = pk_prompt ();
361 line = readline (prompt);
362 free (prompt);
363 if (line == NULL)
364 {
365 /* EOF in stdin (probably Ctrl-D). */
366 pk_puts ("\n");
367 break;
368 }
369
370 if (rl_completion_entry_function == doc_completion_function)
371 {
372 for (char *s = line; *s; ++s)
373 {
374 if (*s == SPACE_SUBSTITUTE)
375 *s = ' ';
376 }
377 }
378
379 /* Ignore empty lines. */
380 if (*line != '\0')
381 {
382 #if defined HAVE_READLINE_HISTORY_H
383 add_history (line);
384 #endif
385
386 pk_cmd_exec (line);
387 }
388 free (line);
389 }
390 #if defined HAVE_READLINE_HISTORY_H
391 if (poke_history) {
392 write_history (poke_history);
393 free (poke_history);
394 }
395 #endif
396
397 ctrlc_buf_valid = 0;
398 }
399
400 static int saved_point;
401 static int saved_end;
402
403 void
pk_repl_display_begin(void)404 pk_repl_display_begin (void)
405 {
406 saved_point = rl_point;
407 saved_end = rl_end;
408 rl_point = rl_end = 0;
409
410 rl_save_prompt ();
411 rl_clear_message ();
412
413 pk_puts (rl_prompt);
414 }
415
416 void
pk_repl_display_end(void)417 pk_repl_display_end (void)
418 {
419 pk_term_flush ();
420 rl_restore_prompt ();
421 rl_point = saved_point;
422 rl_end = saved_end;
423 rl_forced_update_display ();
424 }
425
426 void
pk_repl_insert(const char * str)427 pk_repl_insert (const char *str)
428 {
429 rl_insert_text (str);
430 rl_redisplay ();
431 }
432