Blinking prompt cursor after committing with lazygit and urxvt & VSCode term on Windows
Describe the bug
When using lazygit, with rxvt-unicode, and exits, sometimes the cursor is blinking whilst I normally have a non-blinking cursor in the shell.
To Reproduce I believe the following should trigger the behavior:
Start: my prompt cursor is not blinking
mkdir test && cd testgit initecho "Hello" > worldlazygit- Commit the changes with a message (I think any message will do, but here I used "test")
- Quit lazygit
Now, my prompt cursor is blinking.
Expected behavior Cursor should be in the same state as when lazygit was started.
Version info:
$ lazygit --version
commit=, build date=, build source=unknown, version=unversioned, os=linux, arch=amd64, git version=2.41.0
$ equery l lazygit
* Searching for lazygit ...
[I-O] [ ] dev-vcs/lazygit-0.40.0:0
Additional context
- x11-terms/rxvt-unicode-9.31-r2:0
Could you try other terminal emulators? E.g. kitty, terminator etc.?
Kitty 0.29.2
cursor_blink_interval 0 -> ~/.config/kitty/kitty.conf
Ran my test -- no blinking cursor
Alacritty 0.12.1 Ran my test -- no blinking cursor
WezTerm 20230408-112425 Ran my test -- no blinking cursor
Conclusion: urxvt does things differently :-)
(updated the original report)
Also happens on Windows in VSCode terminal. When exiting it changes the configured cursor to (probably a default) blinking block, while my configured style is nonblinking vertical line.
I checked that the last version that didn't change the cursor style upon exiting is 0.29, so maybe that helps
Thank you @bartoszluka, for finding other sources of this issue. I really like my urxvt and would want to see this fixed in lazygit for sure.
Same here on macOS in VSCode terminal. I don't even have to exit — as soon as lazygit prompts me for input (such as a commit message), the cursor is blinking.
Blinking cursors are a severe distraction for some people, so if lazygit must override the cursor, could it at least set it to a non-blinking block?
As it is, I have to juggle positioning VSCode and a separate terminal rather than simply use the built-in VSCode terminal. (Also, as a note, this doesn't occur in iTerm 2, either…)
I've noticed that if I have a blinking cursor, after using lazygit, and I just start nvim/neovim and quit, the cursor is reset to what I want.
Interestingly, lazygit resets the cursor when I quit — it just blinks while I'm using lazygit in VSCode…
I confirm this issue. Seems lazygit changes cursor style for commit message dialog using XTerm control sequences (DECSCUSR). Is there any way to not change cursor style in commit dialog?
XTerm specification seems has no easy way to restore cursor style (discussion), so better to not change it.
I can confirm this with VS Code's terminal on Windows (but not on Mac).
Lazygit does restore the cursor back to the default when it quits, but this whole business of ANSI escape sequences and termcap/terminfo stuff is so much of a science that I'm honestly not sure if this is a bug in VS Code's terminal. Based on a few experiments I made, I'm inclined to say that it is. For example, if you type printf "\e[5 q" then this should set your cursor to a blinking line (which works). printf "\e[0 q" is supposed to set it back to the configured default. And in Mac's Terminal.app it does; however, in VS Code it only sets the shape back to a block, but it keeps blinking. And if you try the same in various other terminal emulators, you'll see all sorts of different behaviors.
Not sure what to do about this; it takes someone with more expertise in this area than me. But in the meantime, you should be able to workaround it by doing something like alias lg='lazygit; printf "\e[2 q"'.
Thanks for that workaround @stefanhaller .. can confirm it works.
@stefanhaller It works, but a bit ugly. Better to not touch cursor style at all, it is not required for lazygit functionality. For example, I use SecureCRT terminal, cursor style: wide vertical bar without blinking. But in lazygit commit text cursor style changes to blinking block for some reason. In the same time, Midnight Commander has a lot of dialog boxes and never changes cursor style.
Also, I can't restore my default cursor style "wide vertical bar", only change narrow vertical bar, seems width is missing in xterm :(
The workaround isn't useful for me, as the cursor still blinks while in lazygit. My requirements are for the cursor to not blink at any time, which is in accordance with my settings — as I said, this is an accessibility issue.
The fix is for lazygit to not mess with the cursor, or to provide a setting which disables all such messing.
Ok, makes sense to me. I'd be open to changing this. (Of course it's not me who makes decisions here, it's @jesseduffield.)
Trouble is, I couldn't find any code in lazygit that does this, nor in gocui which is the TUI library that lazygit is built on. My suspicion is that this needs to be changed in tcell, but I couldn't find the code that's responsible for changing the cursor there, either.
If anybody who is more knowledgeable with this stuff than me can point me to the code that is responsible for this, I'm happy to work on a PR.
Encoutered this issue by switching from Wezterm stable to Nightly on Windows... but only when running lazygit inside a terminal itself running within neovim, no matter the shell used. Had the added perk of having said blinking cursor blink all over the place as I typed into the prompt/commit. Same lazygit version.
Workaround
lazygit (via tcell) sends DECSCUSR 0 to terminal. Behavior depends on terminal implementation (what it consider a default): some switch cursor to large blinking block (like SecureCRT, rxvt, Windows VSCode, WezTerm), some don't support cursor switch or switch cursor to configured session default (like Kitty, Alacritty, Mac's Terminal).
My workaround: I wrote a wrapper script in Python 3 that hides DECSCUSR 0 from terminal. This way user's cursor shape and blinking is preserved both in lazygit UI and after running it.
Install
~/.bashrc:
# Hide cursor shape change control code from terminal
alias lazygit='/opt/scripts/lazygit-wrap'
Usage
/opt/scripts/lazygit-wrap without installed alias or
lazygit with installed alias.
Script can be used with another command (instead of lazygit): lazygit-wrap -- <cmd> [args...].
Cursor change command can be not stripped, but replaced by custom cursor: environment variable LAZYGIT_WRAP_CURSOR=6.
Script
/opt/scripts/lazygit-wrap:
#!/usr/bin/env python3
"""
Drop 'DECSCUSR 0': 'ESC[0 q'.
Forwards all CLI args to 'lazygit' by default.
To wrap another command: lazygit-wrap -- <cmd> [args...]
Optional: environment variable LAZYGIT_WRAP_CURSOR=6 -> translate 'ESC[0 q' -> 'ESC[6 q'
"""
from __future__ import annotations
import fcntl, os, pty, select, signal, struct, sys, termios, tty
STDIN, STDOUT = 0, 1
READ_APP, READ_TTY = 262_144, 4_096
MARK = b"\x1b[0 q" # 'ESC[0 q'
MAX_KEEP = len(MARK) - 1
val = os.getenv("LAZYGIT_WRAP_CURSOR")
if val is not None and val.isdigit() and len(val) == 1:
REPL = f"\x1b[{val} q".encode()
else:
REPL = b""
def _winsz(fd: int = STDOUT) -> tuple[int, int]:
try:
s = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\0"*8)
r, c, _, _ = struct.unpack("HHHH", s)
return (r or 24, c or 80)
except Exception:
return (24, 80)
def _set_winsz(fd: int, rc: tuple[int, int]) -> None:
try:
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rc[0], rc[1], 0, 0))
except Exception:
pass
def _filter(tail: bytes, chunk: bytes) -> tuple[bytes, bytes]:
buf = chunk if not tail else (tail + chunk)
if MARK in buf:
buf = buf.replace(MARK, REPL)
keep = 0
end = min(MAX_KEEP, len(buf))
for k in range(end, 0, -1):
if buf.endswith(MARK[:k]):
keep = k
break
return (buf[:-keep], buf[-keep:]) if keep else (buf, b"")
def _cmd_from_argv(argv: list[str]) -> list[str]:
if not argv:
return ["lazygit"]
if argv[0] == "--":
return argv[1:] or ["lazygit"]
# Default: pass all args to lazygit
return ["lazygit", *argv]
def main(argv: list[str]) -> int:
cmd = _cmd_from_argv(argv)
pid, master = pty.fork()
if pid == 0:
os.execvp(cmd[0], cmd)
os._exit(127)
_set_winsz(master, _winsz())
old = termios.tcgetattr(STDIN)
tty.setraw(STDIN)
def on_winch(*_):
_set_winsz(master, _winsz())
try:
os.kill(pid, signal.SIGWINCH)
except ProcessLookupError:
pass
signal.signal(signal.SIGWINCH, on_winch)
tail = b""
try:
while True:
r, _, _ = select.select([STDIN, master], [], [])
if master in r:
chunk = os.read(master, READ_APP)
if not chunk:
break
out, tail = _filter(tail, chunk)
if out:
os.write(STDOUT, out)
if STDIN in r:
data = os.read(STDIN, READ_TTY)
if not data:
break
os.write(master, data)
finally:
if tail:
os.write(STDOUT, tail)
termios.tcsetattr(STDIN, termios.TCSADRAIN, old)
try:
_, st = os.waitpid(pid, 0)
if os.WIFEXITED(st):
return os.WEXITSTATUS(st)
if os.WIFSIGNALED(st):
return 128 + os.WTERMSIG(st)
except ChildProcessError:
pass
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))