-*terminal.txt* For Vim version 8.0. Last change: 2017 Jul 28
+*terminal.txt* For Vim version 8.0. Last change: 2017 Jul 30
VIM REFERENCE MANUAL by Bram Moolenaar
The job runs asynchronously from Vim, the window will be updated to show
output from the job, also while editing in any other window.
+
Typing ~
When the keyboard focus is in the terminal window, typed keys will be send to
the job. This uses a pty when possible. You can click outside of the
terminal window to move keyboard focus elsewhere.
-Navigate between windows with CTRL-W commands. E.g. CTRL-W CTRL-W moves focus
-to the next window. Use "CTRL-W :" to edit an Ex command. Use "CTRL-W ." to
-send a CTRL-W to the job in the terminal.
+CTRL-W can be used to navigate between windows and other CTRL-W commands, e.g.:
+ CTRL-W CTRL-W move focus to the next window
+ CTRL-W : enter an Ex command
+See |CTRL-W| for more commands.
+
+Special in the terminal window: *CTRL-W_.* *CTRL-W_N*
+ CTRL-W . send a CTRL-W to the job in the terminal
+ CTRL-W N go to Terminal Normal mode, see |Terminal-mode|
+
+See option 'termkey' for specifying another key instead of CTRL-W that
+will work like CTRL-W. However, typing 'termkey' twice sends 'termkey' to
+the job. For example:
+ 'termkey' CTRL-W move focus to the next window
+ 'termkey' : enter an Ex command
+ 'termkey' 'termkey' send 'termkey' to the job in the terminal
+ 'termkey' . send a CTRL-W to the job in the terminal
+ 'termkey' N go to terminal Normal mode, see below
+ 'termkey' CTRL-N same as CTRL-W N
-See option 'termkey' for specifying another key that precedes a Vim command.
-Typing 'termkey' twice sends 'termkey' to the job.
Size ~
See option 'termsize' for controlling the size of the terminal window.
(TODO: scrolling when the terminal is larger than the window)
+
Syntax ~
:ter[minal] [command] *:ter* *:terminal*
not when 'termsize' is "rowsXcols".
+Terminal Normal mode ~
+ *Terminal-mode*
+When the job is running the contents of the terminal is under control of the
+job. That includes the cursor position. The terminal contents can change at
+any time.
+
+Use CTRL-W N (or 'termkey' N) to go to Terminal Normal mode. Now the contents
+of the terminal window is under control of Vim, the job output is suspended.
+ *E946*
+In this mode you can move the cursor around with the usual Vim commands,
+Visually mark text, yank text, etc. But you cannot change the contents of the
+buffer. The commands that would start insert mode, such as 'i' and 'a',
+return control of the window to the job. Any pending output will now be
+displayed.
+
+In Terminal mode the statusline and window title show "(Terminal)". If the
+job ends while in Terminal mode this changes to "(Terminal-finished)".
+
+
Unix ~
On Unix a pty is used to make it possible to run all kinds of commands. You
* that buffer, attributes come from the scrollback buffer tl_scrollback.
*
* TODO:
+ * - Problem with statusline (Zyx, Christian)
+ * - Make CTRL-W "" paste register content to the job?
+ * - in bash mouse clicks are inserting characters.
+ * - mouse scroll: when over other window, scroll that window.
* - For the scrollback buffer store lines in the buffer, only attributes in
* tl_scrollback.
+ * - Add term_status(): "" if not a terminal, "running" if job running,
+ * "finished" if finished, "running,vim" when job is running and in
+ * Terminal mode, "running,vim,pending" when job output is pending.
* - When the job ends:
* - Need an option or argument to drop the window+buffer right away, to be
- * used for a shell or Vim.
+ * used for a shell or Vim. 'termfinish'; "close", "open" (open window when
+ * job finishes).
+ * - add option values to the command:
+ * :term <24x80> <close> vim notes.txt
* - To set BS correctly, check get_stty(); Pass the fd of the pty.
- * - do not store terminal buffer in viminfo. Or prefix term:// ?
+ * - do not store terminal window in viminfo. Or prefix term:// ?
* - add a character in :ls output
* - when closing window and job has not ended, make terminal hidden?
* - when closing window and job has ended, make buffer hidden?
* - support minimal size when 'termsize' is empty?
* - implement "term" for job_start(): more job options when starting a
* terminal.
+ * - if the job in the terminal does not support the mouse, we can use the
+ * mouse in the Terminal window for copy/paste.
* - when 'encoding' is not utf-8, or the job is using another encoding, setup
* conversions.
* - In the GUI use a terminal emulator for :!cmd.
struct terminal_S {
term_T *tl_next;
+ VTerm *tl_vterm;
+ job_T *tl_job;
+ buf_T *tl_buffer;
+
+ int tl_terminal_mode;
+ int tl_channel_closed;
+
#ifdef WIN3264
void *tl_winpty_config;
void *tl_winpty;
#endif
- VTerm *tl_vterm;
- job_T *tl_job;
- buf_T *tl_buffer;
/* last known vterm size */
int tl_rows;
&& channel_is_open(term->tl_job->jv_channel);
}
+/*
+ * Add the last line of the scrollback buffer to the buffer in the window.
+ */
+ static void
+add_scrollback_line_to_buffer(term_T *term)
+{
+ linenr_T lnum = term->tl_scrollback.ga_len - 1;
+ sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + lnum;
+ garray_T ga;
+ int c;
+ int col;
+ int i;
+
+ ga_init2(&ga, 1, 100);
+ for (col = 0; col < line->sb_cols; col += line->sb_cells[col].width)
+ {
+ if (ga_grow(&ga, MB_MAXBYTES) == FAIL)
+ goto failed;
+ for (i = 0; (c = line->sb_cells[col].chars[i]) > 0 || i == 0; ++i)
+ ga.ga_len += mb_char2bytes(c == NUL ? ' ' : c,
+ (char_u *)ga.ga_data + ga.ga_len);
+ }
+ if (ga_grow(&ga, 1) == FAIL)
+ goto failed;
+ *((char_u *)ga.ga_data + ga.ga_len) = NUL;
+ ml_append_buf(term->tl_buffer, lnum, ga.ga_data, ga.ga_len + 1, FALSE);
+
+ if (lnum == 0)
+ {
+ /* Delete the empty line that was in the empty buffer. */
+ curbuf = term->tl_buffer;
+ ml_delete(2, FALSE);
+ curbuf = curwin->w_buffer;
+ }
+
+failed:
+ ga_clear(&ga);
+}
+
+/*
+ * Add the current lines of the terminal to scrollback and to the buffer.
+ * Called after the job has ended and when switching to Terminal mode.
+ */
+ static void
+move_terminal_to_buffer(term_T *term)
+{
+ win_T *wp;
+ int len;
+ int lines_skipped = 0;
+ VTermPos pos;
+ VTermScreenCell cell;
+ VTermScreenCell *p;
+ VTermScreen *screen = vterm_obtain_screen(term->tl_vterm);
+
+ for (pos.row = 0; pos.row < term->tl_rows; ++pos.row)
+ {
+ len = 0;
+ for (pos.col = 0; pos.col < term->tl_cols; ++pos.col)
+ if (vterm_screen_get_cell(screen, pos, &cell) != 0
+ && cell.chars[0] != NUL)
+ len = pos.col + 1;
+
+ if (len == 0)
+ ++lines_skipped;
+ else
+ {
+ while (lines_skipped > 0)
+ {
+ /* Line was skipped, add an empty line. */
+ --lines_skipped;
+ if (ga_grow(&term->tl_scrollback, 1) == OK)
+ {
+ sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
+ + term->tl_scrollback.ga_len;
+
+ line->sb_cols = 0;
+ line->sb_cells = NULL;
+ ++term->tl_scrollback.ga_len;
+
+ add_scrollback_line_to_buffer(term);
+ }
+ }
+
+ p = (VTermScreenCell *)alloc((int)sizeof(VTermScreenCell) * len);
+ if (p != NULL && ga_grow(&term->tl_scrollback, 1) == OK)
+ {
+ sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
+ + term->tl_scrollback.ga_len;
+
+ for (pos.col = 0; pos.col < len; ++pos.col)
+ {
+ if (vterm_screen_get_cell(screen, pos, &cell) == 0)
+ vim_memset(p + pos.col, 0, sizeof(cell));
+ else
+ p[pos.col] = cell;
+ }
+ line->sb_cols = len;
+ line->sb_cells = p;
+ ++term->tl_scrollback.ga_len;
+
+ add_scrollback_line_to_buffer(term);
+ }
+ else
+ vim_free(p);
+ }
+ }
+
+ FOR_ALL_WINDOWS(wp)
+ {
+ if (wp->w_buffer == term->tl_buffer)
+ {
+ wp->w_cursor.lnum = term->tl_buffer->b_ml.ml_line_count;
+ wp->w_cursor.col = 0;
+ wp->w_valid = 0;
+ redraw_win_later(wp, NOT_VALID);
+ }
+ }
+}
+
+ static void
+set_terminal_mode(term_T *term, int on)
+{
+ term->tl_terminal_mode = on;
+ vim_free(term->tl_status_text);
+ term->tl_status_text = NULL;
+ if (term->tl_buffer == curbuf)
+ maketitle();
+}
+
+/*
+ * Called after the job if finished and Terminal mode is not active:
+ * Move the vterm contents into the scrollback buffer and free the vterm.
+ */
+ static void
+cleanup_vterm(term_T *term)
+{
+ move_terminal_to_buffer(term);
+ term_free_vterm(term);
+ set_terminal_mode(term, FALSE);
+}
+
+/*
+ * Switch from sending keys to the job to Terminal-Normal mode.
+ * Suspends updating the terminal window.
+ */
+ static void
+term_enter_terminal_mode()
+{
+ term_T *term = curbuf->b_term;
+
+ /* Append the current terminal contents to the buffer. */
+ move_terminal_to_buffer(term);
+
+ set_terminal_mode(term, TRUE);
+}
+
+/*
+ * Returns TRUE if the current window contains a terminal and we are in
+ * Terminal-Normal mode.
+ */
+ int
+term_in_terminal_mode()
+{
+ term_T *term = curbuf->b_term;
+
+ return term != NULL && term->tl_terminal_mode;
+}
+
+/*
+ * Switch from Terminal-Normal mode to sending keys to the job.
+ * Restores updating the terminal window.
+ */
+ void
+term_leave_terminal_mode()
+{
+ term_T *term = curbuf->b_term;
+ sb_line_T *line;
+ garray_T *gap;
+
+ /* Remove the terminal contents from the scrollback and the buffer. */
+ gap = &term->tl_scrollback;
+ while (curbuf->b_ml.ml_line_count > term->tl_scrollback_scrolled)
+ {
+ ml_delete(curbuf->b_ml.ml_line_count, FALSE);
+ line = (sb_line_T *)gap->ga_data + gap->ga_len - 1;
+ vim_free(line->sb_cells);
+ --gap->ga_len;
+ if (gap->ga_len == 0)
+ break;
+ }
+ check_cursor();
+
+ set_terminal_mode(term, FALSE);
+
+ if (term->tl_channel_closed)
+ cleanup_vterm(term);
+ redraw_buf_and_status_later(curbuf, NOT_VALID);
+}
+
/*
* Get a key from the user without mapping.
* TODO: use terminal mode mappings.
return OK;
}
+/*
+ * Returns TRUE if the current window contains a terminal and we are sending
+ * keys to the job.
+ */
+ int
+term_use_loop()
+{
+ term_T *term = curbuf->b_term;
+
+ return term != NULL
+ && !term->tl_terminal_mode
+ && term->tl_vterm != NULL
+ && term_job_running(term);
+}
+
/*
* Wait for input and send it to the job.
* Return when the start of a CTRL-W command is typed or anything else that
int c;
int termkey = 0;
- if (curbuf->b_term->tl_vterm == NULL || !term_job_running(curbuf->b_term))
- /* job finished */
- return OK;
-
if (*curwin->w_p_tk != NUL)
termkey = string_to_key(curwin->w_p_tk, TRUE);
/* TODO: skip screen update when handling a sequence of keys. */
update_screen(0);
update_cursor(curbuf->b_term, FALSE);
+
c = term_vgetc();
if (curbuf->b_term->tl_vterm == NULL
|| !term_job_running(curbuf->b_term))
break;
if (termkey == 0 && c == '.')
+ {
/* "CTRL-W .": send CTRL-W to the job */
c = Ctrl_W;
+ }
+ else if (termkey == 0 && c == 'N')
+ {
+ term_enter_terminal_mode();
+ return FAIL;
+ }
else if (termkey == 0 || c != termkey)
{
stuffcharReadbuff(Ctrl_W);
/*
* Called when a job has finished.
+ * This updates the title and status, but does not close the vter, because
+ * there might still be pending output in the channel.
*/
void
term_job_ended(job_T *job)
line->sb_cells = p;
++term->tl_scrollback.ga_len;
++term->tl_scrollback_scrolled;
- }
- return 0; /* ignored */
-}
-
-/*
- * Fill the buffer with the scrollback lines and current lines of the terminal.
- * Called after the job has ended.
- */
- static void
-move_scrollback_to_buffer(term_T *term)
-{
- linenr_T lnum;
- garray_T ga;
- int c;
- int col;
- int i;
- win_T *wp;
- int len;
- int lines_skipped = 0;
- VTermPos pos;
- VTermScreenCell cell;
- VTermScreenCell *p;
- VTermScreen *screen = vterm_obtain_screen(term->tl_vterm);
- /* Append the the visible lines to the scrollback. */
- for (pos.row = 0; pos.row < term->tl_rows; ++pos.row)
- {
- len = 0;
- for (pos.col = 0; pos.col < term->tl_cols; ++pos.col)
- if (vterm_screen_get_cell(screen, pos, &cell) != 0
- && cell.chars[0] != NUL)
- len = pos.col + 1;
-
- if (len == 0)
- ++lines_skipped;
- else
- {
- while (lines_skipped > 0)
- {
- /* Line was skipped, add an empty line. */
- --lines_skipped;
- if (ga_grow(&term->tl_scrollback, 1) == OK)
- {
- sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
- + term->tl_scrollback.ga_len;
-
- line->sb_cols = 0;
- line->sb_cells = NULL;
- ++term->tl_scrollback.ga_len;
- }
- }
-
- p = (VTermScreenCell *)alloc((int)sizeof(VTermScreenCell) * len);
- if (p != NULL && ga_grow(&term->tl_scrollback, 1) == OK)
- {
- sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
- + term->tl_scrollback.ga_len;
-
- for (pos.col = 0; pos.col < len; ++pos.col)
- {
- if (vterm_screen_get_cell(screen, pos, &cell) == 0)
- vim_memset(p + pos.col, 0, sizeof(cell));
- else
- p[pos.col] = cell;
- }
- line->sb_cols = len;
- line->sb_cells = p;
- ++term->tl_scrollback.ga_len;
- }
- else
- vim_free(p);
- }
- }
-
- /* Add the text to the buffer. */
- ga_init2(&ga, 1, 100);
- for (lnum = 0; lnum < term->tl_scrollback.ga_len; ++lnum)
- {
- sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + lnum;
-
- ga.ga_len = 0;
- for (col = 0; col < line->sb_cols; ++col)
- {
- if (ga_grow(&ga, MB_MAXBYTES) == FAIL)
- goto failed;
- for (i = 0; (c = line->sb_cells[col].chars[i]) > 0 || i == 0; ++i)
- ga.ga_len += mb_char2bytes(c == NUL ? ' ' : c,
- (char_u *)ga.ga_data + ga.ga_len);
- }
- if (ga_grow(&ga, 1) == FAIL)
- goto failed;
- *((char_u *)ga.ga_data + ga.ga_len) = NUL;
- ml_append_buf(term->tl_buffer, lnum, ga.ga_data, ga.ga_len + 1, FALSE);
- }
-
- /* Delete the empty line that was in the empty buffer. */
- curbuf = term->tl_buffer;
- ml_delete(lnum + 1, FALSE);
- curbuf = curwin->w_buffer;
-
-failed:
- ga_clear(&ga);
-
- FOR_ALL_WINDOWS(wp)
- {
- if (wp->w_buffer == term->tl_buffer)
- {
- wp->w_cursor.lnum = term->tl_buffer->b_ml.ml_line_count;
- wp->w_cursor.col = 0;
- wp->w_valid = 0;
- }
+ add_scrollback_line_to_buffer(term);
}
+ return 0; /* ignored */
}
static VTermScreenCallbacks screen_callbacks = {
for (term = first_term; term != NULL; term = term->tl_next)
if (term->tl_job == ch->ch_job)
{
+ term->tl_channel_closed = TRUE;
+
vim_free(term->tl_title);
term->tl_title = NULL;
vim_free(term->tl_status_text);
term->tl_status_text = NULL;
- /* move the lines into the buffer and free the vterm */
- move_scrollback_to_buffer(term);
- term_free_vterm(term);
+ /* Unless in Terminal-Normal mode: clear the vterm. */
+ if (!term->tl_terminal_mode)
+ cleanup_vterm(term);
redraw_buf_and_status_later(term->tl_buffer, NOT_VALID);
did_one = TRUE;
VTermState *state;
VTermPos pos;
- if (term == NULL || term->tl_vterm == NULL)
+ if (term == NULL || term->tl_vterm == NULL || term->tl_terminal_mode)
return FAIL;
+
vterm = term->tl_vterm;
screen = vterm_obtain_screen(vterm);
state = vterm_obtain_state(vterm);
return buf->b_term != NULL && buf->b_term->tl_vterm == NULL;
}
+/*
+ * Return TRUE if "wp" is a terminal window where the job has finished or we
+ * are in Terminal-Normal mode.
+ */
+ int
+term_show_buffer(buf_T *buf)
+{
+ term_T *term = buf->b_term;
+
+ return term != NULL && (term->tl_vterm == NULL || term->tl_terminal_mode);
+}
+
/*
* The current buffer is going to be changed. If there is terminal
* highlighting remove it now.
char_u *txt;
size_t len;
- if (term->tl_title != NULL)
+ if (term->tl_terminal_mode)
+ {
+ if (term_job_running(term))
+ txt = (char_u *)_("Terminal");
+ else
+ txt = (char_u *)_("Terminal-finished");
+ }
+ else if (term->tl_title != NULL)
txt = term->tl_title;
else if (term_job_running(term))
txt = (char_u *)_("running");