blob: 9d1f35ff100ad9cab79590170cc65a19aed55a98 [file] [log] [blame]
Bram Moolenaare4f25e42017-07-07 11:54:15 +02001/* vi:set ts=8 sts=4 sw=4 noet:
2 *
3 * VIM - Vi IMproved by Bram Moolenaar
4 *
5 * Do ":help uganda" in Vim to read copying and usage conditions.
6 * Do ":help credits" in Vim to see a list of people who contributed.
7 * See README.txt for an overview of the Vim source code.
8 */
9
10/*
11 * Terminal window support, see ":help :terminal".
12 *
13 * For a terminal one VTerm is constructed. This uses libvterm. A copy of
14 * that library is in the libvterm directory.
15 *
16 * The VTerm invokes callbacks when its screen contents changes. The line
17 * range is stored in tl_dirty_row_start and tl_dirty_row_end. Once in a
Bram Moolenaar938783d2017-07-16 20:13:26 +020018 * while, if the terminal window is visible, the screen contents is drawn.
Bram Moolenaare4f25e42017-07-07 11:54:15 +020019 *
20 * If the terminal window has keyboard focus, typed keys are converted to the
21 * terminal encoding and writting to the job over a channel.
22 *
23 * If the job produces output, it is written to the VTerm.
24 * This will result in screen updates.
25 *
26 * TODO:
Bram Moolenaar938783d2017-07-16 20:13:26 +020027 * - pressing Enter sends two CR and/or NL characters to "bash -i"?
Bram Moolenaar96ca27a2017-07-17 23:20:24 +020028 * Passing Enter as NL seems to work.
Bram Moolenaare4f25e42017-07-07 11:54:15 +020029 * - set buffer options to be scratch, hidden, nomodifiable, etc.
30 * - set buffer name to command, add (1) to avoid duplicates.
Bram Moolenaar96ca27a2017-07-17 23:20:24 +020031 * - If [command] is not given the 'shell' option is used.
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +020032 * - if the job ends, write "-- JOB ENDED --" in the terminal
Bram Moolenaar938783d2017-07-16 20:13:26 +020033 * - when closing window and job ended, delete the terminal
34 * - when closing window and job has not ended, make terminal hidden?
35 * - Use a pty for I/O with the job.
36 * - Windows implementation:
37 * (WiP): https://github.com/mattn/vim/tree/terminal
38 * src/os_win32.c mch_open_terminal()
39 Using winpty ?
40 * - command line completion for :terminal
Bram Moolenaare4f25e42017-07-07 11:54:15 +020041 * - support fixed size when 'termsize' is "rowsXcols".
42 * - support minimal size when 'termsize' is "rows*cols".
43 * - support minimal size when 'termsize' is empty.
44 * - implement ":buf {term-buf-name}"
Bram Moolenaar96ca27a2017-07-17 23:20:24 +020045 * - implement term_list() list of buffers with a terminal
46 * - implement term_getsize(buf)
47 * - implement term_setsize(buf)
48 * - implement term_sendkeys(buf, keys) send keystrokes to a terminal
49 * - implement term_wait(buf) wait for screen to be updated
50 * - implement term_scrape(buf, row) inspect terminal screen
51 * - implement term_open(command, options) open terminal window
52 * - implement term_getjob(buf)
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +020053 * - implement 'termkey'
Bram Moolenaare4f25e42017-07-07 11:54:15 +020054 */
55
56#include "vim.h"
57
58#ifdef FEAT_TERMINAL
59
60#include "libvterm/include/vterm.h"
61
62/* typedef term_T in structs.h */
63struct terminal_S {
64 term_T *tl_next;
65
66 VTerm *tl_vterm;
67 job_T *tl_job;
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +020068 buf_T *tl_buffer;
Bram Moolenaare4f25e42017-07-07 11:54:15 +020069
70 /* Range of screen rows to update. Zero based. */
71 int tl_dirty_row_start; /* -1 if nothing dirty */
72 int tl_dirty_row_end; /* row below last one to update */
73
74 pos_T tl_cursor;
75};
76
77#define MAX_ROW 999999 /* used for tl_dirty_row_end to update all rows */
78
79/*
80 * List of all active terminals.
81 */
82static term_T *first_term = NULL;
83
84static int handle_damage(VTermRect rect, void *user);
85static int handle_moverect(VTermRect dest, VTermRect src, void *user);
86static int handle_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user);
87static int handle_resize(int rows, int cols, void *user);
88
89static VTermScreenCallbacks screen_callbacks = {
90 handle_damage, /* damage */
91 handle_moverect, /* moverect */
92 handle_movecursor, /* movecursor */
93 NULL, /* settermprop */
94 NULL, /* bell */
95 handle_resize, /* resize */
96 NULL, /* sb_pushline */
97 NULL /* sb_popline */
98};
99
100/*
101 * ":terminal": open a terminal window and execute a job in it.
102 */
103 void
104ex_terminal(exarg_T *eap)
105{
106 int rows;
107 int cols;
108 exarg_T split_ea;
109 win_T *old_curwin = curwin;
110 typval_T argvars[2];
111 term_T *term;
112 VTerm *vterm;
113 VTermScreen *screen;
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200114 jobopt_T opt;
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200115
116 if (check_restricted() || check_secure())
117 return;
118
119 term = (term_T *)alloc_clear(sizeof(term_T));
120 if (term == NULL)
121 return;
122 term->tl_dirty_row_end = MAX_ROW;
123
124 /* Open a new window or tab. */
125 vim_memset(&split_ea, 0, sizeof(split_ea));
126 split_ea.cmdidx = CMD_new;
127 split_ea.cmd = (char_u *)"new";
128 split_ea.arg = (char_u *)"";
129 ex_splitview(&split_ea);
130 if (curwin == old_curwin)
131 {
132 /* split failed */
133 vim_free(term);
134 return;
135 }
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200136 term->tl_buffer = curbuf;
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200137
138 curbuf->b_term = term;
139 term->tl_next = first_term;
140 first_term = term;
141
142 /* TODO: set buffer type, hidden, etc. */
143
144 if (*curwin->w_p_tms != NUL)
145 {
146 char_u *p = vim_strchr(curwin->w_p_tms, 'x') + 1;
147
148 rows = atoi((char *)curwin->w_p_tms);
149 cols = atoi((char *)p);
150 /* TODO: resize window if possible. */
151 }
152 else
153 {
154 rows = curwin->w_height;
155 cols = curwin->w_width;
156 }
157
158 vterm = vterm_new(rows, cols);
159 term->tl_vterm = vterm;
160 screen = vterm_obtain_screen(vterm);
161 vterm_screen_set_callbacks(screen, &screen_callbacks, term);
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200162 /* TODO: depends on 'encoding'. */
163 vterm_set_utf8(vterm, 1);
164 /* Required to initialize most things. */
165 vterm_screen_reset(screen, 1 /* hard */);
166
167 /* By default NL means CR-NL. */
168 vterm_input_write(vterm, "\x1b[20h", 5);
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200169
170 argvars[0].v_type = VAR_STRING;
171 argvars[0].vval.v_string = eap->arg;
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200172
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200173 clear_job_options(&opt);
174 opt.jo_mode = MODE_RAW;
175 opt.jo_out_mode = MODE_RAW;
176 opt.jo_err_mode = MODE_RAW;
177 opt.jo_set = JO_MODE | JO_OUT_MODE | JO_ERR_MODE;
178 opt.jo_io[PART_OUT] = JIO_BUFFER;
179 opt.jo_io[PART_ERR] = JIO_BUFFER;
180 opt.jo_set |= JO_OUT_IO + (JO_OUT_IO << (PART_ERR - PART_OUT));
181 opt.jo_io_buf[PART_OUT] = curbuf->b_fnum;
182 opt.jo_io_buf[PART_ERR] = curbuf->b_fnum;
183 opt.jo_set |= JO_OUT_BUF + (JO_OUT_BUF << (PART_ERR - PART_OUT));
184
185 term->tl_job = job_start(argvars, &opt);
186
Bram Moolenaar96ca27a2017-07-17 23:20:24 +0200187 if (term->tl_job == NULL)
188 /* Wiping out the buffer will also close the window. */
189 do_buffer(DOBUF_WIPE, DOBUF_CURRENT, FORWARD, 0, TRUE);
190
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200191 /* Setup pty, see mch_call_shell(). */
192}
193
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200194/*
Bram Moolenaar96ca27a2017-07-17 23:20:24 +0200195 * Free a terminal and everything it refers to.
196 * Kills the job if there is one.
197 * Called when wiping out a buffer.
198 */
199 void
200free_terminal(term_T *term)
201{
202 term_T *tp;
203
204 if (term == NULL)
205 return;
206 if (first_term == term)
207 first_term = term->tl_next;
208 else
209 for (tp = first_term; tp->tl_next != NULL; tp = tp->tl_next)
210 if (tp->tl_next == term)
211 {
212 tp->tl_next = term->tl_next;
213 break;
214 }
215
216 if (term->tl_job != NULL)
217 {
218 if (term->tl_job->jv_status != JOB_ENDED)
219 job_stop(term->tl_job, NULL, "kill");
220 job_unref(term->tl_job);
221 }
222
223 vterm_free(term->tl_vterm);
224 vim_free(term);
225}
226
227/*
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200228 * Invoked when "msg" output from a job was received. Write it to the terminal
229 * of "buffer".
230 */
231 void
232write_to_term(buf_T *buffer, char_u *msg, channel_T *channel)
233{
234 size_t len = STRLEN(msg);
235 VTerm *vterm = buffer->b_term->tl_vterm;
236
237 ch_logn(channel, "writing %d bytes to terminal", (int)len);
238 vterm_input_write(vterm, (char *)msg, len);
239 vterm_screen_flush_damage(vterm_obtain_screen(vterm));
240
241 /* TODO: only update once in a while. */
242 update_screen(0);
243 setcursor();
244 out_flush();
245}
246
247/*
248 * Called to update the window that contains the terminal.
249 */
250 void
251term_update_window(win_T *wp)
252{
253 int vterm_rows;
254 int vterm_cols;
255 VTerm *vterm = wp->w_buffer->b_term->tl_vterm;
256 VTermScreen *screen = vterm_obtain_screen(vterm);
257 VTermPos pos;
258
259 vterm_get_size(vterm, &vterm_rows, &vterm_cols);
260
261 /* TODO: Only redraw what changed. */
262 for (pos.row = 0; pos.row < wp->w_height; ++pos.row)
263 {
264 int off = screen_get_current_line_off();
265
266 if (pos.row < vterm_rows)
267 for (pos.col = 0; pos.col < wp->w_width && pos.col < vterm_cols;
268 ++pos.col)
269 {
270 VTermScreenCell cell;
271 int c;
272
273 vterm_screen_get_cell(screen, pos, &cell);
274 /* TODO: use cell.attrs and colors */
275 /* TODO: use cell.width */
276 /* TODO: multi-byte chars */
277 c = cell.chars[0];
278 ScreenLines[off] = c == NUL ? ' ' : c;
279 ScreenAttrs[off] = 0;
280 ++off;
281 }
282
283 screen_line(wp->w_winrow + pos.row, wp->w_wincol, pos.col, wp->w_width,
284 FALSE);
285 }
286}
287
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200288 static int
289handle_damage(VTermRect rect, void *user)
290{
291 term_T *term = (term_T *)user;
292
293 term->tl_dirty_row_start = MIN(term->tl_dirty_row_start, rect.start_row);
294 term->tl_dirty_row_end = MAX(term->tl_dirty_row_end, rect.end_row);
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200295 redraw_buf_later(term->tl_buffer, NOT_VALID);
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200296 return 1;
297}
298
299 static int
300handle_moverect(VTermRect dest, VTermRect src, void *user)
301{
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200302 term_T *term = (term_T *)user;
303
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200304 /* TODO */
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200305 redraw_buf_later(term->tl_buffer, NOT_VALID);
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200306 return 1;
307}
308
309 static int
310handle_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
311{
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200312 term_T *term = (term_T *)user;
313 win_T *wp;
314 int is_current = FALSE;
315
316 FOR_ALL_WINDOWS(wp)
317 {
318 if (wp->w_buffer == term->tl_buffer)
319 {
320 /* TODO: limit to window size? */
321 wp->w_wrow = pos.row;
322 wp->w_wcol = pos.col;
323 if (wp == curwin)
324 is_current = TRUE;
325 }
326 }
327
328 if (is_current)
329 {
330 setcursor();
331 out_flush();
332 }
333
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200334 return 1;
335}
336
337 static int
338handle_resize(int rows, int cols, void *user)
339{
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200340 term_T *term = (term_T *)user;
341
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200342 /* TODO: handle terminal resize. */
Bram Moolenaarcb8bbe92017-07-16 13:48:22 +0200343 redraw_buf_later(term->tl_buffer, NOT_VALID);
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200344 return 1;
345}
346
347/* TODO: Use win_del_lines() to make scroll up efficient. */
348
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200349
Bram Moolenaar938783d2017-07-16 20:13:26 +0200350/*
351 * Wait for input and send it to the job.
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200352 * Return when a CTRL-W command is typed that moves to another window.
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200353 */
Bram Moolenaar938783d2017-07-16 20:13:26 +0200354 void
355terminal_loop(void)
356{
357 VTerm *vterm = curbuf->b_term->tl_vterm;
358 char buf[200];
359
360 for (;;)
361 {
362 int c;
363 VTermKey key = VTERM_KEY_NONE;
364 VTermModifier mod = VTERM_MOD_NONE;
365 size_t len;
366
367 update_screen(0);
368 setcursor();
369 out_flush();
370
371 c = vgetc();
372 switch (c)
373 {
374 case Ctrl_W:
375 stuffcharReadbuff(Ctrl_W);
376 return;
377
Bram Moolenaar96ca27a2017-07-17 23:20:24 +0200378 /* TODO: which of these two should be used? */
379#if 0
Bram Moolenaar938783d2017-07-16 20:13:26 +0200380 case CAR: key = VTERM_KEY_ENTER; break;
Bram Moolenaar96ca27a2017-07-17 23:20:24 +0200381#else
382 case CAR: c = NL; break;
383#endif
Bram Moolenaar938783d2017-07-16 20:13:26 +0200384 case ESC: key = VTERM_KEY_ESCAPE; break;
385 case K_BS: key = VTERM_KEY_BACKSPACE; break;
386 case K_DEL: key = VTERM_KEY_DEL; break;
387 case K_DOWN: key = VTERM_KEY_DOWN; break;
388 case K_END: key = VTERM_KEY_END; break;
389 case K_F10: key = VTERM_KEY_FUNCTION(10); break;
390 case K_F11: key = VTERM_KEY_FUNCTION(11); break;
391 case K_F12: key = VTERM_KEY_FUNCTION(12); break;
392 case K_F1: key = VTERM_KEY_FUNCTION(1); break;
393 case K_F2: key = VTERM_KEY_FUNCTION(2); break;
394 case K_F3: key = VTERM_KEY_FUNCTION(3); break;
395 case K_F4: key = VTERM_KEY_FUNCTION(4); break;
396 case K_F5: key = VTERM_KEY_FUNCTION(5); break;
397 case K_F6: key = VTERM_KEY_FUNCTION(6); break;
398 case K_F7: key = VTERM_KEY_FUNCTION(7); break;
399 case K_F8: key = VTERM_KEY_FUNCTION(8); break;
400 case K_F9: key = VTERM_KEY_FUNCTION(9); break;
401 case K_HOME: key = VTERM_KEY_HOME; break;
402 case K_INS: key = VTERM_KEY_INS; break;
403 case K_K0: key = VTERM_KEY_KP_0; break;
404 case K_K1: key = VTERM_KEY_KP_1; break;
405 case K_K2: key = VTERM_KEY_KP_2; break;
406 case K_K3: key = VTERM_KEY_KP_3; break;
407 case K_K4: key = VTERM_KEY_KP_4; break;
408 case K_K5: key = VTERM_KEY_KP_5; break;
409 case K_K6: key = VTERM_KEY_KP_6; break;
410 case K_K7: key = VTERM_KEY_KP_7; break;
411 case K_K8: key = VTERM_KEY_KP_8; break;
412 case K_K9: key = VTERM_KEY_KP_9; break;
413 case K_KDEL: key = VTERM_KEY_DEL; break; /* TODO */
414 case K_KDIVIDE: key = VTERM_KEY_KP_DIVIDE; break;
415 case K_KEND: key = VTERM_KEY_KP_1; break; /* TODO */
416 case K_KENTER: key = VTERM_KEY_KP_ENTER; break;
417 case K_KHOME: key = VTERM_KEY_KP_7; break; /* TODO */
418 case K_KINS: key = VTERM_KEY_KP_0; break; /* TODO */
419 case K_KMINUS: key = VTERM_KEY_KP_MINUS; break;
420 case K_KMULTIPLY: key = VTERM_KEY_KP_MULT; break;
421 case K_KPAGEDOWN: key = VTERM_KEY_KP_3; break; /* TODO */
422 case K_KPAGEUP: key = VTERM_KEY_KP_9; break; /* TODO */
423 case K_KPLUS: key = VTERM_KEY_KP_PLUS; break;
424 case K_KPOINT: key = VTERM_KEY_KP_PERIOD; break;
425 case K_LEFT: key = VTERM_KEY_LEFT; break;
426 case K_PAGEDOWN: key = VTERM_KEY_PAGEDOWN; break;
427 case K_PAGEUP: key = VTERM_KEY_PAGEUP; break;
428 case K_RIGHT: key = VTERM_KEY_RIGHT; break;
429 case K_UP: key = VTERM_KEY_UP; break;
430 case TAB: key = VTERM_KEY_TAB; break;
431 }
432
433 /*
434 * Convert special keys to vterm keys:
435 * - Write keys to vterm: vterm_keyboard_key()
436 * - Write output to channel.
437 */
438 if (key != VTERM_KEY_NONE)
439 /* Special key, let vterm convert it. */
440 vterm_keyboard_key(vterm, key, mod);
441 else
442 /* Normal character, let vterm convert it. */
443 vterm_keyboard_unichar(vterm, c, mod);
444
445 /* Read back the converted escape sequence. */
446 len = vterm_output_read(vterm, buf, sizeof(buf));
447
448 /* TODO: if FAIL is returned, stop? */
449 channel_send(curbuf->b_term->tl_job->jv_channel, PART_IN,
450 (char_u *)buf, len, NULL);
451 }
452}
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200453
Bram Moolenaare4f25e42017-07-07 11:54:15 +0200454#endif /* FEAT_TERMINAL */