Archlinux User Softwares

Archlinux user softwares. Common and useful tools and apps for a personal machine.

Swaylock (swaylock-effects)

swaylock is a wayland screen locker. The original project is current unmaintained. Multiple fork projects exist, swaylock-effects is one of them.

The original swaylock project is not actively maintained. The community maintains some forks of the original projects. They inherit a former project, one by one, basically.

To install the fork by jirutka, run yay -S swaylock-effects-git. Note that there are other forks that you could try, but I haven’t used them, so use at your own risk.

Don’t try to kill swaylock on another tty, this would result in a red screen. If you really want to unlock swaylock, send pkill -USR1 swaylock in other tty. See: https://github.com/swaywm/sway/issues/7046

Configure swaylock-effects in .swaylock/config. I posted my config below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
screenshots
clock
indicator
ignore-empty-password
grace-no-mouse
grace-no-touch

indicator-radius=100
indicator-thickness=7
effect-blur=7x5
effect-vignette=0.5:0.5
ring-color=ffa6c2
key-hl-color=5c5f77
line-color=00000000
inside-color=00000088
separator-color=00000000
grace=0
fade-in=0.2
  • The grace attribute defines a period during which a keyboard/mouse event will cancel the lock process
  • The grace-no-mouse and grace-no-touch attributes remove mouse and touchpad events from those can cancel the lock
  • The ring-color and key-hl-color attributes customize the appearance of the ring showing in the lock state

Neovide - a smooth neovim GUI client

If you want fancier animation in neovim, and you don’t have a preference with work with everything inside terminal, you could try Neovide.

On Debian based distros, do NOT use the snap package. It has a lot of problems and the developers said they would probably discontinue it later

You could start neovide from the command line, or through an app launcher like rofi. For launchers, they look for a .desktop entry file in some certain locations that matches the request. The entry file might not set environment variables correctly, as some neovim plugins depend on.

For example, some neovim plugin requires a correct python interpreter to work. And starting Neovide from launcher won’t set $PATH correctly. The best solution is to cook up a shell script and launch neovide there, like this

1
env PATH="$HOME/anaconda3/bin:$PATH" neovide

Create a new desktop entry under ~/.local/share/applications/

1
2
3
4
5
6
7
8
9
[Desktop Entry]
Type=Application
Exec=/home/martin/shell/neovide.sh
Icon=neovide
Name=Neovide
Keywords=Text;Editor;
Categories=Utility;TextEditor;
Comment=No Nonsense Neovim Client in Rust
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;

The entry is put under user directory because even if you editted the the default entry file that comes with Neovide, on the next system upgrade like pacman -Syu, it will be overwritten and your change is gone.

Fcitx5 (chinese IME pinyin)

Install packages pacman -S fcitx5-im fcitx5-chinese-addons gtk4

Set environment variable

1
2
3
4
# /etc/environment
GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx

For fcitx5 to work on Chromium based browsers (e.g. Edge), add --gtk-version=4 to launch parameters. You also need to install gtk4 if you have not.

If you have this problem: fcitx5 pinyin input window looks blurry in some apps such as Alacritty and Chromium-based browser, disable ‘enable fractional scaling on wayland’ in fcitx5-configtool. However, even with gtk4 enabled, chromium core seems to still have issue displaying fcitx5 under wayland. The popup font is blurry (probably due to fractional scaling) and the popup position is misaligned. Using a more native alternative such as Firefox could be a solution.

Tmux + Alacrity + Neovim settings on ArchLinux

To enable tmux true colors so that neovim colorschemes are displayed properly:

.tmux.conf

1
set -ag terminal-overrides ",xterm-256color:RGB"

alacritty.toml

1
2
[env]
TERM = "xterm-256color"

Then restart tmux (could by kill all sessions and start tmux again) for config to load.

Note: this is the minal config (as least for me) to work. You might need other options in tmux configuration to get colors right.

Hyprland Obs Recording

Require xdg-desktop-portal-hyprland for obs to be able to use pipewire screen sharing

Docker

docker is available in the extra package, install with

pacman -S docker.

Start docker daemon with systemctl start docker, or enable it on boot with systemctl enable docker.

You have to use docker commands with root users, e.g. sudo docker pull, etc.

You can asign yourself to the docker group to remove sudo.

usermod -aG docker <username>, then newgrp docker to refresh the group priviledge.

Waybar

To correctly get the CPU temperature in waybar, check directory /sys/class/thermal/. /sys/class contains a bunch of “devices”, in the hardware sense, and exposes theme as if they were just files. Here, you can find thermal devices like temperature gauges, cooling devices like fans, keyboard backlights, etc. It your CPU is of x86 architecture, find the zone under which the type file is ‘x86_pkg_temp’, which means “x86 packaging temperature”. Set thermal-zone to it.

1
2
3
4
5
"temperature": {
"thermal-zone": 9,
"interval": 3,
"format": " {temperatureC}°C",
},

interval controls the polling interval.

Browser

For firefox browser, it is recommented to install an emoji font and a Chinese font for most pages to display properly, such as noto-fonts-cjk and noto-fonts-emoji.

Go to Settings, search for ‘Fonts’, configure fonts for ‘Latin’:

  1. Proportional: set to sans-serif
  2. Serif: a default font for ArchLinux will do, like Nimbus Roman
  3. Sans-serif: a default font will do, like Nimbus Sans
  4. Monospace: JetBrainsMono Nerd Font

For firefox, in the same window where you find the settings for variable font types (serif, sans-serif, etc.), in the very bottom there is a tickbox for “Allow pages to choose their own fonts, instead of your selections above”. Do NOT untick this, because if you do so, sites can’t display external fonts with css sheets. External fonts are those transfered on the internet rather than installed on your computer. You can browser some of those fonts at Google Fonts, where they give you a <link> tag to include in your site document if you wanna use the font.

Media Control (Spotify)

Bring music to your Arch Linux Hyprland!

First, install your favorite music player, e.g. Netease Cloud Music, Spotify, etc. This is of your choice, but remember to pick one that supports MPRIS (Media Player Remote Interfacing Specification). There is any standard media control unit in Linux, but relax, we’re gonna make one!

It turns out, the Python script here can control any MPRIS clients, not just music players! You might be surprised that playing video in browser can be controlled, too.

You get a list of MPRIS clients currently running by

1
2
3
$ playerctl -l
firefox.instance_1_138
spotify

Spotify is recommended as its support for Linux should be stronger. Both Spotify Free or Premium will do, but a premium subscription removes advertisement between songs. A Premium Family plan should take you no more than $160 RMB per year.

Install python-gobject (provides the import gi modules) and playerctl for MPRIS capabilities. gtk4 is also required, in principle. If you have installed other GUI apps before, all those could have been already installed.

1
pacman -S python-gobject playerctl

There’s a caveat here. In the following script, I put a shebang #!/usr/bin/env /usr/bin/python3 at the top, instead of leaving the exact interpreter running it to shell program, I want the system python (the one installed by pacman) to be used to execute the script. If you take a look at the package content of playerctl, you’ll find that the Python library is installed under the system Python’s site-package. The gi library doesn’t have a pip distribution. The official document recommends installation via the python-gobject package, too. If you have an Anaconda Python or some other Python in nonstandard locations, you’d probably want to set up like this.

Python script to control

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/usr/bin/env /usr/bin/python3
import gi
gi.require_version("Playerctl", "2.0")
from gi.repository import Playerctl, GLib
from gi.repository.Playerctl import Player
import argparse
import logging
import sys
import signal
import json
import os
from typing import List

logger = logging.getLogger(__name__)

def signal_handler(sig, frame):
logger.info("Received signal to stop, exiting")
sys.stdout.write("\n")
sys.stdout.flush()
# loop.quit()
sys.exit(0)


class PlayerManager:
def __init__(self, selected_player=None):
self.manager = Playerctl.PlayerManager()
self.loop = GLib.MainLoop()
self.manager.connect(
"name-appeared", lambda *args: self.on_player_appeared(*args))
self.manager.connect(
"player-vanished", lambda *args: self.on_player_vanished(*args))

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
self.selected_player = selected_player

self.init_players()

def init_players(self):
for player in self.manager.props.player_names:
if self.selected_player is not None and self.selected_player != player.name:
logger.debug(f"{player.name} is not the filtered player, skipping it")
continue
self.init_player(player)

def run(self):
logger.info("Starting main loop")
self.loop.run()

def init_player(self, player):
logger.info(f"Initialize new player: {player.name}")
player = Playerctl.Player.new_from_name(player)
player.connect("playback-status",
self.on_playback_status_changed, None)
player.connect("metadata", self.on_metadata_changed, None)
self.manager.manage_player(player)
self.on_metadata_changed(player, player.props.metadata)

def get_players(self) -> List[Player]:
return self.manager.props.players

def write_output(self, text, player):
logger.debug(f"Writing output: {text}")

output = {"text": text,
"class": "custom-" + player.props.player_name,
"alt": player.props.player_name}

sys.stdout.write(json.dumps(output) + "\n")
sys.stdout.flush()

def clear_output(self):
sys.stdout.write("\n")
sys.stdout.flush()

def on_playback_status_changed(self, player, status, _=None):
logger.debug(f"Playback status changed for player {player.props.player_name}: {status}")
self.on_metadata_changed(player, player.props.metadata)

def get_first_playing_player(self):
players = self.get_players()
logger.debug(f"Getting first playing player from {len(players)} players")
if len(players) > 0:
# if any are playing, show the first one that is playing
# reverse order, so that the most recently added ones are preferred
for player in players[::-1]:
if player.props.status == "Playing":
return player
# if none are playing, show the first one
return players[0]
else:
logger.debug("No players found")
return None

def show_most_important_player(self):
logger.debug("Showing most important player")
# show the currently playing player
# or else show the first paused player
# or else show nothing
current_player = self.get_first_playing_player()
if current_player is not None:
self.on_metadata_changed(current_player, current_player.props.metadata)
else:
self.clear_output()

def on_metadata_changed(self, player, metadata, _=None):
logger.debug(f"Metadata changed for player {player.props.player_name}")
player_name = player.props.player_name
artist = player.get_artist()
title = player.get_title()

track_info = ""
if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]:
track_info = "Advertisement"
elif artist is not None and title is not None:
track_info = f"{title} ({artist})"
else:
track_info = title

if track_info:
if player.props.status == "Playing":
track_info = " " + track_info
else:
track_info = " " + track_info
# only print output if no other player is playing
current_playing = self.get_first_playing_player()
if current_playing is None or current_playing.props.player_name == player.props.player_name:
self.write_output(track_info, player)
else:
logger.debug(f"Other player {current_playing.props.player_name} is playing, skipping")

def on_player_appeared(self, _, player):
logger.info(f"Player has appeared: {player.name}")
if player is not None and (self.selected_player is None or player.name == self.selected_player):
self.init_player(player)
else:
logger.debug(
"New player appeared, but it's not the selected player, skipping")

def on_player_vanished(self, _, player):
logger.info(f"Player {player.props.player_name} has vanished")
self.show_most_important_player()

def parse_arguments():
parser = argparse.ArgumentParser()

# Increase verbosity with every occurrence of -v
parser.add_argument("-v", "--verbose", action="count", default=0)

# Define for which player we"re listening
parser.add_argument("--player")

parser.add_argument("--enable-logging", action="store_true")

return parser.parse_args()


def main():
arguments = parse_arguments()

# Initialize logging
if arguments.enable_logging:
logfile = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"media-player.log",
)
logging.basicConfig(filename=logfile, level=logging.DEBUG,
format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s")

# Logging is set by default to WARN and higher.
# With every occurrence of -v it's lowered by one
logger.setLevel(max((3 - arguments.verbose) * 10, 0))

logger.info("Creating player manager")
if arguments.player:
logger.info(f"Filtering for player: {arguments.player}")
player = PlayerManager(arguments.player)
player.run()


if __name__ == "__main__":
main()

Put the script somewhere under your home directory, ~/.config/waybar/ for example. Setup the module in waybar config

1
2
3
4
5
6
7
8
9
10
11
12
13
"custom/media": {
"format": "{icon} {}",
"return-type": "json",
"on-click": "playerctl play-pause --player spotify",
"on-click-right": "playerctl next --player spotify",
"on-click-middle": "playerctl previous --player spotify",
"max-length": 40,
"format-icons": {
"spotify": "",
"default": ""
},
"exec": "$HOME/.config/waybar/custom_modules/mediaplayer.py 2> /dev/null"
}

Remember to chmod the script so you could execute it as if it was a binary:
$HOME/.config/waybar/custom_modules/mediaplayer.py 2> /dev/null

TMUX

TMUX is a well-known Linux terminal session manager. It’s helpful in the way that you can preserve terminal sessions even if you quited the terminal. Most people running long caculation would want that rather than having the entire job lost at logout.

The default tmux configuration isn’t very sensitive and friendly for starters. Some tweaks and plugins are recommended to improve it.

Install TPM (TMUX Plugin Manager)

Clone TPM repo

git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

Put the following at the bottom of ~/.tmux.conf

1
2
3
4
5
6
# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'

# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'

Source the file or restart tmux (by stopping all sessions). Press Prefix + I to install plugins.

Add the following line before the run line to install Catppuccin theme

1
2
3
# Catppuccin theme
set -g @plugin 'catppuccin/tmux'
set -g @catppuccin_flavour 'frappe'

This assumes your tmux config file is ~/.tmux.conf. However somebody’s could be at ~/.config/tmux/tmux.conf, due to XDG_CONFIG_HOME convention. Depending on where your config file is located, the plugins are installed to different directories: ~/.tmux/plugins/ and ~/.config/tmux/plugins/, respectively.

Wechat

Install AUR package

yay -S wechat-universal-bwrap

Remember to configure /etc/resolv.conf properly as the package binds that directory in the sandbox. Wechat will read from that directly for DNS. Otherwise, you might not be able to login via scanning the QR code.

According to wiki, the recommended way is to set /etc/resolv.conf as a soft link to /run/systemd/resolve/stub-resolv.conf. This propagates the systemd-resolved managed configuration to all clients. stub-resolv.conf contains the local stub 127.0.0.53 as the only DNS server.

1
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

File Manager: Dolphin

A recent update made it depend on qt6-base. Configuring icon theme by qt5ct no longer works. Use qt6ct instead.

Wallpaper Daemon: swww

Install swww from AUR: yay -S swww.

Start the daemon by swww-daemon.

Set a wallpaper by swww img <path>

swww supports GIFs and images of many formats. It supports configurable smooth transition effects while changing wallpaper.