0

One of the biggest problems I currently have in Vim is I start working enter a few commands and suddenly all key binds and commands start acting up. After a few seconds of panic and trying to do the same commands and figure out what is wrong, I'll look down and see the caps lock indication on the caps lock button.

To try and circumvent this in the future, is there a way in Vim to either display caps lock on the statusline or capture and alert when caps lock is pressed?

4
  • 1
    Vim typically can't figure out the status of the Caps Lock key, it's typically not exposed by the terminal API... Most OSs offer ways to remap the Caps Lock key so that it acts as Ctrl or as Esc, which is usually more useful than Caps Lock itself... Perhaps that's a good option for you?
    – filbranden
    Jun 11 at 4:21
  • Yeah that could be an option. Even an answer of it can't be done status line, but you can configure it so it doesn't affect vim would be nice.
    – Dom
    Jun 11 at 4:28
  • 2
    Which system are you using? On Linux (and most other Unix-y systems) you can probably work around these kind of limitations by querying some /sys/something and/or listening for keyboard events (you can start an async function or process for this), or maybe even remap caps lock in your WM/DE to enable caps lock and send some signal (pkill -USR1 vim and add a USR1 autocmd?) It's a bit involved, but probably doable with some hackery if you care enough. Jun 11 at 6:25
  • Linux specifically Ubuntu 20.04.
    – Dom
    Jun 11 at 14:41
2

Vim supports inter-process communication via its +clientserver feature. If it is listed in vim --help, you can solve it by starting Vim as a server (if using Gvim, it by default starts as a server so you could skip this step):

vim --servername CAPS

Then have the following shell script running in the background. It requests all Vim servers to modify the status-line whenever it learns from xset q that the caps lock state has been toggled.

#IFS='
#'
while sleep 0.5; do
    xset q | grep -q 'Caps Lock: *on' && now=+ || now=-
    [ "$before" != "$now" ] &&
        for server in $(vim --serverlist); do
            vim --servername "$server" \
                --remote-expr "execute('set statusline${now}=[C]|redraw')"
        done
    before="$now"
done

Notes

1Uncommenting the IFS lines improves the script; it will correctly handle server names with whitespace. However, that makes it easy for someone unfamiliar with the shell to break the script when trying to extend it. I guess the tradeoff isn't worth it.

2My suggestion is that you expose that information somewhere else, both for modularity's sake and because it may be a useful information for other programs as well (there are also scroll lock and num lock to consider). E.g. use a system tray icon/applet or a minimally powerful panel such as Dzen.

3Thanks to D. Ben Knoble for suggesting redraw, without which the status-line update would be delayed.

4Signals are another form of IPC. If +clientserver was not compiled in, Martin's suggestion of sending a USR1 signal is an alternative. You'd only need to add the appropriate autocmd in Vimrc and changing the main line of the shell script.

5
  • Not sure if launching two processes every half second is necessarily the best solution, although I can't really find a good way to intercept keys without a small custom C program (like this). Personally I'd probably use xcape to make caps lock generate a caps lock + F20 (or another unused key), and then remap F20 in my WM to send a signal to Vim like in your example. Bit more work, but saves CPU cycles 🙃 I don't know if DEs like gnome allow easy mapping of F20 etc. Jun 13 at 17:33
  • Also, a caveat with the clientserver feature is that it's synchronous and that the sending process will hang forever waiting for Vim to become available if it's suspended with ^Z; in your script one suspended Vim process will stop the loop and it won't send out the signal to any other open Vims. Not an issue if you never/rarely use ^Z, but if you frequently do (like me) then you'll probably run in to problems with this. Jun 13 at 17:36
  • One way to solve this is by making you script write a file with the status in /tmp or something, and then use job_start() in Vim to read from that. Jun 13 at 17:38
  • @MartinTournoij Pertinent observations. I also do not like the idea of spawning a couple of processes every 0.5 s, even though the increase in the CPU load will be negligible (of course anyone can change the timeout if found otherwise). I suppose one could simply background the --remote commands themselves so that they won't block in case the target server had been suspended. This is meant more as a proof of concept by pointing a doable path; I do not think that this is a job for a text editor.
    – Quasímodo
    Jun 14 at 10:45
  • You're going to end up with a lot of waiting background processes if you use & and you run the risk of starving the system of resources if you keep a Vim suspended for long enough! But yeah, it's a fine proof-of-concept; getting all this IPC stuff right is tricky; just wanted to add some comments to point out (potential) caveats for current and future readers :-) Jun 14 at 15:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.