Lorenzo Bettini is an Associate Professor in Computer Science at the Dipartimento di Statistica, Informatica, Applicazioni "Giuseppe Parenti", Università di Firenze, Italy. Previously, he was a researcher in Computer Science at Dipartimento di Informatica, Università di Torino, Italy.
He has a Masters Degree summa cum laude in Computer Science (Università di Firenze) and a PhD in "Logics and Theoretical Computer Science" (Università di Siena).
His research interests cover design, theory, and the implementation of statically typed programming languages and Domain Specific Languages.
He is also the author of about 90 research papers published in international conferences and international journals.
I report my first experiences with LM Studio in (Arch) Linux.
LM Studio is a desktop application that allows you to discover, download, and run Large Language Models (LLMs) entirely on your own computers, offline.
This is the hardware and system I’m testing it (you see, no GPU with VRAM):
In the past, I had several problems using archinstall for installing Arch Linux in a dual boot with another existing Linux installation. In fact, I gave up using archinstall and created my own script for installing Arch.
In particular, I experienced these two problems:
Archinstall failed installing Arch if the disk already contained a BTRFS partition
The “/boot/efi/” mount point could not be used: you had to use “/boot” (since all my other Linux installations use “/boot/efi”, which required a few tweaks that I was not willing to spend time on)
I tried once more with the July 2026 version, and both problems are finally gone (though, for the second one, there are a few things to know, as shown in the rest of this blog post).
Besides experimenting with Archinstall for dual boot with another Linux installation, I’ll also take the chance to quickly review a few parts of the installation procedure.
I experimented with the process on a KVM virtual machine, where I had already installed EndeavourOS, leaving some space for another installation.
Let’s keep an eye on the current state of “/boot/efi” and see later how Archinstall handles it.
And here’s the UEFI entries; the existing EndeavourOS installation is the “UEFI Misc Device”.
Let’s start the installation by booting the Arch Linux July 2026 ISO and running the Archinstall script:
The crucial part is disk partitioning, where we must select “Manual Partitioning”
Select the existing UEFI partition
And assign the “/boot/efi” mount point:
Then, select the empty partition, and create a new BTRFS partition:
Now, the sad part: I’m used to EndeavourOS or other Calamares-based installers that automatically create the default subvolumes when you create a BTRFS partition for the “/” mount point. Instead, in Archinstall, you have to deal with the subvolumes yourself:
Following the same process four times, I created the standard subvolumes (I skipped the ones for “/var/lib/portables” and “machines, which EndeavourOS creates, since I don’t care about them):
This is the final layout:
Now, select GRUB:
Here’s the crucial part: “Install to removable location”, enabled by default. If you press ENTER, you get more details:
The text explains a few things and tells you it is safe to leave it on.
However, if you do leave it on, then Archinstall will overwrite the NVRAM entry for existing Linux installations.
I tried both options in two different virtual machines; let’s see what happens:
Select NO
With this option, Arch will be the default boot option in the UEFI; however, existing entries can still be selected from the UEFI menu.
So, let’s select NO, and here’s what that entry reports:
Let’s conclude the installation and reboot.
Here’s the GRUB from Arch; let’s enter the UEFI settings
“GRUB” is Arch Linux, and “UEFI Misc Device”, if you look at the beginning of the blog post, is the existing EndeavourOS:
In fact, if we select it and press ENTER, we get to the EndeavourOS GRUB menu:
And here’s how “/boot/efi” has been modified:
Select YES
Let’s instead select YES (as I said, this is a parallel experiment on a different virtual machine, with the same existing EndeavourOS installation).
Of course, we have the Arch GRUB:
And the “/boot/efi/” still has the previously existing EndeaavourOS efi file.
However, if we enter the UEFI settings:
We no longer have an entry for EndeavourOS. This time, “UEFI Misc Device” is the Arch entry, not the EndeavourOS anymore.
However, we can manually re-add the EndeavourOS entry, because its EFI file is still there.
The procedure depends on the UEFI Firmware settings; in this case, we use the settings provided by “Virtual Machine Manager” Tiano Firmware settings:
We have to navigate to the EndeavourOS EFI file:
And now we have the EndeavourOS entry back (this time, with its real name):
Final thoughts
Things have improved, and Archinstall is probably usable for dual-booting now. I’ll try that on real hardware.
However, I still find it’s still far from being as useful as Calamares-based installers of EndeavourOS, CachyOS, or Garuda, to name a few.
I use Fedora Silverblue with GNOME, and I have LibreOffice installed as a Flatpak.
Recently, I hit a strange problem: when searching for an office document from the GNOME Dash — for example, an .odt or .ods file in my Dropbox folder — and selecting it, LibreOffice appeared to start, but then failed with an error saying that a file like this did not exist:
flatpak run org.libreoffice.LibreOffice"/var/home/bettini/Dropbox/sync/path/to/file.odt"
So LibreOffice itself was not broken.
The problem only happened when opening documents from the GNOME Dash search results.
The mysterious /run/user/1000/doc path
When GNOME opened the file from the Dash, LibreOffice complained about a path like this:
1
2
/run/user/1000/doc/149071dc/myfile.odt
This is not a normal temporary directory.
It is part of the Flatpak / XDG document portal mechanism. The document portal exposes selected host files to sandboxed applications through a FUSE mount under:
1
2
/run/user/$UID/doc
The directory name that looks like a hash is a document portal ID.
In my case:
1
2
149071dc
was the document ID.
The file under /run/user/1000/doc/... was not a copy. It was a portal-backed view of the original file. That explains why deleting it also deleted the original document.
chezmoi_modify_manager is needed while chezmoi apply computes file contents
but the usual .chezmoiscripts/run_before_* hooks run too late to install it for the first apply
I wanted a solution that was:
automatic
shell-based
version-pinned
rerun only when the installer script changes
This post shows the setup I ended up with.
The problem
At first glance, it seems natural to put an installer script into .chezmoiscripts, for example, as a run_onchange_before_* script.
That helps with normal lifecycle management, but it does not solve the initial bootstrap.
Why? Because chezmoi needs to compute the target state before it runs those scripts, and modify_ files participate in that computation. If those files use chezmoi_modify_manager, then the binary must already exist beforechezmoi apply reaches the script phase.
So the core issue is:
chezmoi_modify_manager must be installed before chezmoi apply starts doing the work that depends on it.
The approach
The solution is to split the problem into two:
Put the installer in .chezmoiscripts as a run_onchange_before_* script, so the script is tracked by chezmoi and reruns when its contents change.
Wrap chezmoi with a small shell function that manually runs that installer script before delegating to the real chezmoi apply.
That gives us the best of both worlds:
proper chezmoi-managed script in the source repo
successful first bootstrap
automatic reinstall when the installer changes, such as when bumping the pinned version
The script is version-pinned, architecture-aware, and idempotent. It also keeps a tiny version stamp, so it can skip work when the correct version is already installed.
STAMP stores the installed version so the script can cheaply detect whether work is needed
--doctor provides a sanity check after installation
If you later want to upgrade, just change the VERSION string in the script. Because the script is a run_onchange_* script, chezmoi will detect the change.
Manual bootstrapping
After running “chezmoi init <URL>” and before running “chezmoi apply” for the first time, you must remember to manually execute this script
The application under test is an Eclipse-based application, so at first I expected the failure to be caused by some SWTBot timing issue, focus issue, or maybe a regression in my own code.
However, the screenshot captured by the failed test showed something completely unrelated to my application:
That is not Eclipse. That is macOS Setup Assistant.
More specifically, it was the macOS Analytics screen asking whether to share analytics data with Apple and app developers. Since SWTBot drives the UI that currently has focus, this dialog was stealing focus from Eclipse and making the test interact with the wrong window.
The first workaround
My first attempt was to dismiss the Analytics dialog before starting the SWTBot tests:
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- name: Dismiss macOS Analytics setup dialog
if: runner.os == 'macOS'
shell: bash
run: |
foriin{1..30};do
ifpgrep-x"Setup Assistant">/dev/null;then
echo"Found macOS Setup Assistant; trying to dismiss it"
osascript<<'APPLESCRIPT'||true
tellapplication"System Events"
ifexistsprocess"Setup Assistant"then
tellprocess"Setup Assistant"
setfrontmosttotrue
delay0.2
try
clickbutton"Continue"ofwindow1
onerror
keycode36
endtry
endtell
endif
endtell
APPLESCRIPT
sleep2
pgrep-x"Setup Assistant">/dev/null||exit0
fi
sleep1
done
This looked reasonable, but then the failure changed.
The new screenshot showed another Setup Assistant page:
This time it was the “Welcome to Mac” screen.
So the script was managing to click something, but Setup Assistant was still not completely gone. SWTBot was still losing focus before the actual Eclipse application could be tested.
The final workaround
The most robust fix was to avoid heredocs entirely and pass the AppleScript using repeated osascript -e arguments.
Here is the workflow step I now use:
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- name: Remove macOS Setup Assistant
if: runner.os == 'macOS'
shell: bash
run: |
foriin{1..10};do
if!pgrep-x"Setup Assistant">/dev/null;then
echo"Setup Assistant is not running"
exit0
fi
echo"Setup Assistant is running; trying to dismiss it"
osascript\
-e'tell application "System Events"'\
-e'if exists process "Setup Assistant" then'\
-e'tell process "Setup Assistant"'\
-e'set frontmost to true'\
-e'delay 0.2'\
-e'try'\
-e'perform action "AXPress" of button "Continue" of window 1'\
-e'end try'\
-e'delay 0.5'\
-e'end tell'\
-e'key code 36'\
-e'end if'\
-e'end tell'>/dev/null2>&1||true
sleep2
done
echo"Setup Assistant did not go away; killing it"
pkill-9-x"Setup Assistant"||true
sleep2
ifpgrep-x"Setup Assistant">/dev/null;then
echo"::error::Setup Assistant is still running"
exit1
fi
This does three things:
It checks whether Setup Assistant is running.
It tries to press the visible Continue button.
If Setup Assistant still refuses to disappear, it kills the process before the UI tests start.
After adding this step before launching Eclipse, the SWTBot tests started passing again.
Why this matters for SWTBot
SWTBot tests are particularly sensitive to focus problems.
They do not just test some isolated backend logic. They drive a real UI. If another native macOS window appears in front of the Eclipse workbench, the test can fail in confusing ways:
1
2
3
4
WidgetNotFoundException
TimeoutException
Could notfind shell
Could notfind button
The actual problem may have nothing to do with SWTBot or Eclipse. The test may simply be interacting with the wrong application.
That is why screenshots from failed UI tests are invaluable. Without the screenshot, I would probably have spent a lot more time debugging Eclipse, SWTBot, or my own application.
Sometimes a command installed with Homebrew needs to be executed as root. A good example is nethogs, which needs elevated privileges to inspect network traffic.
The problem is that this often fails:
1
2
sudo nethogs
even though this works:
1
2
nethogs
The reason is that sudo usually runs with a different, restricted PATH. Your normal shell can find Homebrew commands, but sudo may not know where Homebrew installed them.
On Linux, Homebrew commands may live somewhere like:
1
2
3
/home/linuxbrew/.linuxbrew/bin
/home/linuxbrew/.linuxbrew/sbin
On macOS, they are commonly under:
1
2
/opt/homebrew
on Apple Silicon, or:
1
2
/usr/local
on Intel Macs.
The simple one-command solution
A quick workaround is to resolve the command path before calling sudo:
1
2
sudo"$(which nethogs)"
This works because which nethogs is evaluated by your normal shell, using your normal PATH, before sudo runs.
So instead of asking sudo to find nethogs, you give it the full path directly.
A shell function for one command
You can wrap that in a function:
Shell
1
2
3
nethogs(){
sudo"$(which nethogs)""$@"
}
This lets you run:
1
2
3
4
nethogs
nethogs eth0
nethogs-d5
and still preserve arguments correctly.
A general brew-sudo helper
A better cross-platform approach is to define a small helper that asks Homebrew for its prefix, checks both bin and sbin, and then runs the matching executable with sudo.
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
brew-sudo(){
if["$#"-eq0];then
echo"usage: brew-sudo <command> [args...]">&2
return2
fi
cmd="$1"
shift
prefix="$(brew --prefix)"||return
fordir inbin sbin;do
exe="$prefix/$dir/$cmd"
if[-x"$exe"];then
sudo"$exe""$@"
return$?
fi
done
echo"brew-sudo: $cmd not found in $prefix/bin or $prefix/sbin">&2
return127
}
Now you can run Homebrew-installed commands with root privileges like this:
1
2
3
4
brew-sudo nethogs
brew-sudo nethogs eth0
brew-sudo some-command--some-option
This works across Linux and macOS because brew --prefix returns the correct Homebrew prefix for the current system.
Defining command-specific wrappers
You can then define convenient wrappers in terms of brew-sudo:
Shell
1
2
3
nethogs(){
brew-sudo nethogs"$@"
}
This gives you the best of both worlds:
1
2
3
4
nethogs
nethogs eth0
nethogs-d5
while internally resolving to something like:
1
2
sudo/home/linuxbrew/.linuxbrew/sbin/nethogs"$@"
on Linux, or the corresponding Homebrew path on macOS.
Full version
Put this in a shell file sourced by both bash and zsh, or copy it into both ~/.bashrc and ~/.zshrc:
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
brew-sudo(){
if["$#"-eq0];then
echo"usage: brew-sudo <command> [args...]">&2
return2
fi
cmd="$1"
shift
prefix="$(brew --prefix)"||return
fordir inbin sbin;do
exe="$prefix/$dir/$cmd"
if[-x"$exe"];then
sudo"$exe""$@"
return$?
fi
done
echo"brew-sudo: $cmd not found in $prefix/bin or $prefix/sbin">&2
return127
}
nethogs(){
brew-sudo nethogs"$@"
}
A note on sudo and Homebrew
This approach does NOT run brew itself with sudo. It only uses brew --prefix to locate Homebrew’s installation directory, then runs a specific Homebrew-installed executable with sudo.
That distinction matters: running brew install or brew upgrade with sudo is generally the wrong approach, but running a tool like nethogs with sudo is appropriate when the tool itself needs root privileges.
Alternative: adding Homebrew to /etc/sudoers
Another possible solution is to teach sudo about Homebrew’s executable directories.
On many systems, sudo uses a restricted path configured in /etc/sudoers, often through a setting called secure_path. You can inspect and edit it with:
It does not give users new sudo permissions by itself. A user still needs to be allowed to run commands with sudo in the first place. But it does make Homebrew-installed commands part of sudo’s command lookup path.
That can be undesirable because Homebrew prefixes are often writable by the normal user, or by users in an admin group. In general, root’s PATH should avoid directories that non-root users can modify. Otherwise, it becomes easier to run a user-controlled executable as root accidentally.
For that reason, I prefer the wrapper approach shown above.
That works well for the very first setup, because my SSH keys are not installed yet.
After the first chezmoi apply, though, my SSH keys are finally in place, and I want the dotfiles repository itself to switch from HTTPS to SSH automatically.
This sounds simple, but there is a small gotcha.
The goal
I wanted chezmoi apply to do this at the end of the first run:
detect that the repo remote is using HTTPS
convert it to the SSH form
update origin automatically
For example:
from https://github.com/username/dotfiles.git
to git@github.com:username/dotfiles.git
My first attempt
My first version used a run_once_after_... script that called chezmoi source-path to find the source repo:
The problem is that the script is being run bychezmoi apply.
So if the script itself runs another chezmoi command, the second process tries to acquire the same persistent state lock that the original chezmoi apply already holds.
In other words:
chezmoi apply starts
it runs your script
your script runs chezmoi source-path
the nested chezmoi process waits on the lock
eventually it times out
The fix
The solution is to avoid calling chezmoi inside the script.
Instead, make the script a template and use {{ .chezmoi.sourceDir }} to inject the source directory path when chezmoi renders the script.
On Atomic Desktops, you can keep using your computer while applying OS updates as they are downloaded and installed in the background. Once an update has been installed, you can reboot your computer to start using the new version. You will not have to wait for the update to be installed either during shutdown or boot up.
On Fedora Silverblue and Fedora Kinoite, OS updates are downloaded automatically and you will be notified when updates are ready to be applied via a reboot. This behavior can be changed in the settings.
On a fresh installation, these are the details of the installed system:
If we open the Gnome Software, we can see that updates are already detected:
Actually, the system is already downloading the updates (and applying them on a new image); after a while, you can simply restart:
After the reboot, the updated image will take effect:
In particular, just rebooting will lead you to a possibly already downloaded and updated image.
Automatic update behavior can be seen in the Gnome Software preferences:
From the command line, you can check available updated images:
GPGSignature: Valid signature by C6E7F081CF80E13146676E88829B606631645531
The “status” command also shows when the new image is being downloaded; for example, after a fresh installation, a new upgrade will be automatically checked and downloaded:
When setting up Sway as your Wayland compositor, you might want to organize your Waybar configuration in a non-standard location — for example, keeping a dedicated ~/.config/waybar/sway/ directory to separate your Sway-specific bar configuration from others.
This seemingly simple task comes with a couple of subtle pitfalls worth knowing about.
The ~ Expansion Problem
The most natural approach would be to launch Waybar directly from the bar block in your Sway config:
Unfortunately, this does not work. The swaybar_command directive does not perform shell expansions, so ~ is passed literally to the command instead of being expanded to your home directory. Waybar will fail to find the configuration file.
A workaround is to launch Waybar via exec instead:
However, this approach has its own drawback: Waybar launched via exec is no longer managed by Sway’s bar subsystem. This means that Sway bar commands — such as swaymsg bar hidden_state toggle to show/hide the bar — will not work.
The Solution: A Wrapper Script
The cleanest solution is to write a small wrapper script that handles the expansion and launches Waybar with the desired configuration:
Now Waybar is properly managed by Sway’s bar subsystem, and the script handles path expansion.
The PATH Problem
There is one more subtle issue: the script lives in ~/.local/bin/, which may not be in PATH when Sway starts. Sway is typically launched from a display manager or TTY, where the environment is minimal and does not source your shell’s configuration files (e.g., ~/.bashrc or ~/.zshrc).
To ensure ~/.local/bin is in PATH for Sway and all processes it spawns, add it in your login shell profile:
1
2
export PATH="$HOME/.local/bin:$PATH"
Your display manager or TTY login will source this file, making the script discoverable when Sway starts.
Fedora Silverblue is Fedora’s atomic, GNOME-based desktop built around an image-based, mostly read-only system rather than a traditional mutable Linux install.
As an atomic desktop: the base OS is image-based, updates are applied as a new deployment, and you switch to it on reboot. That gives you an easy rollback if an update goes wrong.
It is designed to separate apps from the host OS: GUI apps are primarily installed as Flatpaks, command-line/dev tooling is commonly run in Toolbx containers, and traditional RPMs can still be added through the rpm-ostree layering when needed.
The main upside is stability and reproducibility: Fedora says Atomic Desktops are intended to be more stable, easier to test, and well-suited to containerized apps and development.
Thus, it is rather different from other standard “mutable” Linux distros, especially when it comes to installing software.
I’m starting a series of blog posts on Fedora Silverblue.
At the moment, I’m NOT considering using it as my daily driver (which, for now, is Arch, specifically EndeavourOS).
The installation process
The installation procedure is the typical Fedora one:
It detected my language (though I then switched to English):
Then, I switched to the Italian keyboard layout:
I checked the time, and it was already correct; I also checked the Internet time synchronization:
Time to deal with disk partitioning (“Installation Destination”).
Since I’m testing this on a virtual machine, I’ll stick with the automatic partitioning.
And we can now start with the installation:
The “Writing objects” text stays there for a long time.
Then, the rest of the installation steps are performed, including the installation of apps:
Then, the progress bar basically goes directly to the end. The installation completed in a few minutes:
First boot
During the first boot, you configure a few things (most of them, taken from the installation process, but to be confirmed):
I’ll skip the GNOME tour.
The installed system
Concerning installed applications, it’s rather minimal:
You have a terminal (not the Gnome terminal, but “ptyxis) and Firefox:
Let’s see the layout of the partitions:
1
2
3
4
5
6
7
8
9
10
11
12
bettini@fedora:~$ sudo lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sr0 11:0 1 1024M 0 rom
zram0 251:0 0 7.7G 0 disk [SWAP]
vda 253:0 0 80G 0 disk
├─vda1 253:1 0 600M 0 part /boot/efi
├─vda2 253:2 0 2G 0 part /boot
└─vda3 253:3 0 77.4G 0 part /var/home
/var
/sysroot/ostree/deploy/fedora/var
/sysroot
/etc
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bettini@fedora:~$cat/etc/fstab
#
# /etc/fstab
# Created by anaconda on Wed Apr 8 16:52:13 2026
#
# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
#
# After editing this file, run 'systemctl daemon-reload' to update systemd
If you write LaTeX in Neovim using the LazyVim distribution, you already get a fantastic editing experience thanks to VimTeX and texlab. But there’s one thing that often trips people up: how do you quickly wrap selected text in \emph{…}, \textbf{…}, or other LaTeX commands?
The answer is mini.surround — and with a small buffer-local configuration, it becomes a LaTeX wrapping powerhouse.
What mini.surround Gives You (Out of the Box)
LazyVim ships with the mini.surround extra, which you can enable in your lazy.lua (or wherever you manage extras):
For example, visual-select a word and press gsa" to wrap it in double quotes, or gsa) for parentheses. Great — but what about LaTeX-specific commands?
The Problem with LaTeX Commands
VimTeX gives you text objects, motions, completion, and much more — but it doesn’t ship a built-in “surround this selection with \emph{…}” feature. That’s where mini.surround‘s custom surroundings come in.
We can teach mini.surround to understand LaTeX commands by defining custom surroundings in a filetype-specific Lua file.
Setting Up Custom LaTeX Surroundings
Create (or edit) the file ~/.config/nvim/after/ftplugin/tex.lua. This file is automatically sourced by Neovim whenever you open a .tex file.
Why ftplugin? Files in this directory are loaded after all plugin configuration, and only for the matching filetype. This means our settings are buffer-local and don’t interfere with any other filetype.
How It Works
The cmd_surround helper function does two things:
input: A Lua pattern that matches \cmd{...} so that gsd and gsr know what to find and remove.
output: The left (\cmd{) and right (}) strings used when adding a surrounding.
We then assign short, memorable single-character IDs to each command.
Using the Surroundings
Here’s what you can do in any .tex buffer:
Adding a Surrounding
Visual-select some text, then:
Keymap
Result
gsae
\emph{selected text}
gsab
\textbf{selected text}
gsai
\textit{selected text}
gsat
\texttt{selected text}
gsau
\underline{selected text}
gsas
\textsc{selected text}
gsaq
(add opening and closing LaTeX quotations, i.e., two backticks and ”)
You can also use gsa in normal mode with a motion: gsaeiw wraps the current word in \emph{…}.
Deleting a Surrounding
Place your cursor inside a command and press:
Keymap
Action
gsde
Remove \emph{…}, leaving the inner text
gsdb
Remove \textbf{…}
gsdi
Remove \textit{…}
Replacing a Surrounding
Want to change \emph{…} to \textbf{…}? No problem:
1
2
gsreb
That’s gsr (replace surrounding), then e (find \emph{…}), then b (replace with \textbf{…}).
A Practical Example
Suppose you have this text:
1
This is very important information.
You visual-select “very important” and press gsab. You get:
TeX
1
Thisis\textbf{veryimportant}information.
Now you decide that emphasis is better. With the cursor inside the \textbf{…}, press gsrbe:
TeX
1
Thisis\emph{veryimportant}information.
And if you want to remove the markup entirely: gsde:
TeX
1
Thisisveryimportantinformation.
Extending with Your Own Commands
The pattern is easy to extend. Just add more entries to cfg.custom_surroundings:
Lua
1
2
3
4
5
6
7
8
-- enquote (csquotes package)
cfg.custom_surroundings.Q=cmd_surround("enquote")
-- colored text
cfg.custom_surroundings.r={
input={"\\textcolor{[^}]*}{().-()}"},
output={left="\\textcolor{red}{",right="}"},
}
Pick single characters that are easy to remember and don’t conflict with built-in mini.surround ids (avoid (, ), [, ], {, }, ', ", `).
A Note on Limitations
The input patterns use Lua’s pattern matching, which cannot handle nested braces. For example, \emph{foo {bar} baz} may not be matched correctly by gsd or gsr. For the vast majority of LaTeX writing, this is not an issue — but it’s worth keeping in mind if you work heavily with nested commands.
If you use a Wayland compositor like Sway or Hyprland, mako is a great lightweight notification daemon. Many distros make it “start automatically” via a systemd user service, so you don’t need to add exec mako to your compositor config.
I’m experimenting with a setup with KDE, Sway, and Hyprland installed on the same machine and used by the same user, and there’s a catch: KDE on Wayland is also a Wayland session, so the same auto-start logic can kick in there too — and you end up with mako running in Plasma when you don’t want it.
This post shows a clean fix: keep mako auto-starting in Sway/Hyprland, but skip it in KDE, using a systemd drop-in override that lives nicely in your dotfiles (e.g., with chezmoi).
Why mako starts in KDE
On many systems, the provided user unit checks only whether you’re in a Wayland session:
1
2
ExecCondition=/bin/sh-c[-n"$WAYLAND_DISPLAY"]
KDE Wayland sets WAYLAND_DISPLAY too, so mako passes the check and runs.
The fix: restrict the systemd unit to Sway/Hyprland
On another machine, once mako is installed, that drop-in will be picked up automatically. The only thing to remember is that systemd reads unit changes after a reload (or next login), so if you want it to take effect immediately after chezmoi apply, run:
1
2
3
systemctl--user daemon-reload
systemctl--user try-restart mako.service
(try-restart is nice because it won’t error if mako isn’t running.)
Bonus note: D-Bus activation
Depending on your distro and setup, mako can also be started via D-Bus when a request for notifications is made. If you find mako still appears in KDE even when the unit is skipped, check whether it’s being D-Bus activated — and then either:
– rely on a KDE-native notification daemon in Plasma, or
– mask the D-Bus service for org.freedesktop.Notifications for KDE sessions only.
If you use KDE and Sway (or Hyprland) on the same machine with the same user (something I’m experimenting with), when you launch Chrome and log in with your user in KDE, and then switch to Sway, you’ll see that your account is marked as “Paused”: you have to log in again. The same holds the other way round.
That’s because in KDE, Chrome stores the credentials in KWallet, while Sway does not.
To fix this annoying problem, you have to ensure to run Chrome in Sway with the option “–password-store=kwallet6”.
To do that, you can either manually launch Chrome with that option or create the file “~/.local/share/applications/google-chrome.desktop” starting from the default file (in Arch it’s “/usr/share/applications/google-chrome.desktop”) and ensure the occurrences of the “Exec” line have that option, i.e.,
The desktop file in your home folder will have precedence over the default one.
However, while KDE automatically unlocks the KWallet when you log in, Sway does not. The first time you launch Chrome from a Sway session, you’re asked to open the wallet with your login password:
To let KWallet use your login credentials automatically (as KDE does, thanks to “/etc/xdg/autostart/pam_kwallet_init.desktop”), you need to tell Sway to start the corresponding PAM module.
That’s usually enough to avoid the post-login password prompt, provided PAM is already set up correctly (see the next section).
Important: pam_kwallet_init only works if PAM captured your password
pam_kwallet_init does not magically know your login password. It relies on the PAM module (pam_kwallet5.so) having captured it during login and made it available for the session handoff.
So:
If you start Sway via a display manager/session that already has pam_kwallet5.so in its PAM stack, then running /usr/lib/pam_kwallet_init In Sway, the wallet should unlock automatically.
If you start Sway via a path that doesn’t run the kwallet PAM module (common when starting from TTY, or via some greeters, depending on config), then pam_kwallet_init won’t have credentials to use, and you’ll still be prompted.
I’ll show how I installed Linux (EndeavourOS, i.e., Arch) on a Dell Pro Max Tower T2.
This is quite a powerful computer! Here are a few screenshots taken from Windows 11:
Before installing Linux on this computer, I had to change the SSD SATA mode from RAID to AHCI, as documented in a previous blog post. Otherwise, Linux will not detect any SSD.
Prepare the disk with “Disk Management”.
Current situation:
I will not wipe the whole disk because I want to use Windows as well. I won’t touch the other recovery and health partitions either.
Right-click on “C:” and choose “Shrink Volume…”. About 200Gb should be enough for Windows on this computer. Unfortunately, the UI of this dialog is not the best one: you have to compute how much space to remove from the current partition and check whether the “Total size after shrink” is what you want.
I’m also deleting the “D:” volume (I’ll use it for additional partitions both on Linux and maybe on Windows).
Here’s the final result:
Let’s reboot the computer and turn off secure boot. Press F2 when the computer is turning on to enter the BIOS.
Very nice looking:
NOTE: You can use the mouse to navigate the BIOS. In my case, the computer is connected to a KVM switch for keyboard and mouse. When in the BIOS, the mouse just moves vertically. I had to plug the mouse directly into the USB port of the computer to use it inside the BIOS.
Select “Boot Configuration” where you see the “Boot Sequence” (that’s useful in the future to change the boot order or delete old entries). Scroll down til you get to secure boot and disable it:
Let’s apply changes and exit.
I downloaded the EndeavourOS ISO Mercury Neo 2025.03.19, put it into a Ventoy USB stick.
It looks like this PC can boot from USB only from the first port from the bottom (at least in the front: I haven’t tried the ports on the back):
When the computer starts, press F12 for the temporary boot menu. Select the USB stick. Then you get the Ventoy menu where you select the EndeavourOS distribution. I’ll use grub2 mode.
A small blog post on how to install EndeavourOS with LUKS disk encryption.
The installation starts and proceeds as usual (see, e.g., my older post). I’m using KDE for this installation.
When you get to disk partitioning, choose manual:
If you start from a fresh disk, create a new partition table and choose GPT:
If you start from a fresh disk, first create the partition for EFI:
In the rest of free space, I personally prefer to have a swap partition:
But I prefer not to encrypt that (to avoid being asked for the decryption password twice; at least, that’s what I guess… I’ll experiment that in the future):
Then, the partition for the actual system (in this case, I’m not using the whole disk); here, you specify the filesystem (I prefer BTRFS) and check “Encrypt”; you’ll be asked for the encryption password (of course, choose a strong one and ensure you remember that password):
Here’s the final layout:
Now, proceed as usual.
When you reboot, before getting to GRUB, you’ll be asked for the encryption password:
Once inserted, wait for the system to verify that:
And then, you finally get to GRUB as usual.
In fact, I haven’t created a separate partition for “/boot” (which would be a bad idea if you want to use BTRFS snapshots); thus, the grub configuration is in the encrypted file system, and when EFI boots, it needs the encryption password right away.
When you log in, you should see the directory layout with LUKS encryption:
I love LaTeX, but I don’t love LaTeX noise. If you use LazyVim’s lang.tex extra, you’ve probably seen a familiar friend pop up in your editor diagnostics: Underfull \hbox. It’s usually harmless, but it’s distracting—especially when you’re trying to focus on content. In this post, I’ll show two clean ways to silence those warnings: one for VimTeX’s quickfix and one for TexLab’s LSP diagnostics, without turning off the good stuff.
In LazyVim’s Tex extra, those Underfull \hbox … messages are coming from VimTeX’s Quickfix parsing (it’s enabled by default in the extra). VimTeX lets you hide specific warnings by regex-matching them with g:vimtex_quickfix_ignore_filters.
Create a custom plugin specification, e.g., “~/.config/nvim/lua/plugins/extend-vimtex.lua”, with these contents:
Lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
return{
{
"lervag/vimtex",
-- use `init` for vim.g.* so it’s set before VimTeX uses it
init=function()
-- These are Vim regexes. Match the log line(s) you want to hide.
-- [[Overfull \\hbox]], -- (optional) hide overfull hbox warnings too
-- [[Underfull \\vbox]], -- (optional) hide underfull vbox warnings too
}
end,
},
}
Restart Neovim, and the quickfix list will no longer contain those warnings.
However, you’re still seeing “Underfull \hbox” as LSP diagnostics (not quickfix), that’s coming from TexLab (separate configuration):
You need to create another custom plugin specification for customizing the TexLab LSP, e.g., “~/.config/nvim/lua/plugins/extend-lspconfig.lua”:
Lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
return{
{
"neovim/nvim-lspconfig",
opts={
servers={
texlab={
settings={
texlab={
diagnostics={
ignoredPatterns={
-- Rust regexes. Match the diagnostic text you want to suppress.
[[Underfull \\hbox]],
-- If you want to be extra broad:
-- [[Underfull \\hbox.*]],
},
},
},
},
},
},
},
},
}
Restart, and also the editor’s inline warnings of that space will be gone.
If you don’t like that deep specification, in LazyVim, you don’t have to restate the whole opts = { servers = { texlab = { … }}} block. You can patch the existing LSP config in a tiny opts = function(_, opts) ... end snippet (LazyVim shows this pattern for extending defaults, and also documents the opts.setup hook for server setup).
The final result of this series of tutorials can be found here: https://github.com/LorenzoBettini/lazyvim-tex. Branches will denote the state of the repository for a specific blog post section.
Stay tuned for more blog posts on LaTeX and Neovim! 🙂
How to prevent JDT LS (via m2e) from adding generated-sources APT folders and org.eclipse.jdt.apt prefs to an Eclipse+Maven project in VS Code.
If you open a Maven Java project in Visual Studio Code that also contains Eclipse project metadata (.project, .classpath, .settings/…), you might notice that VS Code’s Java tooling (JDT Language Server) “helpfully” edits your Eclipse files.
In particular, it may keep re-inserting entries like these into your .classpath:
VS Code Java support is powered by Eclipse JDT Language Server (JDT LS). When it detects Eclipse metadata (.project / .classpath), it will often use Eclipse-style project configuration and keep it synchronized.
For Maven projects, JDT LS relies on m2e (the “Maven integration for Eclipse”), and in many setups m2e-apt is present as well. m2e-apt is the component that manages annotation processing (APT) integration and, as part of that, it adds the standard “generated sources” folders into .classpath.
I find that very annoying!
If your project doesn’t use annotation processing and you don’t want these Eclipse files constantly modified, and you remove the entries, Visual Studio Code will re-add them when you open the projects in Visual Studio Code. If you open the projects from Eclipse and you “update” the Maven projects, Eclipse will remove the entries… and so on and so forth!
Put it under the regular Maven <properties> section:
XHTML
1
2
3
4
5
6
<project>
...
<properties>
<m2e.apt.activation>disabled</m2e.apt.activation>
</properties>
</project>
That’s it. After this, m2e-apt will stop treating your project as something it should manage, and VS Code/JDT LS will no longer keep reintroducing those APT-related .classpath entries.
Note: The documentation mentions a “settings section” in the POM. There is nosettings element in pom.xml; Maven “settings” live in ~/.m2/settings.xml. In the POM, this is implemented via a property, so properties (or a profile’s properties) is the right place.
Refresh VS Code so it stops regenerating the files
After editing the POM, VS Code may still have cached the project configuration. Do this once:
Open the Command Palette
Run: Java: Clean Java Language Server Workspace
Let the Java server restart and re-import the project
Then you can delete the unwanted entries/files one last time:
Remove the APT-related classpathentry … m2e-apt … blocks from .classpath
Delete .settings/org.eclipse.jdt.apt.core.prefs if you don’t want it around
They should not come back.
If you only want to disable it in certain environments, you can place the property in a Maven profile or in the pom file of a single project.
Remembering every Sway shortcut is tough. I wrote a small script that parses your Sway config, displays all bindsym shortcuts in a clean, searchable list via Rofi, and executes the command associated with whichever one you select.
It’s fast, keyboard-friendly, and great for discovery: “What did I bind to Mod + Shift + P again?” Now you can search, see, and execute it.
What the script does
Reads your Sway config from $XDG_CONFIG_HOME/sway/config (or ~/.config/sway/config)
Finds all bindsym … lines
Formats each entry nicely, e.g.
Mod + Return → exec alacritty
Shows the list in a wide Rofi dmenu
When you select an entry, it executes the associated command through swaymsg
Dependencies
sway (for swaymsg)
rofi
awk, sed, grep (standard on most distros)
notify-send (optional – shows an error if the config isn’t found)
The script
Save this as ~/.local/bin/rofi-sway-keybindings.sh and make it executable.
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env bash
# This script lists Sway keybindings from the Sway config file and allows the user to select one via rofi dmenu.
# When a keybinding is selected, it executes the associated command.
sed 's/^\s*bindsym\s*//' strips the leading bindsym
awk splits the line into (and does some cleanup):
– keys: the first token (e.g. $mod+Return)
– cmd: the rest of the line (e.g. exec alacritty)
It also strips trailing inline comments (after #) and skips bindsym flags like --release or --locked before reading the key. Finally, it prettifies modifiers and prints a fixed-width column so the arrows line up.
Rofi presents that list with -dmenu. When you pick one, the script extracts the command part (after →) and sends it to swaymsg. That means anything you can put after bindsym (like exec …, workspace …, kill, etc.) will run on demand.
Usage
Run the script from a terminal: rofi-sway-keybindings.sh
If you use the LaTeX listings package to typeset Java, you’ve probably noticed that modern Java has moved faster than the package itself. Records, var, and text blocks may not highlight correctly out of the box. The good news: the listings package is extensible so that you can teach it “modern Java” with a tiny language definition.
The minimal language extension for Java 17
Here’s a drop‑in snippet that builds on the stock Java lexer to support key Java 17 features:
TeX
1
2
3
4
5
6
\lstdefinelanguage{Java17}{
language=Java,
morekeywords={var,record},
deletekeywords={label},
morestring=[b]"""
}
What each line does:
language = Java: inherit all of the listings’ built‑in Java rules.
morekeywords = {var,record}: colorize var and record as keywords (var is contextual, but highlighting it improves readability in code listings).
deletekeywords = {label}: avoid mistakenly highlighting labeled statements like label: for (…) { … }. label is not a Java keyword; removing it prevents false positives.
morestring=[b]”””: treat triple quotes as a balanced string delimiter so Java text blocks highlight as a single string.
Using it in your document
Activate the language globally:
TeX
1
\lstset{language=Java17}
…or per listing:
TeX
1
2
3
\begin{lstlisting}[language=Java17]
//codehere
\end{lstlisting}
If you already have a custom style (e.g., mystyle) with colors and fonts, combine them:
TeX
1
\lstset{language=Java17,style=mystyle}
Minimal working example
This is a compact MWE you can compile with pdflatex, xelatex, or lualatex. Adjust the style to your taste:
TeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}% not needed on XeLaTeX/LuaLaTeX
\usepackage{xcolor}
\usepackage{listings}
%Simple,readabledefaultstyle
\lstdefinestyle{mystyle}{
flexiblecolumns=true,
keepspaces=true,
tabsize=4,
showstringspaces=false,
basewidth={0em,0em},,
numbers=left,
basicstyle=\scriptsize,
commentstyle=\itshape,
stringstyle=\ttfamily,
}
%ModernJavalanguageextension
\lstdefinelanguage{Java17}{
language=Java,
morekeywords={var,record},
deletekeywords={label},
morestring=[b]"""
}
\begin{document}
\section*{Demo}
Withthedefault\texttt{Java}languagedefinition:
\begin{lstlisting}[language=Java,style=mystyle]
recordPoint(intx,inty){}
classDemo{
publicstaticvoidmain(String[]args){
varmsg="""
Hello,
Java17"textblocks!"
""";
System.out.println(msg);
label:for(inti=0;i<1;i++){
breaklabel;//'label'isnothighlightedasakeyword
}
}
}
\end{lstlisting}
\noindent
Withthe\texttt{Java17}languagedefinition:
\begin{lstlisting}[language=Java17,style=mystyle]
recordPoint(intx,inty){}
classDemo{
publicstaticvoidmain(String[]args){
varmsg="""
Hello,
Java17"textblocks!"
""";
System.out.println(msg);
label:for(inti=0;i<1;i++){
breaklabel;//'label'isnothighlightedasakeyword
}
}
}
\end{lstlisting}
\end{document}
Here’s the result, where you can see the differences (note in the standard behavior the wrong highlighting of the double-quoted string in the text-block):
Happy highlighting! 🙂
We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.
Cookie
Duration
Description
cookielawinfo-checkbox-analytics
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics".
cookielawinfo-checkbox-functional
11 months
The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional".
cookielawinfo-checkbox-necessary
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary".
cookielawinfo-checkbox-others
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other.
cookielawinfo-checkbox-performance
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance".
viewed_cookie_policy
11 months
The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data.
Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.
Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.
Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.