For all the reasons I mentionned in my first article about OSC7 and Neovim, running all of your shells in Neovim's terminal is super nice. But as I mentionned in the same article, the shell's current directory not being synchronized with Neovim's was a bummer.
From 2017 to 2021, I've been solving this synchronization problem with a disgusting mix of python, shell and vimscript that ran each time I changed directory in the shell. In 2021, I got fed up with the jankyness of my solution and wrote a Neovim patch to get synchronization working directly in Neovim's terminal through the use of OSC 7 escape sequences.
Working through the various iterations of my patch, I got increasingly uncomfortable with how narrow it was and ended up drifting toward a generic autocommand named TermOSC that would enable handling any OSC event.
I eventually became happy enough with the patch that I started daily-driving my fork, opened a PR and wrote an article about it. I wasn't aware of it at the time, but as outlined in this comment, Neovim autocommands run asynchronously, so my perfectly-valid-for-OSC7 patch wasn't actually usable for OSC events that required more synchronization with the terminal's output. I recommended not merging my PR, but also said that I would not work on getting synchronous OSC support working in Neovim, as my patch was sufficient for my needs.
And so my PR died. I kept using my increasingly stale fork until a year later, when my hero @gpanders decided to revive it, renaming the TermOSC autocommand to TermRequest and performing a few fixes, and got it merged in less than a week 🤯. This TermRequest event still has the major synchronization issue my TermOSC event had, but I'm glad nonetheless: the PR getting merged means that I can run Neovim nightly again.
This means that, if you run Neovim nightly, you can now synchronize your shell's current working dir and Neovim's current working dir with just a tiny bit of shell and lua scripting!
In your init.lua, add the following:
vim.api.nvim_create_autocmd({ 'TermRequest' }, { callback = function(e) if string.sub(vim.v.termrequest, 1, 4) == "\x1b]7;" then local dir = string.gsub(vim.v.termrequest, "\x1b]7;file://[^/]*", "") if vim.fn.isdirectory(dir) == 0 then return end vim.api.nvim_buf_set_var(e.buf, "last_osc7_payload", dir) if vim.o.autochdir and vim.api.nvim_get_current_buf() == e.buf then vim.cmd.cd(dir) end end end }) vim.api.nvim_create_autocmd({ 'bufenter', 'winenter', 'dirchanged' }, { callback = function(e) if vim.b.last_osc7_payload ~= nil and vim.fn.isdirectory(vim.b.last_osc7_payload) == 1 then vim.cmd.cd(vim.b.last_osc7_payload) end end })
You may want to remove the check for vim.o.autochdir depending on your preferences. Then, just make sure your shell is emitting OSC 7 sequences. For bash, add the following to your .bashrc:
function print_osc7() { printf "\033]7;file://$HOSTNAME/$PWD\033\\" } PROMPT_COMMAND="print_osc7${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
For zsh, add the following to your .zshrc:
function print_osc7() { if [ "$ZSH_SUBSHELL" -eq 0 ] ; then printf "\033]7;file://$HOST/$PWD\033\\" fi } autoload -Uz add-zsh-hook add-zsh-hook -Uz chpwd print_osc7 print_osc7
For fish, add the following to your fish config:
function print_osc7 --on-variable=PWD printf "\033]7;file://$HOSTNAME/$PWD\033\\" end
I received a question by email:
Your PROMPT_COMMAND has this printf "\033]7;file://$HOSTNAME/$PWD\033\\" which starts with \033]7; in your lua script you check for "\x1b]7;" which well is different than the first one. I even went a step further and printed the entire vim.v.termrequest and that starts with ^]]7 which is once again different. Initially I thought that was a problem and couldn't for the love of god figure out what's going on. Do you have any insights for a newbie as to what all of this means?
To which I replied:
Basically, it's all the same value, except with different representations. The OSC7 protocol says that an OSC7 sequence starts with the bytes 0x1B 0x5D 0x37 0x3B. The problem is, 0x1B is not a printable character in the ASCII table, it's the byte that stands for the "Escape" character. So we have to use tricks to represent this byte in all the different layers we use.Fun facts:
- Posix printf does not support emitting arbitrary bytes using their hexadecimal representation, so we have to use their octal representation: \033.
- Lua doesn't support octal escape sequences in strings, so we have to use the hexadecimal representation instead, \x1b.
- (Neo)Vim uses the special character sequence ^] to print the byte \033 / \x1B.
I hope that clears things up! If you're interested in learning more about terminal escape sequences, I can recommend reading gpanders' State of the Terminal, it's pretty good!
- If we abandon posix printf and assume GNU printf instead, we can use hexadecimal sequences, i.e. printf "\x1B]7;…". Posix printf explicitly doesn't support hexadecimal sequences because the standards body didn't want to have to choose a maximal length for these sequences (search for "Hexadecimal character constants" in https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/printf.html). GNU printf just decided that hex sequences would be one or two characters.
- When using a Neovim compiled with a Lua 5.1 implementation that isn't LuaJIT (nobody does this, to my knowledge), things will not work. This is because hexadecimal escape sequences in string literals were only added to Lua 5.2 - LuaJIT is a Lua 5.1 implementation but supports hex escape sequences as an extension (see https://luajit.org/extensions.html#lua52). If we wanted to be compatible across Lua implementations, we would have to use the decimal escape sequence "\027]7;" instead.