aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThiago de Arruda <tpadilha84@gmail.com>2015-03-08 08:58:31 -0300
committerThiago de Arruda <tpadilha84@gmail.com>2015-03-25 18:57:35 -0300
commitcdedd89d228a016a4e433968702e9e3ce5165e7d (patch)
tree2a98eb4008a6033621c293f84db29cb3127f9740
parent6f471fa4fc24e72e1ac75c1579970414b168381e (diff)
downloadrneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.tar.gz
rneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.tar.bz2
rneovim-cdedd89d228a016a4e433968702e9e3ce5165e7d.zip
terminal: New module that implements a terminal emulator
This commit integrates libvterm with Neovim and implements a terminal emulator with nvim buffers as the display mechanism. Terminal buffers can be created using any of the following methods: - Opening a file with name following the "term://[${cwd}//[${pid}:]]${cmd}" URI pattern where: - cwd is the working directory of the process - pid is the process id. This is just for use in session files where a pid would have been assigned to the saved buffer title. - cmd is the command to run - Invoking the `:terminal` ex command - Invoking the `termopen` function which returns a job id for automating the terminal window. Some extra changes were also implemented to adapt with terminal buffers. Here's an overview: - The `main` function now sets a BufReadCmd autocmd to intercept the term:// URI and spawn the terminal buffer instead of reading the file. - terminal buffers behave as if the following local buffer options were set: - `nomodifiable` - `swapfile` - `undolevels=-1` - `bufhidden=hide` - All commands that delete buffers(`:bun`, `:bd` and `:bw`) behave the same for terminal buffers, but only work when bang is passed(eg: `:bwipeout!`) - A new "terminal" mode was added. A consequence is that a new set of mapping commands were implemented with the "t" prefix(tmap, tunmap, tnoremap...) - The `edit` function(which enters insert mode) will actually enter terminal mode if the current buffer is a terminal - The `put` operator was adapted to send data to the terminal instead of modifying the buffer directly. - A window being resized will also trigger a terminal resize if the window displays the terminal.
-rw-r--r--src/nvim/buffer.c50
-rw-r--r--src/nvim/buffer_defs.h7
-rw-r--r--src/nvim/edit.c6
-rw-r--r--src/nvim/eval.c211
-rw-r--r--src/nvim/ex_cmds.c2
-rw-r--r--src/nvim/ex_cmds.lua25
-rw-r--r--src/nvim/ex_docmd.c28
-rw-r--r--src/nvim/fileio.c10
-rw-r--r--src/nvim/fileio.h1
-rw-r--r--src/nvim/fold.c5
-rw-r--r--src/nvim/getchar.c6
-rw-r--r--src/nvim/macros.h2
-rw-r--r--src/nvim/main.c11
-rw-r--r--src/nvim/memline.c8
-rw-r--r--src/nvim/ops.c27
-rw-r--r--src/nvim/option.c8
-rw-r--r--src/nvim/os/event.c8
-rw-r--r--src/nvim/quickfix.c15
-rw-r--r--src/nvim/screen.c36
-rw-r--r--src/nvim/syntax.c5
-rw-r--r--src/nvim/terminal.c1129
-rw-r--r--src/nvim/terminal.h33
-rw-r--r--src/nvim/undo.c3
-rw-r--r--src/nvim/vim.h5
-rw-r--r--src/nvim/window.c28
25 files changed, 1577 insertions, 92 deletions
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
index 03adba97cd..dd20d61f75 100644
--- a/src/nvim/buffer.c
+++ b/src/nvim/buffer.c
@@ -68,6 +68,7 @@
#include "nvim/spell.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
+#include "nvim/terminal.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#include "nvim/version.h"
@@ -307,20 +308,28 @@ close_buffer (
bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE);
bool wipe_buf = (action == DOBUF_WIPE);
- /*
- * Force unloading or deleting when 'bufhidden' says so.
- * The caller must take care of NOT deleting/freeing when 'bufhidden' is
- * "hide" (otherwise we could never free or delete a buffer).
- */
- if (buf->b_p_bh[0] == 'd') { /* 'bufhidden' == "delete" */
- del_buf = true;
+ // Force unloading or deleting when 'bufhidden' says so, but not for terminal
+ // buffers.
+ // The caller must take care of NOT deleting/freeing when 'bufhidden' is
+ // "hide" (otherwise we could never free or delete a buffer).
+ if (!buf->terminal) {
+ if (buf->b_p_bh[0] == 'd') { // 'bufhidden' == "delete"
+ del_buf = true;
+ unload_buf = true;
+ } else if (buf->b_p_bh[0] == 'w') { // 'bufhidden' == "wipe"
+ del_buf = true;
+ unload_buf = true;
+ wipe_buf = true;
+ } else if (buf->b_p_bh[0] == 'u') // 'bufhidden' == "unload"
+ unload_buf = true;
+ }
+
+ if (buf->terminal && (unload_buf || del_buf || wipe_buf)) {
+ // terminal buffers can only be wiped
unload_buf = true;
- } else if (buf->b_p_bh[0] == 'w') { /* 'bufhidden' == "wipe" */
del_buf = true;
- unload_buf = true;
wipe_buf = true;
- } else if (buf->b_p_bh[0] == 'u') /* 'bufhidden' == "unload" */
- unload_buf = true;
+ }
if (win_valid(win)) {
/* Set b_last_cursor when closing the last window for the buffer.
@@ -383,6 +392,10 @@ close_buffer (
if (buf->b_nwindows > 0 || !unload_buf)
return;
+ if (buf->terminal) {
+ terminal_close(buf->terminal, NULL);
+ }
+
/* Always remove the buffer when there is no file name. */
if (buf->b_ffname == NULL)
del_buf = TRUE;
@@ -925,8 +938,8 @@ do_buffer (
if (action != DOBUF_WIPE && buf->b_ml.ml_mfp == NULL && !buf->b_p_bl)
return FAIL;
- if (!forceit && bufIsChanged(buf)) {
- if ((p_confirm || cmdmod.confirm) && p_write) {
+ if (!forceit && (buf->terminal || bufIsChanged(buf))) {
+ if ((p_confirm || cmdmod.confirm) && p_write && !buf->terminal) {
dialog_changed(buf, FALSE);
if (!buf_valid(buf))
/* Autocommand deleted buffer, oops! It's not changed
@@ -937,9 +950,14 @@ do_buffer (
if (bufIsChanged(buf))
return FAIL;
} else {
- EMSGN(_("E89: No write since last change for buffer %" PRId64
- " (add ! to override)"),
- buf->b_fnum);
+ if (buf->terminal) {
+ EMSG2(_("E89: %s will be killed(add ! to override)"),
+ (char *)buf->b_fname);
+ } else {
+ EMSGN(_("E89: No write since last change for buffer %" PRId64
+ " (add ! to override)"),
+ buf->b_fnum);
+ }
return FAIL;
}
}
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index 3b8b171bd4..35fa3978b6 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -27,7 +27,7 @@
// for String
#include "nvim/api/private/defs.h"
-#define MODIFIABLE(buf) (buf->b_p_ma)
+#define MODIFIABLE(buf) (!buf->terminal && buf->b_p_ma)
/*
* Flags for w_valid.
@@ -103,6 +103,9 @@ typedef struct file_buffer buf_T; /* forward declaration */
// for FileID
#include "nvim/os/fs_defs.h"
+// for Terminal
+#include "nvim/terminal.h"
+
/*
* The taggy struct is used to store the information about a :tag command.
*/
@@ -751,6 +754,8 @@ struct file_buffer {
* may use a different synblock_T. */
signlist_T *b_signlist; /* list of signs to draw */
+
+ Terminal *terminal; // Terminal instance associated with the buffer
};
/*
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 516278f34d..8b2ac1943f 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -56,6 +56,7 @@
#include "nvim/tag.h"
#include "nvim/ui.h"
#include "nvim/mouse.h"
+#include "nvim/terminal.h"
#include "nvim/undo.h"
#include "nvim/window.h"
#include "nvim/os/event.h"
@@ -244,6 +245,11 @@ edit (
long count
)
{
+ if (curbuf->terminal) {
+ terminal_enter(curbuf->terminal, true);
+ return false;
+ }
+
int c = 0;
char_u *ptr;
int lastc;
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 08ad33e60e..6d8f47ee9c 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -1,4 +1,5 @@
/*
+ *
* VIM - Vi IMproved by Bram Moolenaar
*
* Do ":help uganda" in Vim to read copying and usage conditions.
@@ -78,6 +79,7 @@
#include "nvim/tempfile.h"
#include "nvim/ui.h"
#include "nvim/mouse.h"
+#include "nvim/terminal.h"
#include "nvim/undo.h"
#include "nvim/version.h"
#include "nvim/window.h"
@@ -440,6 +442,15 @@ static struct vimvar {
static dictitem_T vimvars_var; /* variable used for v: */
#define vimvarht vimvardict.dv_hashtab
+typedef struct {
+ Job *job;
+ Terminal *term;
+ bool exited;
+ int refcount;
+ char *autocmd_file;
+} TerminalJobData;
+
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval.c.generated.h"
#endif
@@ -447,7 +458,6 @@ static dictitem_T vimvars_var; /* variable used for v: */
#define FNE_INCL_BR 1 /* find_name_end(): include [] in name */
#define FNE_CHECK_START 2 /* find_name_end(): check name starts with
valid character */
-
// Memory pool for reusing JobEvent structures
typedef struct {
int id;
@@ -6600,6 +6610,7 @@ static struct fst {
{"tan", 1, 1, f_tan},
{"tanh", 1, 1, f_tanh},
{"tempname", 0, 0, f_tempname},
+ {"termopen", 1, 2, f_termopen},
{"test", 1, 1, f_test},
{"tolower", 1, 1, f_tolower},
{"toupper", 1, 1, f_toupper},
@@ -10750,12 +10761,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
// The last item of argv must be NULL
argv[i] = NULL;
- JobOptions opts = JOB_OPTIONS_INIT;
- opts.argv = argv;
- opts.data = xstrdup((char *)argvars[0].vval.v_string);
- opts.stdout_cb = on_job_stdout;
- opts.stderr_cb = on_job_stderr;
- opts.exit_cb = on_job_exit;
+ JobOptions opts = common_job_options(argv, (char *)argvars[0].vval.v_string);
if (args && argvars[3].v_type == VAR_DICT) {
dict_T *job_opts = argvars[3].vval.v_dict;
@@ -10774,15 +10780,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
}
}
- job_start(opts, &rettv->vval.v_number);
-
- if (rettv->vval.v_number <= 0) {
- if (rettv->vval.v_number == 0) {
- EMSG(_(e_jobtblfull));
- } else {
- EMSG(_(e_jobexe));
- }
- }
+ common_job_start(opts, rettv);
}
// "jobstop()" function
@@ -14870,6 +14868,72 @@ static void f_tempname(typval_T *argvars, typval_T *rettv)
rettv->vval.v_string = vim_tempname();
}
+// "termopen(cmd[, cwd])" function
+static void f_termopen(typval_T *argvars, typval_T *rettv)
+{
+ if (check_restricted() || check_secure()) {
+ return;
+ }
+
+ if (curbuf->b_changed) {
+ EMSG(_("Can only call this function in an unmodified buffer"));
+ return;
+ }
+
+ if (argvars[0].v_type != VAR_STRING
+ || (argvars[1].v_type != VAR_STRING
+ && argvars[1].v_type != VAR_UNKNOWN)) {
+ // Wrong argument types
+ EMSG(_(e_invarg));
+ return;
+ }
+
+ char **argv = shell_build_argv((char *)argvars[0].vval.v_string, NULL);
+ JobOptions opts = common_job_options(argv, NULL);
+ opts.pty = true;
+ opts.width = curwin->w_width;
+ opts.height = curwin->w_height;
+ opts.term_name = xstrdup("xterm-256color");
+ Job *job = common_job_start(opts, rettv);
+ if (!job) {
+ return;
+ }
+ TerminalJobData *data = opts.data;
+ TerminalOptions topts = TERMINAL_OPTIONS_INIT;
+ topts.data = data;
+ topts.width = curwin->w_width;
+ topts.height = curwin->w_height;
+ topts.write_cb = term_write;
+ topts.resize_cb = term_resize;
+ topts.close_cb = term_close;
+
+ char *cwd = ".";
+ if (argvars[1].v_type == VAR_STRING
+ && os_isdir(argvars[1].vval.v_string)) {
+ cwd = (char *)argvars[1].vval.v_string;
+ }
+ int pid = job_pid(job);
+ char buf[1024];
+ // format the title with the pid to conform with the term:// URI
+ snprintf(buf, sizeof(buf), "term://%s//%d:%s", cwd, pid,
+ (char *)argvars[0].vval.v_string);
+ // at this point the buffer has no terminal instance associated yet, so unset
+ // the 'swapfile' option to ensure no swap file will be created
+ curbuf->b_p_swf = false;
+ (void)setfname(curbuf, (uint8_t *)buf, NULL, true);
+ data->autocmd_file = xstrdup(buf);
+ // Save the job id and pid in b:terminal_job_{id,pid}
+ Error err;
+ dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_id"),
+ INTEGER_OBJ(rettv->vval.v_number), &err);
+ dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_pid"),
+ INTEGER_OBJ(pid), &err);
+
+ Terminal *term = terminal_open(topts);
+ data->term = term;
+ data->refcount++;
+}
+
/*
* "test(list)" function: Just checking the walls...
*/
@@ -19750,16 +19814,51 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags)
return ret;
}
+static inline JobOptions common_job_options(char **argv, char *autocmd_file)
+{
+ TerminalJobData *data = xcalloc(1, sizeof(TerminalJobData));
+ if (autocmd_file) {
+ data->autocmd_file = xstrdup(autocmd_file);
+ }
+ JobOptions opts = JOB_OPTIONS_INIT;
+ opts.argv = argv;
+ opts.data = data;
+ opts.stdout_cb = on_job_stdout;
+ opts.stderr_cb = on_job_stderr;
+ opts.exit_cb = on_job_exit;
+ return opts;
+}
+
+static inline Job *common_job_start(JobOptions opts, typval_T *rettv)
+{
+ Job *job = job_start(opts, &rettv->vval.v_number);
+ TerminalJobData *data = opts.data;
+ data->refcount++;
+
+ if (rettv->vval.v_number <= 0) {
+ if (rettv->vval.v_number == 0) {
+ EMSG(_(e_jobtblfull));
+ free(data->autocmd_file);
+ } else {
+ EMSG(_(e_jobexe));
+ free(opts.data);
+ }
+ return NULL;
+ }
+ data->job = job;
+ return job;
+}
+
// JobActivity autocommands will execute vimscript code, so it must be executed
// on Nvim main loop
-static inline void push_job_event(Job *job, RStream *rstream, char *type)
+static inline void push_job_event(Job *job, char *type, char *data,
+ size_t count)
{
JobEvent *event_data = kmp_alloc(JobEventPool, job_event_pool);
event_data->received = NULL;
- if (rstream) {
+ if (data) {
event_data->received = list_alloc();
- char *ptr = rstream_read_ptr(rstream);
- size_t count = rstream_pending(rstream);
+ char *ptr = data;
size_t remaining = count;
size_t off = 0;
@@ -19780,10 +19879,10 @@ static inline void push_job_event(Job *job, RStream *rstream, char *type)
off++;
}
list_append_string(event_data->received, (uint8_t *)ptr, off);
- rbuffer_consumed(rstream_buffer(rstream), count);
}
+ TerminalJobData *d = job_data(job);
event_data->id = job_id(job);
- event_data->name = job_data(job);
+ event_data->name = d->autocmd_file;
event_data->type = type;
event_push((Event) {
.handler = on_job_event,
@@ -19793,21 +19892,75 @@ static inline void push_job_event(Job *job, RStream *rstream, char *type)
static void on_job_stdout(RStream *rstream, void *data, bool eof)
{
- if (!eof) {
- push_job_event(data, rstream, "stdout");
- }
+ on_job_output(rstream, data, eof, "stdout");
}
static void on_job_stderr(RStream *rstream, void *data, bool eof)
{
- if (!eof) {
- push_job_event(data, rstream, "stderr");
+ on_job_output(rstream, data, eof, "stderr");
+}
+
+static void on_job_output(RStream *rstream, Job *job, bool eof, char *type)
+{
+ if (eof) {
+ return;
}
+
+ TerminalJobData *data = job_data(job);
+ char *ptr = rstream_read_ptr(rstream);
+ size_t len = rstream_pending(rstream);
+
+ // The order here matters, the terminal must receive the data first because
+ // push_job_event will modify the read buffer(convert NULs into NLs)
+ if (data->term) {
+ terminal_receive(data->term, ptr, len);
+ }
+
+ push_job_event(job, type, ptr, len);
+ rbuffer_consumed(rstream_buffer(rstream), len);
+}
+
+static void on_job_exit(Job *job, void *d)
+{
+ TerminalJobData *data = d;
+ push_job_event(job, "exit", NULL, 0);
+
+ if (data->term && !data->exited) {
+ data->exited = true;
+ terminal_close(data->term,
+ _("\r\n[Program exited, press any key to close]"));
+ }
+ term_job_data_decref(data);
+}
+
+static void term_write(char *buf, size_t size, void *data)
+{
+ Job *job = ((TerminalJobData *)data)->job;
+ WBuffer *wbuf = wstream_new_buffer(xmemdup(buf, size), size, 1, free);
+ job_write(job, wbuf);
+}
+
+static void term_resize(uint16_t width, uint16_t height, void *data)
+{
+ job_resize(((TerminalJobData *)data)->job, width, height);
}
-static void on_job_exit(Job *job, void *data)
+static void term_close(void *d)
{
- push_job_event(job, NULL, "exit");
+ TerminalJobData *data = d;
+ if (!data->exited) {
+ data->exited = true;
+ job_stop(data->job);
+ }
+ terminal_destroy(data->term);
+ term_job_data_decref(d);
+}
+
+static void term_job_data_decref(TerminalJobData *data)
+{
+ if (!(--data->refcount)) {
+ free(data);
+ }
}
static void on_job_event(Event event)
diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c
index d85664d1ba..81c7ecab5f 100644
--- a/src/nvim/ex_cmds.c
+++ b/src/nvim/ex_cmds.c
@@ -2724,7 +2724,7 @@ do_ecmd (
/* close the link to the current buffer */
u_sync(FALSE);
close_buffer(oldwin, curbuf,
- (flags & ECMD_HIDE) ? 0 : DOBUF_UNLOAD, FALSE);
+ (flags & ECMD_HIDE) || curbuf->terminal ? 0 : DOBUF_UNLOAD, FALSE);
/* Autocommands may open a new window and leave oldwin open
* which leads to crashes since the above call sets
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index 0897006555..e1951e88f8 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -2236,6 +2236,11 @@ return {
func='ex_tearoff',
},
{
+ command='terminal',
+ flags=bit.bor(BANG, FILES, CMDWIN),
+ func='ex_terminal',
+ },
+ {
command='tfirst',
flags=bit.bor(RANGE, NOTADR, BANG, TRLBAR, ZEROR),
func='ex_tag',
@@ -2256,6 +2261,16 @@ return {
func='ex_tag',
},
{
+ command='tmap',
+ flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN),
+ func='ex_map',
+ },
+ {
+ command='tmapclear',
+ flags=bit.bor(EXTRA, TRLBAR, CMDWIN),
+ func='ex_mapclear',
+ },
+ {
command='tmenu',
flags=bit.bor(RANGE, NOTADR, ZEROR, EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN),
func='ex_menu',
@@ -2266,6 +2281,11 @@ return {
func='ex_tag',
},
{
+ command='tnoremap',
+ flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN),
+ func='ex_map',
+ },
+ {
command='topleft',
flags=bit.bor(NEEDARG, EXTRA, NOTRLCOM),
func='ex_wrongmodifier',
@@ -2291,6 +2311,11 @@ return {
func='ex_tag',
},
{
+ command='tunmap',
+ flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN),
+ func='ex_unmap',
+ },
+ {
command='tunmenu',
flags=bit.bor(EXTRA, TRLBAR, NOTRLCOM, USECTRLV, CMDWIN),
func='ex_menu',
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index de04a3ea32..776ed844e9 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -62,6 +62,7 @@
#include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/tag.h"
+#include "nvim/terminal.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#include "nvim/version.h"
@@ -71,6 +72,8 @@
#include "nvim/os/time.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/mouse.h"
+#include "nvim/os/rstream.h"
+#include "nvim/os/wstream.h"
static int quitmore = 0;
static int ex_pressedreturn = FALSE;
@@ -1510,7 +1513,9 @@ static char_u * do_one_cmd(char_u **cmdlinep,
errormsg = (char_u *)_(e_sandbox);
goto doend;
}
- if (!MODIFIABLE(curbuf) && (ea.argt & MODIFY)) {
+ if (!MODIFIABLE(curbuf) && (ea.argt & MODIFY)
+ // allow :put in terminals
+ && (!curbuf->terminal || ea.cmdidx != CMD_put)) {
/* Command not allowed in non-'modifiable' buffer */
errormsg = (char_u *)_(e_modifiable);
goto doend;
@@ -2610,7 +2615,7 @@ set_one_cmd_context (
xp->xp_context = EXPAND_FILES;
/* For a shell command more chars need to be escaped. */
- if (usefilter || ea.cmdidx == CMD_bang) {
+ if (usefilter || ea.cmdidx == CMD_bang || ea.cmdidx == CMD_terminal) {
#ifndef BACKSLASH_IN_FILENAME
xp->xp_shell = TRUE;
#endif
@@ -5126,8 +5131,10 @@ static void ex_quit(exarg_T *eap)
|| (only_one_window() && check_changed_any(eap->forceit))) {
not_exiting();
} else {
- if (only_one_window()) /* quit last window */
+ if (only_one_window()) {
+ // quit last window
getout(0);
+ }
/* close window; may free buffer */
win_close(curwin, !P_HID(curwin->w_buffer) || eap->forceit);
}
@@ -8060,7 +8067,9 @@ makeopens (
/*
* Wipe out an empty unnamed buffer we started in.
*/
- if (put_line(fd, "if exists('s:wipebuf')") == FAIL)
+ if (put_line(fd, "if exists('s:wipebuf') "
+ "&& getbufvar(s:wipebuf, '&buftype') isnot# 'terminal'")
+ == FAIL)
return FAIL;
if (put_line(fd, " silent exe 'bwipe ' . s:wipebuf") == FAIL)
return FAIL;
@@ -8269,7 +8278,7 @@ put_view (
* Load the file.
*/
if (wp->w_buffer->b_ffname != NULL
- && !bt_nofile(wp->w_buffer)
+ && (!bt_nofile(wp->w_buffer) || wp->w_buffer->terminal)
) {
/*
* Editing a file in this buffer: use ":edit file".
@@ -8857,3 +8866,12 @@ static void ex_folddo(exarg_T *eap)
global_exe(eap->arg);
ml_clearmarked(); /* clear rest of the marks */
}
+
+static void ex_terminal(exarg_T *eap)
+{
+ char cmd[512];
+ snprintf(cmd, sizeof(cmd), ":enew%s | call termopen('%s') | startinsert",
+ eap->forceit==TRUE ? "!" : "",
+ strcmp((char *)eap->arg, "") ? (char *)eap->arg : (char *)p_sh);
+ do_cmdline_cmd((uint8_t *)cmd);
+}
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index 799a6a2a50..29bcaec84a 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -4756,10 +4756,11 @@ buf_check_timestamp (
char_u *s;
char *reason;
- /* If there is no file name, the buffer is not loaded, 'buftype' is
- * set, we are in the middle of a save or being called recursively: ignore
- * this buffer. */
- if (buf->b_ffname == NULL
+ // If its a terminal, there is no file name, the buffer is not loaded,
+ // 'buftype' is set, we are in the middle of a save or being called
+ // recursively: ignore this buffer.
+ if (buf->terminal
+ || buf->b_ffname == NULL
|| buf->b_ml.ml_mfp == NULL
|| *buf->b_p_bt != NUL
|| buf->b_saving
@@ -5208,6 +5209,7 @@ static struct event_name {
{"TabNew", EVENT_TABNEW},
{"TabNewEntered", EVENT_TABNEWENTERED},
{"TermChanged", EVENT_TERMCHANGED},
+ {"TermOpen", EVENT_TERMOPEN},
{"TermResponse", EVENT_TERMRESPONSE},
{"TextChanged", EVENT_TEXTCHANGED},
{"TextChangedI", EVENT_TEXTCHANGEDI},
diff --git a/src/nvim/fileio.h b/src/nvim/fileio.h
index 7a9e2adae1..833a4fade6 100644
--- a/src/nvim/fileio.h
+++ b/src/nvim/fileio.h
@@ -100,6 +100,7 @@ typedef enum auto_event {
EVENT_TABNEWENTERED, /* after entering a new tab */
EVENT_SHELLCMDPOST, /* after ":!cmd" */
EVENT_SHELLFILTERPOST, /* after ":1,2!cmd", ":w !cmd", ":r !cmd". */
+ EVENT_TERMOPEN, // after opening a terminal buffer
EVENT_TEXTCHANGED, /* text was modified */
EVENT_TEXTCHANGEDI, /* text was modified in Insert mode*/
EVENT_CMDUNDEFINED, ///< command undefined
diff --git a/src/nvim/fold.c b/src/nvim/fold.c
index 74444d4ab7..267c586543 100644
--- a/src/nvim/fold.c
+++ b/src/nvim/fold.c
@@ -137,7 +137,7 @@ void copyFoldingState(win_T *wp_from, win_T *wp_to)
int hasAnyFolding(win_T *win)
{
/* very simple now, but can become more complex later */
- return win->w_p_fen
+ return !win->w_buffer->terminal && win->w_p_fen
&& (!foldmethodIsManual(win) || !GA_EMPTY(&win->w_folds));
}
@@ -768,6 +768,9 @@ void clearFolding(win_T *win)
void foldUpdate(win_T *wp, linenr_T top, linenr_T bot)
{
fold_T *fp;
+ if (wp->w_buffer->terminal) {
+ return;
+ }
/* Mark all folds from top to bot as maybe-small. */
(void)foldFind(&curwin->w_folds, top, &fp);
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c
index bb251c102e..d901e99a2d 100644
--- a/src/nvim/getchar.c
+++ b/src/nvim/getchar.c
@@ -2538,6 +2538,7 @@ fix_input_buffer (
* for :xmap mode is VISUAL
* for :smap mode is SELECTMODE
* for :omap mode is OP_PENDING
+ * for :tmap mode is TERM_FOCUS
*
* for :abbr mode is INSERT + CMDLINE
* for :iabbr mode is INSERT
@@ -3056,6 +3057,8 @@ int get_map_mode(char_u **cmdp, int forceit)
mode = SELECTMODE; /* :smap */
else if (modec == 'o')
mode = OP_PENDING; /* :omap */
+ else if (modec == 't')
+ mode = TERM_FOCUS; // :tmap
else {
--p;
if (forceit)
@@ -3923,6 +3926,9 @@ makemap (
case LANGMAP:
c1 = 'l';
break;
+ case TERM_FOCUS:
+ c1 = 't';
+ break;
default:
EMSG(_("E228: makemap: Illegal mode"));
return FAIL;
diff --git a/src/nvim/macros.h b/src/nvim/macros.h
index 653f46fb44..60f6cd454f 100644
--- a/src/nvim/macros.h
+++ b/src/nvim/macros.h
@@ -154,4 +154,6 @@
/// zero in those cases (-Wdiv-by-zero in GCC).
#define ARRAY_SIZE(arr) ((sizeof(arr)/sizeof((arr)[0])) / ((size_t)(!(sizeof(arr) % sizeof((arr)[0])))))
+#define RGB(r, g, b) ((r << 16) | (g << 8) | b)
+
#endif // NVIM_MACROS_H
diff --git a/src/nvim/main.c b/src/nvim/main.c
index 9f4bc22ae0..47bb2bc515 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -285,6 +285,17 @@ int main(int argc, char **argv)
input_start_stdin(fd);
}
+ // open terminals when opening files that start with term://
+ do_cmdline_cmd((uint8_t *)
+ "autocmd BufReadCmd term://* "
+ ":call termopen( "
+ // Capture the command string
+ "matchstr(expand(\"<amatch>\"), "
+ "'\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), "
+ // capture the working directory
+ "get(matchlist(expand(\"<amatch>\"), "
+ "'\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))");
+
/* Execute --cmd arguments. */
exe_pre_commands(&params);
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
index d6d7d3db1a..8b2ebfe554 100644
--- a/src/nvim/memline.c
+++ b/src/nvim/memline.c
@@ -278,10 +278,11 @@ int ml_open(buf_T *buf)
/*
* When 'updatecount' is non-zero swap file may be opened later.
*/
- if (p_uc && buf->b_p_swf)
+ if (!buf->terminal && p_uc && buf->b_p_swf) {
buf->b_may_swap = true;
- else
+ } else {
buf->b_may_swap = false;
+ }
/*
* Open the memfile. No swap file is created yet.
@@ -488,7 +489,8 @@ void ml_open_file(buf_T *buf)
char_u *dirp;
mfp = buf->b_ml.ml_mfp;
- if (mfp == NULL || mfp->mf_fd >= 0 || !buf->b_p_swf || cmdmod.noswapfile) {
+ if (mfp == NULL || mfp->mf_fd >= 0 || !buf->b_p_swf || cmdmod.noswapfile
+ || buf->terminal) {
return; /* nothing to do */
}
diff --git a/src/nvim/ops.c b/src/nvim/ops.c
index 50710cd78a..2714798368 100644
--- a/src/nvim/ops.c
+++ b/src/nvim/ops.c
@@ -45,6 +45,7 @@
#include "nvim/screen.h"
#include "nvim/search.h"
#include "nvim/strings.h"
+#include "nvim/terminal.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#include "nvim/window.h"
@@ -2643,11 +2644,13 @@ do_put (
return;
}
- /* Autocommands may be executed when saving lines for undo, which may make
- * y_array invalid. Start undo now to avoid that. */
- if (u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1) == FAIL) {
- ELOG(_("Failed to save undo information"));
- return;
+ if (!curbuf->terminal) {
+ // Autocommands may be executed when saving lines for undo, which may make
+ // y_array invalid. Start undo now to avoid that.
+ if (u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1) == FAIL) {
+ ELOG(_("Failed to save undo information"));
+ return;
+ }
}
if (insert_string != NULL) {
@@ -2692,6 +2695,20 @@ do_put (
y_array = y_current->y_array;
}
+ if (curbuf->terminal) {
+ for (int i = 0; i < count; i++) {
+ // feed the lines to the terminal
+ for (int j = 0; j < y_size; j++) {
+ if (j) {
+ // terminate the previous line
+ terminal_send(curbuf->terminal, "\n", 1);
+ }
+ terminal_send(curbuf->terminal, (char *)y_array[j], STRLEN(y_array[j]));
+ }
+ }
+ return;
+ }
+
if (y_type == MLINE) {
if (flags & PUT_LINE_SPLIT) {
/* "p" or "P" in Visual mode: split the lines to put the text in
diff --git a/src/nvim/option.c b/src/nvim/option.c
index 19202030c6..3f12709521 100644
--- a/src/nvim/option.c
+++ b/src/nvim/option.c
@@ -1746,7 +1746,7 @@ static char *(p_scbopt_values[]) = {"ver", "hor", "jump", NULL};
static char *(p_debug_values[]) = {"msg", "throw", "beep", NULL};
static char *(p_ead_values[]) = {"both", "ver", "hor", NULL};
static char *(p_buftype_values[]) =
-{"nofile", "nowrite", "quickfix", "help", "acwrite", NULL};
+{"nofile", "nowrite", "quickfix", "help", "acwrite", "terminal", NULL};
static char *(p_bufhidden_values[]) = {"hide", "unload", "delete", "wipe", NULL};
static char *(p_bs_values[]) = {"indent", "eol", "start", NULL};
static char *(p_fdm_values[]) = {"manual", "expr", "marker", "indent", "syntax",
@@ -4097,9 +4097,11 @@ did_set_string_option (
}
/* When 'buftype' is set, check for valid value. */
else if (gvarp == &p_bt) {
- if (check_opt_strings(curbuf->b_p_bt, p_buftype_values, FALSE) != OK)
+ if ((curbuf->terminal && curbuf->b_p_bt[0] != 't')
+ || (!curbuf->terminal && curbuf->b_p_bt[0] == 't')
+ || check_opt_strings(curbuf->b_p_bt, p_buftype_values, FALSE) != OK) {
errmsg = e_invarg;
- else {
+ } else {
if (curwin->w_status_height) {
curwin->w_redr_status = TRUE;
redraw_later(VALID);
diff --git a/src/nvim/os/event.c b/src/nvim/os/event.c
index 92a5205eaf..fd9ff5b230 100644
--- a/src/nvim/os/event.c
+++ b/src/nvim/os/event.c
@@ -20,6 +20,7 @@
#include "nvim/misc2.h"
#include "nvim/ui.h"
#include "nvim/screen.h"
+#include "nvim/terminal.h"
#include "nvim/lib/klist.h"
@@ -63,6 +64,7 @@ void event_init(void)
// finish mspgack-rpc initialization
channel_init();
server_init();
+ terminal_init();
}
void event_teardown(void)
@@ -83,6 +85,7 @@ void event_teardown(void)
job_teardown();
server_teardown();
signal_teardown();
+ terminal_teardown();
// this last `uv_run` will return after all handles are stopped, it will
// also take care of finishing any uv_close calls made by other *_teardown
// functions.
@@ -169,11 +172,6 @@ void event_push(Event event, bool deferred)
void event_process(void)
{
process_events_from(deferred_events);
-
- if (must_redraw) {
- update_screen(0);
- ui_flush();
- }
}
static void process_events_from(klist_t(Event) *queue)
diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c
index 39117b262a..8e55cced78 100644
--- a/src/nvim/quickfix.c
+++ b/src/nvim/quickfix.c
@@ -2377,7 +2377,6 @@ static void qf_fill_buffer(qf_info_T *qi)
KeyTyped = old_KeyTyped;
}
-
/*
* Return TRUE if "buf" is the quickfix buffer.
*/
@@ -2386,22 +2385,18 @@ int bt_quickfix(buf_T *buf)
return buf != NULL && buf->b_p_bt[0] == 'q';
}
-/*
- * Return TRUE if "buf" is a "nofile" or "acwrite" buffer.
- * This means the buffer name is not a file name.
- */
+// Return TRUE if "buf" is a "nofile", "acwrite" or "terminal" buffer.
+// This means the buffer name is not a file name.
int bt_nofile(buf_T *buf)
{
return buf != NULL && ((buf->b_p_bt[0] == 'n' && buf->b_p_bt[2] == 'f')
- || buf->b_p_bt[0] == 'a');
+ || buf->b_p_bt[0] == 'a' || buf->terminal);
}
-/*
- * Return TRUE if "buf" is a "nowrite" or "nofile" buffer.
- */
+// Return TRUE if "buf" is a "nowrite", "nofile" or "terminal" buffer.
int bt_dontwrite(buf_T *buf)
{
- return buf != NULL && buf->b_p_bt[0] == 'n';
+ return buf != NULL && (buf->b_p_bt[0] == 'n' || buf->terminal);
}
int bt_dontwrite_msg(buf_T *buf)
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 04a092c4f7..c32603afb0 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -131,6 +131,7 @@
#include "nvim/spell.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
+#include "nvim/terminal.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#include "nvim/version.h"
@@ -2210,7 +2211,7 @@ win_line (
}
/* Check for columns to display for 'colorcolumn'. */
- color_cols = wp->w_p_cc_cols;
+ color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols;
if (color_cols != NULL)
draw_color_col = advance_color_col(VCOL_HLC, &color_cols);
@@ -2592,6 +2593,13 @@ win_line (
off += col;
}
+ // wont highlight after 1024 columns
+ int term_attrs[1024] = {0};
+ if (wp->w_buffer->terminal) {
+ terminal_get_line_attributes(wp->w_buffer->terminal, wp, lnum, term_attrs);
+ extra_check = true;
+ }
+
/*
* Repeat for the whole displayed line.
*/
@@ -3213,6 +3221,8 @@ win_line (
syntax_flags = 0;
else
syntax_flags = get_syntax_info(&syntax_seqnr);
+ } else if (!attr_pri) {
+ char_attr = 0;
}
/* Check spelling (unless at the end of the line).
@@ -3290,6 +3300,11 @@ win_line (
else
char_attr = hl_combine_attr(spell_attr, char_attr);
}
+
+ if (wp->w_buffer->terminal) {
+ char_attr = hl_combine_attr(char_attr, term_attrs[vcol]);
+ }
+
/*
* Found last space before word: check for line break.
*/
@@ -3787,6 +3802,18 @@ win_line (
}
}
+ if (wp->w_buffer->terminal) {
+ // terminal buffers may need to highlight beyond the end of the
+ // logical line
+ while (col < wp->w_width) {
+ ScreenLines[off] = ' ';
+ if (enc_utf8) {
+ ScreenLinesUC[off] = 0;
+ }
+ ScreenAttrs[off++] = term_attrs[vcol++];
+ col++;
+ }
+ }
SCREEN_LINE(screen_row, wp->w_wincol, col, wp->w_width, wp->w_p_rl);
row++;
@@ -6536,7 +6563,8 @@ int showmode(void)
int sub_attr;
do_mode = ((p_smd && msg_silent == 0)
- && ((State & INSERT)
+ && ((State & TERM_FOCUS)
+ || (State & INSERT)
|| restart_edit
|| VIsual_active
));
@@ -6591,7 +6619,9 @@ int showmode(void)
}
}
} else {
- if (State & VREPLACE_FLAG)
+ if (State & TERM_FOCUS) {
+ MSG_PUTS_ATTR(_(" TERMINAL"), attr);
+ } else if (State & VREPLACE_FLAG)
MSG_PUTS_ATTR(_(" VREPLACE"), attr);
else if (State & REPLACE_FLAG)
MSG_PUTS_ATTR(_(" REPLACE"), attr);
diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c
index c339468233..20bfbc8db4 100644
--- a/src/nvim/syntax.c
+++ b/src/nvim/syntax.c
@@ -40,10 +40,12 @@
#include "nvim/option.h"
#include "nvim/os_unix.h"
#include "nvim/path.h"
+#include "nvim/macros.h"
#include "nvim/regexp.h"
#include "nvim/screen.h"
#include "nvim/strings.h"
#include "nvim/syntax_defs.h"
+#include "nvim/terminal.h"
#include "nvim/ui.h"
#include "nvim/os/os.h"
#include "nvim/os/time.h"
@@ -6636,7 +6638,7 @@ static garray_T attr_table = GA_EMPTY_INIT_VALUE;
* if the combination is new.
* Return 0 for error.
*/
-static int get_attr_entry(attrentry_T *aep)
+int get_attr_entry(attrentry_T *aep)
{
garray_T *table = &attr_table;
attrentry_T *taep;
@@ -7424,7 +7426,6 @@ char_u *get_highlight_name(expand_T *xp, int idx)
return HL_TABLE()[idx].sg_name;
}
-#define RGB(r, g, b) ((r << 16) | (g << 8) | b)
color_name_table_T color_name_table[] = {
// Color names taken from
// http://www.rapidtables.com/web/color/RGB_Color.htm
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
new file mode 100644
index 0000000000..87b2d8ff99
--- /dev/null
+++ b/src/nvim/terminal.c
@@ -0,0 +1,1129 @@
+// VT220/xterm-like terminal emulator implementation for Neovim. Powered by
+// libvterm(http://www.leonerd.org.uk/code/libvterm/).
+//
+// libvterm is a pure C99 terminal emulation library with abstract input and
+// display. This means that the library needs to read data from the master fd
+// and feed VTerm instances, which will invoke user callbacks with screen
+// update instructions that must be mirrored to the real display.
+//
+// Keys are pressed in VTerm instances by calling
+// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that
+// must be fed back to the master fd.
+//
+// This implementation uses Neovim buffers as the display mechanism for both
+// the visible screen and the scrollback buffer. When focused, the window
+// "pins" to the bottom of the buffer and mirrors libvterm screen state.
+//
+// When a line becomes invisible due to a decrease in screen height or because
+// a line was pushed up during normal terminal output, we store the line
+// information in the scrollback buffer, which is mirrored in the Neovim buffer
+// by appending lines just above the visible part of the buffer.
+//
+// When the screen height increases, libvterm will ask for a row in the
+// scrollback buffer, which is mirrored in the Neovim buffer displaying lines
+// that were previously invisible.
+//
+// The vterm->Neovim synchronization is performed in intervals of 10
+// milliseconds. This is done to minimize screen updates when receiving when
+// receiving large bursts of data.
+//
+// This module is decoupled from the processes that normally feed it data, so
+// it's possible to use it as a general purpose console buffer(possibly as a
+// log/display mechanism for Neovim in the future)
+//
+// Inspired by vimshell(http://www.wana.at/vimshell/) and
+// Conque(https://code.google.com/p/conque/). Libvterm usage instructions (plus
+// some extra code) were taken from
+// pangoterm(http://www.leonerd.org.uk/code/pangoterm/)
+#include <assert.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <vterm.h>
+
+#include "nvim/vim.h"
+#include "nvim/terminal.h"
+#include "nvim/message.h"
+#include "nvim/memory.h"
+#include "nvim/option.h"
+#include "nvim/macros.h"
+#include "nvim/mbyte.h"
+#include "nvim/buffer.h"
+#include "nvim/ascii.h"
+#include "nvim/getchar.h"
+#include "nvim/ui.h"
+#include "nvim/syntax.h"
+#include "nvim/screen.h"
+#include "nvim/keymap.h"
+#include "nvim/edit.h"
+#include "nvim/mouse.h"
+#include "nvim/memline.h"
+#include "nvim/mark.h"
+#include "nvim/map.h"
+#include "nvim/misc1.h"
+#include "nvim/move.h"
+#include "nvim/ex_docmd.h"
+#include "nvim/ex_cmds.h"
+#include "nvim/window.h"
+#include "nvim/fileio.h"
+#include "nvim/os/event.h"
+#include "nvim/api/private/helpers.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "terminal.c.generated.h"
+#endif
+
+#define SCROLLBACK_BUFFER_DEFAULT_SIZE 1000
+// Delay for refreshing the terminal buffer after receiving updates from
+// libvterm. This is greatly improves performance when receiving large bursts
+// of data.
+#define REFRESH_DELAY 10
+
+static uv_timer_t refresh_timer;
+static bool refresh_pending = false;
+
+typedef struct {
+ size_t cols;
+ VTermScreenCell cells[];
+} ScrollbackLine;
+
+struct terminal {
+ // options passed to terminal_open
+ TerminalOptions opts;
+ // libvterm structures
+ VTerm *vt;
+ VTermScreen *vts;
+ // buffer used to:
+ // - convert VTermScreen cell arrays into utf8 strings
+ // - receive data from libvterm as a result of key presses.
+ char textbuf[0x1fff];
+ // Scrollback buffer storage for libvterm.
+ // TODO(tarruda): Use a doubly-linked list
+ ScrollbackLine **sb_buffer;
+ // number of rows pushed to sb_buffer
+ size_t sb_current;
+ // sb_buffer size;
+ size_t sb_size;
+ // "virtual index" that points to the first sb_buffer row that we need to
+ // push to the terminal buffer when refreshing the scrollback. When negative,
+ // it actually points to entries that are no longer in sb_buffer (because the
+ // window height has increased) and must be deleted from the terminal buffer
+ int sb_pending;
+ // buf_T instance that acts as a "drawing surface" for libvterm
+ buf_T *buf;
+ // program exited
+ bool closed, destroy;
+ // some vterm properties
+ bool forward_mouse;
+ // invalid rows libvterm screen
+ int invalid_start, invalid_end;
+ struct {
+ int row, col;
+ bool visible;
+ } cursor;
+ // which mouse button is pressed
+ int pressed_button;
+ // pending width/height
+ bool pending_resize;
+ // color palette. this isn't set directly in the vterm instance because
+ // the default values are used to obtain the color numbers passed to cterm
+ // colors
+ RgbValue colors[256];
+ // attributes for focused/unfocused cursor cells
+ int focused_cursor_attr_id, unfocused_cursor_attr_id;
+};
+
+static VTermScreenCallbacks vterm_screen_callbacks = {
+ .damage = term_damage,
+ .moverect = term_moverect,
+ .movecursor = term_movecursor,
+ .settermprop = term_settermprop,
+ .bell = term_bell,
+ .sb_pushline = term_sb_push,
+ .sb_popline = term_sb_pop,
+};
+
+static PMap(ptr_t) *invalidated_terminals;
+static Map(int, int) *color_indexes;
+static int default_vt_fg, default_vt_bg;
+static VTermColor default_vt_bg_rgb;
+
+void terminal_init(void)
+{
+ invalidated_terminals = pmap_new(ptr_t)();
+ uv_timer_init(uv_default_loop(), &refresh_timer);
+
+ // initialize a rgb->color index map for cterm attributes(VTermScreenCell
+ // only has RGB information and we need color indexes for terminal UIs)
+ color_indexes = map_new(int, int)();
+ VTerm *vt = vterm_new(24, 80);
+ VTermState *state = vterm_obtain_state(vt);
+
+ for (int color_index = 0; color_index < 256; color_index++) {
+ VTermColor color;
+ vterm_state_get_palette_color(state, color_index, &color);
+ map_put(int, int)(color_indexes,
+ RGB(color.red, color.green, color.blue), color_index + 1);
+ }
+
+ VTermColor fg, bg;
+ vterm_state_get_default_colors(state, &fg, &bg);
+ default_vt_fg = RGB(fg.red, fg.green, fg.blue);
+ default_vt_bg = RGB(bg.red, bg.green, bg.blue);
+ default_vt_bg_rgb = bg;
+ vterm_free(vt);
+}
+
+void terminal_teardown(void)
+{
+ uv_timer_stop(&refresh_timer);
+ uv_close((uv_handle_t *)&refresh_timer, NULL);
+ pmap_free(ptr_t)(invalidated_terminals);
+ map_free(int, int)(color_indexes);
+}
+
+// public API {{{
+
+Terminal *terminal_open(TerminalOptions opts)
+{
+ // Create a new terminal instance and configure it
+ Terminal *rv = xcalloc(1, sizeof(Terminal));
+ rv->opts = opts;
+ rv->cursor.visible = true;
+ // Associate the terminal instance with the new buffer
+ rv->buf = curbuf;
+ curbuf->terminal = rv;
+ // Create VTerm
+ rv->vt = vterm_new(opts.height, opts.width);
+ vterm_set_utf8(rv->vt, 1);
+ // Setup state
+ VTermState *state = vterm_obtain_state(rv->vt);
+ vterm_state_set_bold_highbright(state, true);
+ // Set up screen
+ rv->vts = vterm_obtain_screen(rv->vt);
+ vterm_screen_enable_altscreen(rv->vts, true);
+ // delete empty lines at the end of the buffer
+ vterm_screen_set_callbacks(rv->vts, &vterm_screen_callbacks, rv);
+ vterm_screen_set_damage_merge(rv->vts, VTERM_DAMAGE_SCROLL);
+ vterm_screen_reset(rv->vts, 1);
+ // force a initial refresh of the screen to ensure the buffer will always
+ // have as many lines as screen rows when refresh_scrollback is called
+ rv->invalid_start = 0;
+ rv->invalid_end = opts.height;
+ refresh_screen(rv);
+ set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL);
+ // some sane settings for terminal buffers
+ set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL);
+ set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL);
+ set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL);
+ RESET_BINDING(curwin);
+ // Apply TermOpen autocmds so the user can configure the terminal
+ apply_autocmds(EVENT_TERMOPEN, NULL, NULL, true, curbuf);
+
+ // Configure the scrollback buffer. Try to get the size from:
+ //
+ // - b:terminal_scrollback_buffer_size
+ // - g:terminal_scrollback_buffer_size
+ // - SCROLLBACK_BUFFER_DEFAULT_SIZE
+ //
+ // but limit to 100k.
+ int size = get_config_int(rv, "terminal_scrollback_buffer_size");
+ rv->sb_size = size > 0 ? (size_t)size : SCROLLBACK_BUFFER_DEFAULT_SIZE;
+ rv->sb_size = MIN(rv->sb_size, 100000);
+ rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size);
+
+ // Configure the color palette. Try to get the color from:
+ //
+ // - b:terminal_color_{NUM}
+ // - g:terminal_color_{NUM}
+ // - the VTerm instance
+ for (int i = 0; i < (int)ARRAY_SIZE(rv->colors); i++) {
+ RgbValue color_val = -1;
+ char var[64];
+ snprintf(var, sizeof(var), "terminal_color_%d", i);
+ char *name = get_config_string(rv, var);
+ if (name) {
+ color_val = name_to_color((uint8_t *)name);
+ free(name);
+
+ if (color_val != -1) {
+ rv->colors[i] = color_val;
+ }
+ }
+
+ if (color_val == -1) {
+ // the default is taken from vterm
+ VTermColor color;
+ vterm_state_get_palette_color(state, i, &color);
+ rv->colors[i] = RGB(color.red, color.green, color.blue);
+ }
+ }
+
+ // Configure cursor highlighting when focused/unfocused
+ char *group = get_config_string(rv, "terminal_focused_cursor_highlight");
+ if (group) {
+ int group_id = syn_name2id((uint8_t *)group);
+ free(group);
+
+ if (group_id) {
+ rv->focused_cursor_attr_id = syn_id2attr(group_id);
+ }
+ }
+ if (!rv->focused_cursor_attr_id) {
+ rv->focused_cursor_attr_id = get_attr_entry(&(attrentry_T) {
+ .rgb_ae_attr = HL_INVERSE, .rgb_fg_color = -1, .rgb_bg_color = -1,
+ .cterm_ae_attr = HL_INVERSE, .cterm_fg_color = 0, .cterm_bg_color = 0
+ });
+ }
+
+ group = get_config_string(rv, "terminal_unfocused_cursor_highlight");
+ if (group) {
+ int group_id = syn_name2id((uint8_t *)group);
+ free(group);
+
+ if (group_id) {
+ rv->unfocused_cursor_attr_id = syn_id2attr(group_id);
+ }
+ }
+ if (!rv->unfocused_cursor_attr_id) {
+ int yellow_rgb = RGB(0xfc, 0xe9, 0x4f);
+ int yellow_term = 12;
+ rv->unfocused_cursor_attr_id = get_attr_entry(&(attrentry_T) {
+ .rgb_ae_attr = 0, .rgb_fg_color = -1, .rgb_bg_color = yellow_rgb,
+ .cterm_ae_attr = 0, .cterm_fg_color = 0, .cterm_bg_color = yellow_term,
+ });
+ }
+
+ return rv;
+}
+
+void terminal_close(Terminal *term, char *msg)
+{
+ if (term->closed) {
+ return;
+ }
+
+ term->forward_mouse = false;
+ term->closed = true;
+ if (!msg || exiting) {
+ // If no msg was given, this was called by close_buffer(buffer.c) so we
+ // should not wait for the user to press a key. Also cannot wait if
+ // `exiting == true`
+ term->opts.close_cb(term->opts.data);
+ } else {
+ terminal_receive(term, msg, strlen(msg));
+ }
+}
+
+void terminal_resize(Terminal *term, uint16_t width, uint16_t height)
+{
+ if (term->closed) {
+ // will be called after exited if two windows display the same terminal and
+ // one of the is closed as a consequence of pressing a key.
+ return;
+ }
+ int curwidth, curheight;
+ vterm_get_size(term->vt, &curheight, &curwidth);
+
+ if (!width) {
+ width = (uint16_t)curwidth;
+ }
+
+ if (!height) {
+ height = (uint16_t)curheight;
+ }
+
+ // The new width/height are the minimum for all windows that display the
+ // terminal in the current tab.
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ if (!wp->w_closing && wp->w_buffer == term->buf) {
+ width = (uint16_t)MIN(width, (uint16_t)wp->w_width);
+ height = (uint16_t)MIN(height, (uint16_t)wp->w_height);
+ }
+ }
+
+ if (curheight == height && curwidth == width) {
+ return;
+ }
+
+ vterm_set_size(term->vt, height, width);
+ vterm_screen_flush_damage(term->vts);
+ term->pending_resize = true;
+ invalidate_terminal(term, -1, -1);
+}
+
+void terminal_enter(Terminal *term, bool process_deferred)
+{
+ checkpcmark();
+ setpcmark();
+ int save_state = State;
+ int save_rd = RedrawingDisabled;
+ State = TERM_FOCUS;
+ RedrawingDisabled = false;
+ bool save_mapped_ctrl_c = mapped_ctrl_c;
+ mapped_ctrl_c = true;
+ // go to the bottom when the terminal is focused
+ adjust_topline(term, false);
+ // erase the unfocused cursor
+ invalidate_terminal(term, term->cursor.row, term->cursor.row + 1);
+ showmode();
+ ui_busy_start();
+ redraw(false);
+ int c;
+ bool close = false;
+
+ for (;;) {
+ if (process_deferred) {
+ event_enable_deferred();
+ }
+
+ c = safe_vgetc();
+
+ if (process_deferred) {
+ event_disable_deferred();
+ }
+
+ switch (c) {
+ case Ctrl_BSL:
+ c = safe_vgetc();
+ if (c == Ctrl_N) {
+ goto end;
+ }
+ terminal_send_key(term, c);
+ break;
+
+ case K_LEFTMOUSE:
+ case K_LEFTDRAG:
+ case K_LEFTRELEASE:
+ case K_MIDDLEMOUSE:
+ case K_MIDDLEDRAG:
+ case K_MIDDLERELEASE:
+ case K_RIGHTMOUSE:
+ case K_RIGHTDRAG:
+ case K_RIGHTRELEASE:
+ case K_MOUSEDOWN:
+ case K_MOUSEUP:
+ if (send_mouse_event(term, c)) {
+ goto end;
+ }
+ break;
+
+ case K_EVENT:
+ event_process();
+ break;
+
+ default:
+ if (term->closed) {
+ close = true;
+ goto end;
+ }
+
+ terminal_send_key(term, c);
+ }
+ }
+
+end:
+ restart_edit = 0;
+ State = save_state;
+ RedrawingDisabled = save_rd;
+ // draw the unfocused cursor
+ invalidate_terminal(term, term->cursor.row, term->cursor.row + 1);
+ mapped_ctrl_c = save_mapped_ctrl_c;
+ unshowmode(true);
+ redraw(false);
+ ui_busy_stop();
+ if (close) {
+ term->opts.close_cb(term->opts.data);
+ do_cmdline_cmd((uint8_t *)"bwipeout!");
+ }
+}
+
+void terminal_destroy(Terminal *term)
+{
+ term->buf->terminal = NULL;
+ term->buf = NULL;
+ pmap_del(ptr_t)(invalidated_terminals, term);
+ for (size_t i = 0 ; i < term->sb_current; i++) {
+ free(term->sb_buffer[i]);
+ }
+ free(term->sb_buffer);
+ vterm_free(term->vt);
+ free(term);
+}
+
+void terminal_send(Terminal *term, char *data, size_t size)
+{
+ if (term->closed) {
+ return;
+ }
+ term->opts.write_cb(data, size, term->opts.data);
+}
+
+void terminal_send_key(Terminal *term, int c)
+{
+ VTermModifier mod = VTERM_MOD_NONE;
+ VTermKey key = convert_key(c, &mod);
+
+ if (key) {
+ vterm_keyboard_key(term->vt, key, mod);
+ } else {
+ vterm_keyboard_unichar(term->vt, (uint32_t)c, mod);
+ }
+
+ size_t len = vterm_output_read(term->vt, term->textbuf,
+ sizeof(term->textbuf));
+ terminal_send(term, term->textbuf, (size_t)len);
+}
+
+void terminal_receive(Terminal *term, char *data, size_t len)
+{
+ if (!data) {
+ return;
+ }
+
+ vterm_input_write(term->vt, data, len);
+ vterm_screen_flush_damage(term->vts);
+}
+
+void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr,
+ int *term_attrs)
+{
+ int height, width;
+ vterm_get_size(term->vt, &height, &width);
+ assert(linenr);
+ int row = linenr_to_row(term, linenr);
+ if (row >= height) {
+ // Terminal height was decreased but the change wasn't reflected into the
+ // buffer yet
+ return;
+ }
+
+ for (int col = 0; col < width; col++) {
+ VTermScreenCell cell;
+ fetch_cell(term, row, col, &cell);
+ // Get the rgb value set by libvterm.
+ int vt_fg = RGB(cell.fg.red, cell.fg.green, cell.fg.blue);
+ int vt_bg = RGB(cell.bg.red, cell.bg.green, cell.bg.blue);
+ vt_fg = vt_fg != default_vt_fg ? vt_fg : - 1;
+ vt_bg = vt_bg != default_vt_bg ? vt_bg : - 1;
+ // Since libvterm does not expose the color index used by the program, we
+ // use the rgb value to find the appropriate index in the cache computed by
+ // `terminal_init`.
+ int vt_fg_idx = vt_fg != default_vt_fg ?
+ map_get(int, int)(color_indexes, vt_fg) : 0;
+ int vt_bg_idx = vt_bg != default_vt_bg ?
+ map_get(int, int)(color_indexes, vt_bg) : 0;
+ // The index is now used to get the final rgb value from the
+ // user-customizable palette.
+ int vt_fg_rgb = vt_fg_idx != 0 ? term->colors[vt_fg_idx - 1] : -1;
+ int vt_bg_rgb = vt_bg_idx != 0 ? term->colors[vt_bg_idx - 1] : -1;
+
+ int hl_attrs = (cell.attrs.bold ? HL_BOLD : 0)
+ | (cell.attrs.italic ? HL_ITALIC : 0)
+ | (cell.attrs.reverse ? HL_INVERSE : 0)
+ | (cell.attrs.underline ? HL_UNDERLINE : 0);
+
+ int attr_id = 0;
+
+ if (hl_attrs || vt_fg != -1 || vt_bg != -1) {
+ attr_id = get_attr_entry(&(attrentry_T) {
+ .cterm_ae_attr = (int16_t)hl_attrs,
+ .cterm_fg_color = vt_fg_idx,
+ .cterm_bg_color = vt_bg_idx,
+ .rgb_ae_attr = (int16_t)hl_attrs,
+ .rgb_fg_color = vt_fg_rgb,
+ .rgb_bg_color = vt_bg_rgb,
+ });
+ }
+
+ if (term->cursor.visible && term->cursor.row == row
+ && term->cursor.col == col) {
+ attr_id = hl_combine_attr(attr_id, is_focused(term) && wp == curwin ?
+ term->focused_cursor_attr_id : term->unfocused_cursor_attr_id);
+ }
+
+ term_attrs[col] = attr_id;
+ }
+}
+
+// }}}
+// libvterm callbacks {{{
+
+static int term_damage(VTermRect rect, void *data)
+{
+ invalidate_terminal(data, rect.start_row, rect.end_row);
+ return 1;
+}
+
+static int term_moverect(VTermRect dest, VTermRect src, void *data)
+{
+ invalidate_terminal(data, MIN(dest.start_row, src.start_row),
+ MAX(dest.end_row, src.end_row));
+ return 1;
+}
+
+static int term_movecursor(VTermPos new, VTermPos old, int visible,
+ void *data)
+{
+ Terminal *term = data;
+ term->cursor.row = new.row;
+ term->cursor.col = new.col;
+ invalidate_terminal(term, old.row, old.row + 1);
+ invalidate_terminal(term, new.row, new.row + 1);
+ return 1;
+}
+
+static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
+{
+ Terminal *term = data;
+
+ switch (prop) {
+ case VTERM_PROP_ALTSCREEN:
+ break;
+
+ case VTERM_PROP_CURSORVISIBLE:
+ term->cursor.visible = val->boolean;
+ invalidate_terminal(term, term->cursor.row, term->cursor.row + 1);
+ break;
+
+ case VTERM_PROP_TITLE: {
+ Error err;
+ dict_set_value(term->buf->b_vars,
+ cstr_as_string("term_title"),
+ STRING_OBJ(cstr_as_string(val->string)), &err);
+ break;
+ }
+
+ case VTERM_PROP_MOUSE:
+ term->forward_mouse = (bool)val->number;
+ break;
+
+ default:
+ return 0;
+ }
+
+ return 1;
+}
+
+static int term_bell(void *data)
+{
+ ui_putc('\x07');
+ return 1;
+}
+
+// the scrollback push/pop handlers were copied almost verbatim from pangoterm
+static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
+{
+ Terminal *term = data;
+ // copy vterm cells into sb_buffer
+ size_t c = (size_t)cols;
+ ScrollbackLine *sbrow = NULL;
+ if (term->sb_current == term->sb_size) {
+ if (term->sb_buffer[term->sb_current - 1]->cols == c) {
+ // Recycle old row if it's the right size
+ sbrow = term->sb_buffer[term->sb_current - 1];
+ } else {
+ free(term->sb_buffer[term->sb_current - 1]);
+ }
+
+ memmove(term->sb_buffer + 1, term->sb_buffer,
+ sizeof(term->sb_buffer[0]) * (term->sb_current - 1));
+
+ } else if (term->sb_current > 0) {
+ memmove(term->sb_buffer + 1, term->sb_buffer,
+ sizeof(term->sb_buffer[0]) * term->sb_current);
+ }
+
+ if (!sbrow) {
+ sbrow = xmalloc(sizeof(ScrollbackLine) + c * sizeof(sbrow->cells[0]));
+ sbrow->cols = c;
+ }
+
+ term->sb_buffer[0] = sbrow;
+ if (term->sb_current < term->sb_size) {
+ term->sb_current++;
+ }
+
+ if (term->sb_pending < (int)term->sb_size) {
+ term->sb_pending++;
+ }
+
+ memcpy(sbrow->cells, cells, sizeof(cells[0]) * c);
+ pmap_put(ptr_t)(invalidated_terminals, term, NULL);
+
+ return 1;
+}
+
+static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
+{
+ Terminal *term = data;
+
+ if (!term->sb_current) {
+ return 0;
+ }
+
+ if (term->sb_pending) {
+ term->sb_pending--;
+ }
+
+ // restore vterm state
+ size_t c = (size_t)cols;
+ ScrollbackLine *sbrow = term->sb_buffer[0];
+ term->sb_current--;
+ memmove(term->sb_buffer, term->sb_buffer + 1,
+ sizeof(term->sb_buffer[0]) * (term->sb_current));
+
+ size_t cols_to_copy = c;
+ if (cols_to_copy > sbrow->cols) {
+ cols_to_copy = sbrow->cols;
+ }
+
+ // copy to vterm state
+ memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy);
+ for (size_t col = cols_to_copy; col < c; col++) {
+ cells[col].chars[0] = 0;
+ cells[col].width = 1;
+ }
+ free(sbrow);
+ pmap_put(ptr_t)(invalidated_terminals, term, NULL);
+
+ return 1;
+}
+
+// }}}
+// input handling {{{
+
+static void convert_modifiers(VTermModifier *statep)
+{
+ if (mod_mask & MOD_MASK_SHIFT) { *statep |= VTERM_MOD_SHIFT; }
+ if (mod_mask & MOD_MASK_CTRL) { *statep |= VTERM_MOD_CTRL; }
+ if (mod_mask & MOD_MASK_ALT) { *statep |= VTERM_MOD_ALT; }
+}
+
+static VTermKey convert_key(int key, VTermModifier *statep)
+{
+ convert_modifiers(statep);
+
+ switch (key) {
+ case K_BS: return VTERM_KEY_BACKSPACE;
+ case TAB: return VTERM_KEY_TAB;
+ case Ctrl_M: return VTERM_KEY_ENTER;
+ case ESC: return VTERM_KEY_ESCAPE;
+
+ case K_UP: return VTERM_KEY_UP;
+ case K_DOWN: return VTERM_KEY_DOWN;
+ case K_LEFT: return VTERM_KEY_LEFT;
+ case K_RIGHT: return VTERM_KEY_RIGHT;
+
+ case K_INS: return VTERM_KEY_INS;
+ case K_DEL: return VTERM_KEY_DEL;
+ case K_HOME: return VTERM_KEY_HOME;
+ case K_END: return VTERM_KEY_END;
+ case K_PAGEUP: return VTERM_KEY_PAGEUP;
+ case K_PAGEDOWN: return VTERM_KEY_PAGEDOWN;
+
+ case K_K0:
+ case K_KINS: return VTERM_KEY_KP_0;
+ case K_K1:
+ case K_KEND: return VTERM_KEY_KP_1;
+ case K_K2: return VTERM_KEY_KP_2;
+ case K_K3:
+ case K_KPAGEDOWN: return VTERM_KEY_KP_3;
+ case K_K4: return VTERM_KEY_KP_4;
+ case K_K5: return VTERM_KEY_KP_5;
+ case K_K6: return VTERM_KEY_KP_6;
+ case K_K7:
+ case K_KHOME: return VTERM_KEY_KP_7;
+ case K_K8: return VTERM_KEY_KP_8;
+ case K_K9:
+ case K_KPAGEUP: return VTERM_KEY_KP_9;
+ case K_KDEL:
+ case K_KPOINT: return VTERM_KEY_KP_PERIOD;
+ case K_KENTER: return VTERM_KEY_KP_ENTER;
+ case K_KPLUS: return VTERM_KEY_KP_PLUS;
+ case K_KMINUS: return VTERM_KEY_KP_MINUS;
+ case K_KMULTIPLY: return VTERM_KEY_KP_MULT;
+ case K_KDIVIDE: return VTERM_KEY_KP_DIVIDE;
+
+ default: return VTERM_KEY_NONE;
+ }
+}
+
+static void mouse_action(Terminal *term, int button, int row, int col,
+ bool drag, VTermModifier mod)
+{
+ if (term->pressed_button && (term->pressed_button != button || !drag)) {
+ // release the previous button
+ vterm_mouse_button(term->vt, term->pressed_button, 0, mod);
+ term->pressed_button = 0;
+ }
+
+ // move the mouse
+ vterm_mouse_move(term->vt, row, col, mod);
+
+ if (!term->pressed_button) {
+ // press the button if not already pressed
+ vterm_mouse_button(term->vt, button, 1, mod);
+ term->pressed_button = button;
+ }
+}
+
+// process a mouse event while the terminal is focused. return true if the
+// terminal should lose focus
+static bool send_mouse_event(Terminal *term, int c)
+{
+ int row = mouse_row, col = mouse_col;
+ win_T *mouse_win = mouse_find_win(&row, &col);
+
+ if (term->forward_mouse && mouse_win->w_buffer == term->buf) {
+ // event in the terminal window and mouse events was enabled by the
+ // program. translate and forward the event
+ int button;
+ bool drag = false;
+
+ switch (c) {
+ case K_LEFTDRAG: drag = true;
+ case K_LEFTMOUSE: button = 1; break;
+ case K_MIDDLEDRAG: drag = true;
+ case K_MIDDLEMOUSE: button = 2; break;
+ case K_RIGHTDRAG: drag = true;
+ case K_RIGHTMOUSE: button = 3; break;
+ case K_MOUSEDOWN: button = 4; break;
+ case K_MOUSEUP: button = 5; break;
+ default: return false;
+ }
+
+ mouse_action(term, button, row, col, drag, 0);
+ size_t len = vterm_output_read(term->vt, term->textbuf,
+ sizeof(term->textbuf));
+ terminal_send(term, term->textbuf, (size_t)len);
+ return false;
+ }
+
+ if (c == K_MOUSEDOWN || c == K_MOUSEUP) {
+ win_T *save_curwin = curwin;
+ // switch window/buffer to perform the scroll
+ curwin = mouse_win;
+ curbuf = curwin->w_buffer;
+ int direction = c == K_MOUSEDOWN ? MSCR_DOWN : MSCR_UP;
+ if (mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL)) {
+ scroll_redraw(direction, curwin->w_botline - curwin->w_topline);
+ } else {
+ scroll_redraw(direction, 3L);
+ }
+
+ curwin->w_redr_status = true;
+ curwin = save_curwin;
+ curbuf = curwin->w_buffer;
+ redraw_win_later(mouse_win, NOT_VALID);
+ invalidate_terminal(term, -1, -1);
+ // Only need to exit focus if the scrolled window is the terminal window
+ return mouse_win == curwin;
+ }
+
+ ins_char_typebuf(c);
+ return true;
+}
+
+// }}}
+// terminal buffer refresh & misc {{{
+
+
+void fetch_row(Terminal *term, int row, int end_col)
+{
+ int col = 0;
+ size_t line_len = 0;
+ char *ptr = term->textbuf;
+
+ while (col < end_col) {
+ VTermScreenCell cell;
+ fetch_cell(term, row, col, &cell);
+ int cell_len = 0;
+ if (cell.chars[0]) {
+ for (int i = 0; cell.chars[i]; i++) {
+ cell_len += utf_char2bytes((int)cell.chars[i],
+ (uint8_t *)ptr + cell_len);
+ }
+ } else {
+ *ptr = ' ';
+ cell_len = 1;
+ }
+ char c = *ptr;
+ ptr += cell_len;
+ if (c != ' ') {
+ // only increase the line length if the last character is not whitespace
+ line_len = (size_t)(ptr - term->textbuf);
+ }
+ col += cell.width;
+ }
+
+ // trim trailing whitespace
+ term->textbuf[line_len] = 0;
+}
+
+static void fetch_cell(Terminal *term, int row, int col,
+ VTermScreenCell *cell)
+{
+ if (row < 0) {
+ ScrollbackLine *sbrow = term->sb_buffer[-row - 1];
+ if ((size_t)col < sbrow->cols) {
+ *cell = sbrow->cells[col];
+ } else {
+ // fill the pointer with an empty cell
+ *cell = (VTermScreenCell) {
+ .chars = { 0 },
+ .width = 1,
+ .bg = default_vt_bg_rgb
+ };
+ }
+ } else {
+ vterm_screen_get_cell(term->vts, (VTermPos){.row = row, .col = col},
+ cell);
+ }
+}
+
+// queue a terminal instance for refresh
+static void invalidate_terminal(Terminal *term, int start_row, int end_row)
+{
+ if (start_row != -1 && end_row != -1) {
+ term->invalid_start = MIN(term->invalid_start, start_row);
+ term->invalid_end = MAX(term->invalid_end, end_row);
+ }
+
+ pmap_put(ptr_t)(invalidated_terminals, term, NULL);
+ if (!refresh_pending) {
+ uv_timer_start(&refresh_timer, refresh_timer_cb, REFRESH_DELAY, 0);
+ refresh_pending = true;
+ }
+}
+
+// libuv timer callback. This will enqueue on_refresh to be processed as an
+// event.
+static void refresh_timer_cb(uv_timer_t *handle)
+{
+ event_push((Event) {.handler = on_refresh}, false);
+ refresh_pending = false;
+}
+
+// Refresh all invalidated terminals
+static void on_refresh(Event event)
+{
+ if (exiting) {
+ // bad things can happen if we redraw when exiting, and there's no need to
+ // update the buffer.
+ return;
+ }
+ Terminal *term;
+ void *stub; (void)(stub);
+ // dont process autocommands while updating terminal buffers. JobActivity can
+ // be used act on terminal output.
+ block_autocmds();
+ map_foreach(invalidated_terminals, term, stub, {
+ if (!term->buf) {
+ // destroyed by `close_buffer`. Dont do anything else
+ continue;
+ }
+ bool pending_resize = term->pending_resize;
+ WITH_BUFFER(term->buf, {
+ refresh_size(term);
+ refresh_scrollback(term);
+ refresh_screen(term);
+ redraw_buf_later(term->buf, NOT_VALID);
+ });
+ adjust_topline(term, pending_resize);
+ });
+ pmap_clear(ptr_t)(invalidated_terminals);
+ unblock_autocmds();
+ redraw(true);
+}
+
+static void refresh_size(Terminal *term)
+{
+ if (!term->pending_resize || term->closed) {
+ return;
+ }
+
+ term->pending_resize = false;
+ int width, height;
+ vterm_get_size(term->vt, &height, &width);
+ term->invalid_start = 0;
+ term->invalid_end = height;
+ term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data);
+}
+
+// Refresh the scrollback of a invalidated terminal
+static void refresh_scrollback(Terminal *term)
+{
+ int width, height;
+ vterm_get_size(term->vt, &height, &width);
+
+ while (term->sb_pending > 0) {
+ // This means that either the window height has decreased or the screen
+ // became full and libvterm had to push all rows up. Convert the first
+ // pending scrollback row into a string and append it just above the visible
+ // section of the buffer
+ if (((int)term->buf->b_ml.ml_line_count - height) >= (int)term->sb_size) {
+ // scrollback full, delete lines at the top
+ ml_delete(1, false);
+ deleted_lines(1, 1);
+ }
+ fetch_row(term, -term->sb_pending, width);
+ int buf_index = (int)term->buf->b_ml.ml_line_count - height;
+ ml_append(buf_index, (uint8_t *)term->textbuf, 0, false);
+ appended_lines(buf_index, 1);
+ term->sb_pending--;
+ }
+
+ // Remove extra lines at the bottom
+ int max_line_count = (int)term->sb_current + height;
+ while (term->buf->b_ml.ml_line_count > max_line_count) {
+ ml_delete(term->buf->b_ml.ml_line_count, false);
+ deleted_lines(term->buf->b_ml.ml_line_count, 1);
+ }
+}
+
+// Refresh the screen(visible part of the buffer when the terminal is
+// focused) of a invalidated terminal
+static void refresh_screen(Terminal *term)
+{
+ int changed = 0;
+ int added = 0;
+ int height;
+ int width;
+ vterm_get_size(term->vt, &height, &width);
+ // It's possible that the terminal height decreased and `term->invalid_end`
+ // doesn't reflect it yet
+ term->invalid_end = MIN(term->invalid_end, height);
+
+ for (int r = term->invalid_start, linenr = row_to_linenr(term, r);
+ r < term->invalid_end; r++, linenr++) {
+ fetch_row(term, r, width);
+
+ if (linenr <= term->buf->b_ml.ml_line_count) {
+ ml_replace(linenr, (uint8_t *)term->textbuf, true);
+ changed++;
+ } else {
+ ml_append(linenr - 1, (uint8_t *)term->textbuf, 0, false);
+ added++;
+ }
+ }
+
+ int change_start = row_to_linenr(term, term->invalid_start);
+ int change_end = change_start + changed;
+ changed_lines(change_start, 0, change_end, added);
+ term->invalid_start = INT_MAX;
+ term->invalid_end = -1;
+}
+
+static void redraw(bool restore_cursor)
+{
+ int save_row, save_col;
+ if (restore_cursor) {
+ // save the current row/col to restore after updating screen when not
+ // focused
+ save_row = ui_current_row();
+ save_col = ui_current_col();
+ }
+ block_autocmds();
+ validate_cursor();
+
+ if (must_redraw) {
+ update_screen(0);
+ }
+
+ redraw_statuslines();
+
+ if (need_maketitle) {
+ maketitle();
+ }
+
+ showruler(false);
+
+ Terminal *term = curbuf->terminal;
+ if (term && is_focused(term)) {
+ curwin->w_wrow = term->cursor.row;
+ curwin->w_wcol = term->cursor.col + win_col_off(curwin);
+ setcursor();
+ } else if (restore_cursor) {
+ ui_cursor_goto(save_row, save_col);
+ } else {
+ // exiting terminal focus, put the window cursor in a valid position
+ int height, width;
+ vterm_get_size(term->vt, &height, &width);
+ curwin->w_wrow = height - 1;
+ curwin->w_wcol = 0;
+ setcursor();
+ }
+
+ unblock_autocmds();
+ ui_flush();
+}
+
+static void adjust_topline(Terminal *term, bool force)
+{
+ int height, width;
+ vterm_get_size(term->vt, &height, &width);
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ if (wp->w_buffer == term->buf) {
+ // for every window that displays a terminal, ensure the cursor is in a
+ // valid line
+ wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, term->buf->b_ml.ml_line_count);
+ if (force || curbuf != term->buf || is_focused(term)) {
+ // if the terminal is not in the current window or if it's focused,
+ // adjust topline/cursor so the window will "follow" the terminal
+ // output
+ wp->w_cursor.lnum = term->buf->b_ml.ml_line_count;
+ set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1));
+ }
+ }
+ }
+}
+
+static int row_to_linenr(Terminal *term, int row)
+{
+ return row != INT_MAX ? row + (int)term->sb_current + 1 : INT_MAX;
+}
+
+static int linenr_to_row(Terminal *term, int linenr)
+{
+ return linenr - (int)term->sb_current - 1;
+}
+
+static bool is_focused(Terminal *term)
+{
+ return State & TERM_FOCUS && curbuf == term->buf;
+}
+
+#define GET_CONFIG_VALUE(t, k, o) \
+ do { \
+ Error err; \
+ o = dict_get_value(t->buf->b_vars, cstr_as_string(k), &err); \
+ if (obj.type == kObjectTypeNil) { \
+ o = dict_get_value(&globvardict, cstr_as_string(k), &err); \
+ } \
+ } while (0)
+
+static char *get_config_string(Terminal *term, char *key)
+{
+ Object obj = OBJECT_INIT;
+ GET_CONFIG_VALUE(term, key, obj);
+ if (obj.type == kObjectTypeString) {
+ return obj.data.string.data;
+ }
+ return NULL;
+}
+
+static int get_config_int(Terminal *term, char *key)
+{
+ Object obj = OBJECT_INIT;
+ GET_CONFIG_VALUE(term, key, obj);
+ if (obj.type == kObjectTypeInteger) {
+ return (int)obj.data.integer;
+ }
+ return 0;
+}
+
+// }}}
+
+// vim: foldmethod=marker foldenable
diff --git a/src/nvim/terminal.h b/src/nvim/terminal.h
new file mode 100644
index 0000000000..6e0b062fbd
--- /dev/null
+++ b/src/nvim/terminal.h
@@ -0,0 +1,33 @@
+#ifndef NVIM_TERMINAL_H
+#define NVIM_TERMINAL_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+typedef struct terminal Terminal;
+typedef void (*terminal_write_cb)(char *buffer, size_t size, void *data);
+typedef void (*terminal_resize_cb)(uint16_t width, uint16_t height, void *data);
+typedef void (*terminal_close_cb)(void *data);
+
+typedef struct {
+ void *data;
+ uint16_t width, height;
+ terminal_write_cb write_cb;
+ terminal_resize_cb resize_cb;
+ terminal_close_cb close_cb;
+} TerminalOptions;
+
+#define TERMINAL_OPTIONS_INIT ((TerminalOptions) { \
+ .data = NULL, \
+ .width = 80, \
+ .height = 24, \
+ .write_cb = NULL, \
+ .resize_cb = NULL, \
+ .close_cb = NULL \
+ })
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "terminal.h.generated.h"
+#endif
+#endif // NVIM_TERMINAL_H
diff --git a/src/nvim/undo.c b/src/nvim/undo.c
index e67c5f2aba..67195235fe 100644
--- a/src/nvim/undo.c
+++ b/src/nvim/undo.c
@@ -315,6 +315,9 @@ int undo_allowed(void)
*/
static long get_undolevel(void)
{
+ if (curbuf->terminal) {
+ return -1;
+ }
if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL)
return p_ul;
return curbuf->b_p_ul;
diff --git a/src/nvim/vim.h b/src/nvim/vim.h
index a4ce9084c2..12b73843f8 100644
--- a/src/nvim/vim.h
+++ b/src/nvim/vim.h
@@ -112,9 +112,10 @@ Error: configure did not run properly.Check auto/config.log.
#define SHOWMATCH (0x700 + INSERT) /* show matching paren */
#define CONFIRM 0x800 /* ":confirm" prompt */
#define SELECTMODE 0x1000 /* Select mode, only for mappings */
+#define TERM_FOCUS 0x2000 // Terminal focus mode
-#define MAP_ALL_MODES (0x3f | SELECTMODE) /* all mode bits used for
- * mapping */
+// all mode bits used for mapping
+#define MAP_ALL_MODES (0x3f | SELECTMODE | TERM_FOCUS)
/* directions */
#define FORWARD 1
diff --git a/src/nvim/window.c b/src/nvim/window.c
index 7b5848c124..9f07f2bddc 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -52,6 +52,7 @@
#include "nvim/search.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
+#include "nvim/terminal.h"
#include "nvim/undo.h"
#include "nvim/os/os.h"
@@ -1765,6 +1766,12 @@ static int close_last_window_tabpage(win_T *win, int free_buf, tabpage_T *prev_c
}
buf_T *old_curbuf = curbuf;
+ Terminal *term = win->w_buffer->terminal;
+ if (term) {
+ // Don't free terminal buffers
+ free_buf = false;
+ }
+
/*
* Closing the last window in a tab page. First go to another tab
* page and then close the window and the tab page. This avoids that
@@ -1789,6 +1796,13 @@ static int close_last_window_tabpage(win_T *win, int free_buf, tabpage_T *prev_c
if (h != tabline_height())
shell_new_rows();
}
+
+ if (term) {
+ // When a window containing a terminal buffer is closed, recalculate its
+ // size
+ terminal_resize(term, 0, 0);
+ }
+
/* Since goto_tabpage_tp above did not trigger *Enter autocommands, do
* that now. */
apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, FALSE, curbuf);
@@ -1907,6 +1921,8 @@ int win_close(win_T *win, int free_buf)
|| close_last_window_tabpage(win, free_buf, prev_curtab))
return FAIL;
+ // let terminal buffers know that this window dimensions may be ignored
+ win->w_closing = true;
/* Free the memory used for the window and get the window that received
* the screen space. */
wp = win_free_mem(win, &dir, NULL);
@@ -1963,7 +1979,6 @@ int win_close(win_T *win, int free_buf)
if (help_window)
restore_snapshot(SNAP_HELP_IDX, close_curwin);
-
redraw_all_later(NOT_VALID);
return OK;
}
@@ -4689,6 +4704,11 @@ void win_new_height(win_T *wp, int height)
redraw_win_later(wp, SOME_VALID);
wp->w_redr_status = TRUE;
invalidate_botline_win(wp);
+
+ if (wp->w_buffer->terminal) {
+ terminal_resize(wp->w_buffer->terminal, 0, wp->w_height);
+ redraw_win_later(wp, CLEAR);
+ }
}
/*
@@ -4706,6 +4726,11 @@ void win_new_width(win_T *wp, int width)
}
redraw_win_later(wp, NOT_VALID);
wp->w_redr_status = TRUE;
+
+ if (wp->w_buffer->terminal) {
+ terminal_resize(wp->w_buffer->terminal, wp->w_width, 0);
+ redraw_win_later(wp, CLEAR);
+ }
}
void win_comp_scroll(win_T *wp)
@@ -5570,4 +5595,3 @@ static int frame_check_width(frame_T *topfrp, int width)
return TRUE;
}
-