Emacs has had a spell-checker for a very long time. In fact, I’ve been using ispell-word for at least several years now, as I have mentioned for example here. However, I’ve only used Flyspell mode once, over two decades ago, when I tried it out on a machine with maybe 8MB RAM and a Pentium-class processor clocked at about 100MHz and decided that it’s too slow to be really useful.
Well, things have changed a bit since then, haven’t they?
My current laptop has 32GB RAM and a 6-core i7 processor at 2.2GHz. I figured it should be fast enough to run a spell-checker, so I tried to enable flyspell-mode again.
For some reason, it still felt laggy. It may have had something to do with the fact that I ran it on a several-megabytes-large Org mode file. Happily, I remembered reading about Jinx, and I quickly tried it out. After a few minutes I decided to install it and added this to my init.el:
(add-hook 'emacs-startup-hook #'global-jinx-mode) (keymap-global-set "M-$" #'jinx-correct)
The default settings were not enough for me, though. I had quite specific needs. First of all, I needed Jinx to support two languages – English and Polish. That is easy enough:
(setq jinx-languages "en_US pl_PL")
Probably a better solution would be to have some code to detect and set the language of a file when visiting it. That would be a bit more complex, though, and would not help anyway since my personal Org files sometimes have both English and Polish inside.
The next problem was slightly more tricky. I needed Jinx to only use Polish in one specific directory. (I wanted spell-checking to work for my 9-year-old son, who’s just started to learn English, so he only writes in Polish.) Easy enough with per-directory local variables, especially that Emacs has the convenient add-dir-local-variable command. Here are the contents of the .dir-locals.el file in the directory where my son’s files live:
;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((jinx-languages . "pl_PL"))))
Of course, having Emacs variable-safety mechanism kick in whenever any file from that directory is visited is no good, either:
(add-to-list 'safe-local-variable-values '(jinx-languages . "pl_PL"))
The next customizations are even more tricky. Jinx’s way of selecting the correct spelling of a word is pretty sophisticated and I like it quite a lot. However, it turns out that Ivy interferes with it. I could write a simple wrapper around jinx-correct, disabling Ivy for a moment, but that would require rebinding M-$ in the jinx-overlay-map keymap (which is used by jinx-overlay). This means that I want jinx-correct itself to disable Ivy. Advice to the rescue:
(defun disable-ivy (orig-fun &rest args)
"Run ORIG-FUN with Ivy disabled."
(interactive)
(let ((completing-read-function #'completing-read-default))
(apply orig-fun args)))
(advice-add 'jinx-correct :around #'disable-ivy)
The next tweak I wanted was related to an excellent article about Avy, another of Oleh Krehel’s incredibly useful packages. It turns out that Avy can be used for a lot more things than just quickly jumping to places, and spell-checking is one of them. You can use Avy to point Emacs to a misspelled word anywhere in the buffer and select the ispell Avy action to correct it without moving point. (Well actually, the point moves internally of course, but the whole action is wrapped in save-excursion so that the user never notices that. Also, it turns out that when the misspelled word is in another buffer, the point is moved to its current position in that buffer. While technically this is probably a bug, I can definitely live with that – correcting a word in another buffer is weird enough a case that I don’t care too much about this behavior.) So, I want that Avy action to use Jinx, too:
(defun avy-action-jinx-correct (pt)
"Use Jinx to correct word at PT."
(save-excursion
(goto-char pt)
(jinx-correct)))
(setf (alist-get ?i avy-dispatch-alist)
#'avy-action-jinx-correct)
Another issue I wanted to change was that I really did not want Jinx to insert its name together with a very lengthy language name (en_US
pl_PL) into my modeline. My normal way of using Emacs is with two windows side-by-side, and too much stuff in the modeline really annoys me, so I added jinx-mode to mode-line-collapse-minor-modes. (I used to use Diminish, but Emacs now has a good replacement. Notice that as of writing this, you need to compile Emacs from sources to have this very new feature!)
The final thing is that I do have some files I don’t want to be spell-checked. As I learned recently, it is enough to say
# Local Variables: # jinx-mode: nil # End:
in such files.
And that’s it for my Jinx config. I have to say that I love it! Apart from being fast, it has quite a few nice features. For example, jinx-correct just corrects the nearest word if the current one is already spelled correctly. (In that case, “nearest” means the nearest one before point, and if all words visible in the buffer before point are spelled correctly, the nearest one after point.)
Happy spell-checking!
Some time ago, a friend of mine asked if I know an app which could allow him to analyze his spending based on bank statements. I did a quick research and I found a few tools like this, but nothing looked good enough for me to safely recommend it to him. Fortunately for him, he asked me that question in the exact moment in my life when (a) I had a lot of free time and (b) I wanted to spend it learning some potentially useful new skills. It didn’t take me long to decide to code such a tool myself.
After some back and forth with an LLM I settled on Python. I learned a tiny bit of Python in my student years, but never wrote anything more than a few dozen lines. Also, I figured that writing an application from scratch with a coding agent will be a good way to learn to use it. Of course, the risk is that I will just prompt it and repeatedly press enter instead of actually learning Python. That’s why I started with putting this in the instructions for the agent:
# Learning guidance **IMPORTANT** This is a learning project — the user writes the code himself. - When explaining Python concepts, bring up Node.js analogues when they help clarify (e.g. "this is like `require.main === module` in Node.js") - Guide and explain rather than writing implementation code unprompted - Only write code when explicitly asked to. When the user says "let's get coding", "time to code now", or something similar, it means *the user* wants to start coding. - When asked to check or review my code, be critical and honest – I want signal, not comfort. Pay attention to correct but not idiomatic code and suggest improvements.
I started with tkinter, but pretty soon migrated to PySide6 because I couldn’t get the coding agent to do what I needed to do in the former. I also added this to the instructions:
- Exception: I don't need to learn UI coding in Python – it's ok and in fact desirable for you to edit/code UI parts yourself.
A bit later, I also added this:
# User profile Even though I develop this on GNU/Linux, assume that the end-user will be a Windows/Excel user.
I have to say that the about 32 hours I’ve spent on this project, delivering a working prototype (genuinely useful despite some rough edges!) were a very pleasant experience. I learned quite a bit of Python. It turned out that its syntax is even worse than it was two decades ago (and that is quite an accomplishment!), but the standard library is absolutely great (especially compared to Node’s one, although that bar is not particularly high…). Basically, the only external dependencies I needed were PySide6, python-dotenv, pytest and a few developer tools: pyright, Ruff and PyInstaller. Everything else, including SQLite support, parsing and generating csv and timestamp support is built-in. This is in a very stark contrast to Node.js, where you have a ton of, say, CSV parsers on NPM, half of which probably contain some malware, and none built-in…
Still, I missed a few things from the JS ecosystem. For example, hot reload where saving a file immediately updates the web app you are developing is great. A poor man’s version I used was
$ while [ true ]; do uv run python main.py; done
and exiting the app whenever I wanted to see the changes live – but of course it’s far from a real hot reload, where you stay on the same view, with the same data etc. (It was enough for my simple app, though.) I found this project, but haven’t yet had a chance to use it.
Anyway, the app is on GitHub. Expect quite a few rough edges, do not expect I will review (not to mention merge) any PRs etc. – I am really busy these days, and if you happen to need my code, that’s great, but don’t count on me doing any work with it. (That doesn’t mean I won’t – I definitely want to, but without any guarantees.)
See you next week!
As I wrote many times, I use pdf-tools as main pdf viewer. I thought that one thing that would be nice is being able to open pdfs in it from outside Emacs. For example, texdoc could use it instead of Evince (which is pretty nice, but it’s not Emacs!).
Finally, I sat down to it. (Of course, my solution is Linux-centric; it might work on a Mac, but I have no access to such a weird machine to check it.) The first step is to create an emacsclient.desktop file in ~/.local/share/applications:
[Desktop Entry] Name=Emacs Exec=emacsclient -r -a emacs Type=Application MimeType=application/pdf;text/plain;text/x-tex NoDisplay=false Terminal=false
You can add more MimeTypes if you want. Then you run update-desktop-database ~/.local/share/applications/ so that file managers (other than Dired) will know about “opening with Emacs”. Finally, make this the default for pdfs: xdg-mime default emacs-pdf.desktop application/pdf. And from now on, double-clicking a pdf in a graphical file manager or launching texdoc <package-name> will open pdf files in Emacs (using the current Emacs instance if the server is running).