lazygit icon indicating copy to clipboard operation
lazygit copied to clipboard

Blinking prompt cursor after committing with lazygit and urxvt & VSCode term on Windows

Open ygaeon opened this issue 2 years ago • 14 comments

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

  1. mkdir test && cd test
  2. git init
  3. echo "Hello" > world
  4. lazygit
  5. Commit the changes with a message (I think any message will do, but here I used "test")
  6. 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

ygaeon avatar Aug 20 '23 08:08 ygaeon

Could you try other terminal emulators? E.g. kitty, terminator etc.?

mark2185 avatar Aug 20 '23 11:08 mark2185

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)

ygaeon avatar Aug 20 '23 15:08 ygaeon

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

bartoszluka avatar Sep 28 '23 09:09 bartoszluka

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.

ygaeon avatar Sep 28 '23 09:09 ygaeon

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…)

unikitty37 avatar Jul 12 '24 08:07 unikitty37

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.

ygaeon avatar Jul 12 '24 08:07 ygaeon

Interestingly, lazygit resets the cursor when I quit — it just blinks while I'm using lazygit in VSCode…

unikitty37 avatar Jul 12 '24 08:07 unikitty37

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.

Vanav avatar Oct 10 '24 07:10 Vanav

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"'.

stefanhaller avatar Oct 10 '24 09:10 stefanhaller

Thanks for that workaround @stefanhaller .. can confirm it works.

ygaeon avatar Oct 10 '24 09:10 ygaeon

@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 :(

Vanav avatar Oct 10 '24 20:10 Vanav

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.

unikitty37 avatar Oct 11 '24 08:10 unikitty37

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.

stefanhaller avatar Oct 11 '24 10:10 stefanhaller

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.

woertsposzibllen4me avatar Apr 30 '25 23:04 woertsposzibllen4me

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:]))

Vanav avatar Aug 28 '25 23:08 Vanav