Some people hate it, some enjoy it… setting up a new machine, and a new operating system. Generally speaking, the majority of the people who enjoy this process are probably Linux users to which I count myself. There is even a word for the process of repeatedly setting up new machines in the Linux community… “distro-hopping”. After a while hopping from distro to distro though, I realized that the importance of distros is somewhat exaggerated in the community. One of the many advantages of Linux (at least if you are familiar with it) is the configurability and freedom to do whatever you want with your machine. This means you can make any distro look and feel like any other. In this post, I explain how I configured my Arch Linux machine, and control my desktop environment with XMonad.
So, before I continue, I guess this is a good point to put a
disclaimer… this post contains reports of my personal experiences
and opinions, and as such is probably disagreeable to some. If you
feel that way, simply stop reading please. I have tried, used, and
worked with Microsoft Windows, OSX/macOS, and Linux in the past. Given
a choice today, I would choose Linux everytime, as it is simply the
operating system in which I feel most comfortable.
I built my current computer myself from individual components and like configuring and tinkering with it (on hardware and software), which is why I settled on the Arch Linux distribution. However, as mentioned before, it was a long road to this point and is still a constant adjustment to current likes, dislikes, and needs.
My Linux journey began on a late afternoon when I asked my dad about what operating systems were, and about alternatives to Windows 95 which was installed on my (now ancient) IBM ThinkPad 770 handed down by him. And before you say anything… yes, I know how lucky I was to receive a proper computer at that time and age! Being a trained electrical engineer, IBM researcher, and a general tech enthusiast, he proudly explained to a (at that time) 9 year old boy about Linux. He spent probably hours teaching me all about computers, from hardware all the way down to software, and as if entranced, I listened to every word… until mom annoyedly called to dinner for the forth time. That day, a common hobby of ours started.
The next day, my dad came back from work and gave me a CD. On it, was a bootable live system of Knoppix installed, a Linux distro designed for just such a purpose. We opened the laptop’s CD drive, inserted the disc and booted up without any problems. From that time onwards, I would use Linux intermittently for school, or to simply explore the system.
During my teenager years, Microsoft practically monopolized computer gaming, and consequently made multi-booting essential to me. Microsoft always stayed installed for the purpose of gaming on my then computer. As for many others starting their Linux adventures, my next distribution of Linux was Ubuntu, then openSUSE, Linux Mint, back to Ubuntu, KDE neon, afterwards Debian for quite a long while. At university, I finally had my first “official” programming course for which the recommended OS was openSUSE (in school we only had limited contact with computers). From that point on, Linux had become the most important OS for my studies, and later also for work as a pre-doctoral researcher. I was granted access to the universities own supercomputing cluster zBox4 running Red Hat Linux-based Scientific Linux on which I spent countless hours writing, running, and debugging code.
A few years ago, Pop!OS emerged as a very user-friendly distribution, which finally made me switch to a full-fledged Linux-only user. It ships with a gorgeous, self-maintained, Rust-based, GNOME-like desktop (probably the right decision if you know what I mean). Pop!OS finally proved to me that gaming was now (with only a little more hardship) completely possible, even if this was less important to me than during my teenager years. It also introduced me to the concept of window tiling… which connects to the second part of this post’s title.
After Pop!OS, I wanted a slightly more bare-bones distribution, and a desktop environment which is less resource-intense when idle. My goal was to fully customize everything about my machine, and have everything set up just as I want it. It came down to two options: Debian or Arch. Since I’ve already tried Debian, and heard only good things about the rolling release scheme, I picked Arch.
Many people are reluctant to give Arch a try, because the installation process is not exactly a few mouse clicks away from completion. In fact, Arch is typically installed from the tty, and most things which seem to be happening automatically in the background for other distributions, have to be done manually for Arch. Of course, for some this seems tedious, but for me it means full control over everything, and it teaches us about the inner workings of the Linux system.
In the following section, I describe how I set up Arch on my machines,
step by step, command after command. For the most parts, I tried to
stick to the command-line and avoid opening a text-editor, but if
shell scripting confuses you and you prefer working in vim (or another
text-editor of your choice), skip the echo >>
and sed -i
commands
and simply open the file instead.
Before you follow my guide on how to install Arch, remember that it has a rolling release scheme and thus, is potentially subject to changes. Always check the official installation guide on the arch wiki for updates, when in doubt, or for troubleshooting.
First, download an iso with the latest version of Arch
cd ~/Downloads arch_mirror="mirror.rackspace.com" # change depending on your location wget -c "${arch_mirror}/archlinux/iso/latest/md5sums.txt" sed -i '/archlinux-bootstrap/d' md5sums.txt read md5sum arch_iso <<< $(cat md5sums.txt) wget -c "${arch_mirror}/archlinux/iso/latest/${arch_iso}" md5sum -c md5sums.txt
Throughout this guide, I’ll use /dev/sde
as the USB device file,
and /dev/sda
as the hard drive where we want to install
Arch. Adjust according to your hardware, for example on modern
machines you might want to install on NVMe SSDs which are
typically at /dev/nvme0n1
, /dev/nvme1n1
, etc.
It is easiest to install Arch from a bootable medium. On Linux, the creation of such a medium, e.g. a USB stick, is straight-forward.
Insert your USB stick, but do not mount it (or if it auto-mounts, unmount it before proceeding).
cat ~/Downloads/archlinux-2022.02.01-x86_64.iso > /dev/sde
Once, the iso is copied onto the flash drive, reboot the machine, and select the flash drive in your boot menu. You should then be greeted with a short informational message in the shell.
If you use and choose a non-US keyboard layout, you may load a different one before we go on. E.g., for the Swiss keyboard layout
localectl list-keymaps | grep CH loadkeys de_CH-latin1
For your eyesight, it might also be advantageous to use a bigger
font (ter-132n
is best suited for Hi-DPI screens, if this turns
out to be too big, choose a smaller font size such as ter-128b
,
ter-124b
, ter-120b
, ter-114b
etc.)
setfont ter-132b
First, we check whether we are booted in EFI mode (which most modern machines should do on default). If the following command creates any output, you are indeed booted in EFI mode. If not, you may have to change some settings in your BIOS or consult the Arch wiki for a CMS-mode installation guide.
ls /sys/firmware/efi/efivars
Then, we test for an internet connection. To make your life easy, I recommend an ethernet connection, rather than wifi.
ping -c 3 archlinux.org
If the ping doesn’t work, inspect your network devices
ip -c link
and if necessary activate the device of your choice.
Once, the internet connection has been verified, ensure the system clock is accurate
timedatectl set-ntp true timedatectl status
and update your package manager’s mirrorlist for optimal download speeds (change specifics for your locale)
pacman -Syy reflector -c Switzerland -a 6 --protocol https --sort rate --save /etc/pacman.d/mirrorlist pacman -Syy
Here, you have to make several decisions:
/dev/nvme0n1
, /dev/nvme1n1
…/dev/sda
, /dev/sdb
…/
and an EFI system partition /efi
are minimally
required/home
partition (or
subvolume), in order to easily reinstall or restore Arch without
any data loss. (Note: it is still possible without a separate
home partition, but probably comes with headaches.)swap
partition is always a good idea, even if you have
enough RAM, say 64GB ;). The linux kernel moves memory pages
that are hardly ever used to swap space to ensure that enough
RAM is available for more frequently used ones. Alternatively,
you can use swap on zram (see the zramd package on the AUR), if
you prefer to save disk space.ext4
btrfs
. It has
advanced features such as Copy-on-Write, self-healing, device
pooling, and nearly-instant snapshotting capabilities (this
is what I personally find most useful, especially for rolling
release distributions such as Arch)Note that for optimal long-term performance of older SSD and NVMe drives, it is recommended to manually “over-provision” (leave some free space, typically about 10%). Most drives these days come OP from the factory, which is the reason why the capacity of such drives is usually lower than advertised.
To people whose utmost priority is stability, I would probably recommend sticking with the “classic” partitioning scheme:
device | filesystem | mount point | size |
---|---|---|---|
sda1 | ESP (ef00) | {/mnt}/boot or {/mnt}/efi | +512M |
sda2 | swap (8200) | [SWAP] | +8G |
sda3 | linux (8300) | {mnt} | +64G |
sda4 | linux (8300) | {/mnt}/home | rest |
This example assumes a 1 TB drive, so adjust the size of the
partitions appropriately if your device differs. The ESP needs at
least 300M and swap at least 512M. I prefer /mnt/efi
as mount
point for the ESP, but this could create problems for some boot
managers which look in the /boot
directory. grub
can boot
from anywhere though (when configured correctly).
However, Arch while by no means “unstable”, is probably not the
first choice for people whose highest priority is stability
anyways. Hence, my personal recommendation is a less conventional
partitioning scheme using the btrfs
filesystem. For simplicity
and brevity, I skip drive encryption in this post altogether, but
I might post something in the future about booting from a fully
encrypted drive (or see my arch-install.org guide for a less
structured introduction to encryption).
My preferred partitioning scheme for a btrfs system is
device | filesystem | mount point | size |
---|---|---|---|
sda1 | EFI (ef00) | {/mnt}/efi | +512M |
sda2 | swap (8200) | [SWAP] | +8G |
sda3 | linux (8300) | {mnt} | rest |
Here, we don’t create a separate home partition, because we
generate individual btrfs subvolumes which can also be mounted
separately. With btrfs, we can also use device pooling to set up
RAID systems. Snapshotting programs such as timeshift
assume a
certain structure of btrfs
subvolumes. By creating incompatible
subvolumes, we can selectively exclude something from snapshots
such as temporary (and unimportant) data on mount-points /tmp
and /var
. If you rather prefer these included in the snapshots,
simply don’t add them as subvolumes.
btrfs subvolume |
---|
@ |
@home |
@var |
@tmp |
@snapshots |
To begin the paritioning of the drive, we use gdisk
gdisk /dev/sda # generate a GPT table > o # create a EFI partition > n, 1, <Enter>, +512M, ef00 # create swap partition > n, 2, <Enter>, +8G, 8200 # create root partition > n, 3, <Enter>, <Enter>, <Enter> (or 8300) # write scheme to disk and exit > w, Y
Confirm that the drive was correctly partitioned using
lsblk -o NAME,PATH,FSTYPE,FSSIZE,MOUNTPOINT
You should now see multiple paritions on /dev/sda
, e.g.
/dev/sda1
for the boot partition, /dev/sda2
for the swap
partition, and /dev/sda3
for the root partition. Once done,
format the partitions using
mkfs.fat -F 32 /dev/sda1 mkswap /dev/sda2 swapon /dev/sda2 mkfs.btrfs /dev/sda3
If you decided to go with a RAID system simply add the drives to
the last command, i.e. mkfs.btrfs /dev/sda3 /dev/sdb /dev/sdc
...
Create the subvolumes of the btrfs
filesystem you just formatted.
mount /dev/sda3 /mnt cd /mnt btrfs subvolume create @ btrfs subvolume create @home btrfs subvolume create @var btrfs subvolume create @tmp btrfs subvolume create @snapshots cd umount /mnt
Then, create the folders and mount the partitions
accordingly. Note: if you want to create the home partition or
subvolume on a separate filesystem, you have to cd
out of the
/mnt
directory, unmount the previous partition, and mount the
other disk to /mnt
. On RAID systems this doesn’t matter as
multiple drives form a single filesystem.
mkdir -p /mnt{efi,home,var,tmp,snapshots} mount -o noatime,compress=zstd,space_cache=v2,discard=async,subvol=@ /dev/sda3 /mnt mount -o noatime,compress=zstd,space_cache=v2,discard=async,subvol=@home /dev/sda3 /mnt/home mount -o noatime,compress=zstd,space_cache=v2,discard=async,subvol=@var /dev/sda3 /mnt/var mount -o noatime,compress=zstd,space_cache=v2,discard=async,subvol=@tmp /dev/sda3 /mnt/tmp mount -o noatime,compress=zstd,space_cache=v2,discard=async,subvol=@snapshots /dev/sda3 /mnt/snapshots mount /dev/sda1 /mnt/efi
Note that space_cache=v2
is designed for large filesystems
(above TB), but it is quite new and thus may be less stable.
Once everything is correctly partitioned, formatted, and mounted,
we use pacstrap
to install the base system, linux kernel and
necessary firmware for the machine. Note, for AMD processors use
amd-ucode
instead of the intel microcode update image. If you
chose the classic partitioning layout, there is also no need for
btrfs-progs
.
If stability is of utmost importance, the linux-lts kernel is the way to go. For steam and other high-performance tasks the linux-zen kernel is optimal. If at some later point another kernel is needed, it is always possible to install another alongside your main kernel.
pacstrap /mnt base linux linux-firmware intel-ucode git vim btrfs-progs
Once the base install has finished, we generate the filesystem table which tells the system on reboot what drives to mount and how
genfstab -U /mnt >> /mnt/etc/fstab cat /mnt/etc/fstab
chroot
into /mnt
and set up the hostFirst chroot into the installation to finish setting up the host.
arch-chroot /mnt
This changes the root and effectively replaces /mnt
with /
.
Next, we have to configure the timezone, system language, and keymap, which will be dependent on your location and preference.
For me the timezone is Zurich
ln -sf /usr/share/zoneinfo/Europe/Zurich /etc/localtime hwclock --systohc
this generates a symlink to /etc/localtime
and a file
/etc/adjtime
which ensures the system clock is correctly
synchronized.
For the language, I prefer english. In some rare cases, programs
(for instance steam) require the en_US.UTF-8
locale, so it’s
best to use at least that one. Note that multiple locales are
allowed. The locales are generated by uncommenting the
corresponding lines in /etc/locale.gen
and using the
local-gen
command.
sed -i '177s/.//' /etc/locale.gen # uncomments en_US.UTF-8 UTF-8 locale-gen echo "LANG=en_US.UTF-8" >> /etc/locale.conf
Arch assumes a US keyboard layout by default, so if you choose to use a different layout, say a Swiss keyboard, use
echo "KEYMAP=de_CH-latin1" >> /etc/vconsole.conf
Again, setting the hostname for the machine is a personal choice. I tend to use names of mystical or mythological creatures, some use names from fantasy books or movies such as the Lord of the Rings, and others simply use the product name of the machine.
In this example, I’ll call my host wolpertinger
;)
echo "wolpertinger" >> /etc/hostname echo "127.0.0.1 localhost" >> /etc/hosts echo "::1 localhost" >> /etc/hosts echo "127.0.1.1 wolpertinger.localdomain wolpertinger" >> /etc/hosts
If you haven’t already done so, this is a good time to set the
password for the root (strong(=long) and random passwords are
best from a security perspective). Sometimes, it is more
convenient to ssh
into a new machine and install Arch from a
remote shell, which would have required you to install openssh
and set the root password beforehand.
passwd
In case you decided to use btrfs and/or encryption, you need to
regenerate the initramfs with some modifications. In
/etc/mkinitcpio.conf
, add btrfs
to the MODULES
list.
Later, we will also have to add another hook to the HOOKS
list
to enable snapshots showing up in the grub boot menu.
In /etc/mkinitcpio.conf
add
MODULES=(btrfs)
and regenerate the initramfs image with
mkinitcpio -P
Installing packages is always possible at a later point, and with time you’ll install many more. However, some packages are necessary to make the base system usable. Here is a list which I consider minimally necessary to more or less “conveniently” operate your desktop from the tty after reboot.
pacman -S grub grub-btrfs efibootmgr dosfstools mtools dialog base-devel linux-headers xdg-utils networkmanager wpa_supplicant alsa-utils pulseaudio
this ensures you have a boot manager and basic control over your file systems, network, and audio. For more functionality such as bash completion, more console fonts, ssh, bluetooth, wifi, and printer system you may add
pacman -S bash-completion terminus-fonts openssh bluez bluez-utils blueman wireless_tools pulseaudio-bluetooth pavucontrol cups
I curate a much longer list of packages which I typically use to install all of my packages at this point (see arch.sh for details).
Once you installed all the necessary packages, add yourself as
user. It is generally advisable to create at least another user
which is not root. Add the user to any group that is required.
My username is always phdenzel
, change accordingly in the
following commands
useradd -m phdenzel passwd phdenzel usermod -aG wheel,audio,video,optical,storage phdenzel echo "phdenzel ALL=(ALL) ALL" >> /etc/sudoers.d/phdenzel
In the previous step, you installed the grub
package. Now it’s
time to install the boot loader and configure it.
grub-install --target=x86_64-efi --efi-directory=/efi --boot-directory=/efi --bootloader-id=GRUB
Any changes to the GRUB boot menu can be made in
/etc/defaults/grub
. In my case, I use an Nvidia card. Nvidia
drivers unfortunately don’t play well with the kernel
framebuffer, which makes setting resolutions of the tty a
nightmare. For me, the solution was to disable CSM in the UEFI
BIOS and changing/adding the following lines
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet nvidia-drm.modeset=1" GRUB_GFXMODE=2560x1440x32,1920x1080x32,auto GRUB_GFXPAYLOAD_LINUX=keep GRUB_FONT=/efi/grub/fonts/ter-32b.pf2
For this to work however, we have to install the font for grub
grub-mkfont -o /efi/grub/fonts/ter-32b.pf2 /usr/share/fonts/misc/ter-u32b.otb
Once all the changes to the grub defaults have been made, install them to the ESP partition
grub-mkconfig -o /efi/grub/grub.cfg
Before we boot into our new machine, I suggest already enabling services which are critical for operation post-boot.
systemctl enable reflector.timer systemctl enable NetworkManager systemctl enable bluetooth systemctl enable cups.service systemctl enable sshd systemctl enable fstrim.timer
Naturally, if you skipped installing any of the above packages, these system services cannot be enabled.
I also like the package manager to have colorized output which
can be activated by uncommenting a line in /etc/pacman.conf
sudo sed -i 's/#Color/Color/g' /etc/pacman.conf
grub-btrfs
Additionally, if you chose to install grub-btrfs
, you also need
to change the path of the ESP in the configuration file
/etc/default/grub-btrfs/config
GRUB_BTRFS_GRUB_DIRNAME="/efi/grub"
Remember to update the grub configuration with grub-mkconfig -o
/efi/grub/grub.cfg
and add the initramfs hook at the end to be
able to boot read-only snapshots
HOOKS=(... grub-btrfs-overlayfs)
Afterwars we need to regenerate the image again with mkinitcpio
-P
.
Once everything is installed and configured, exit the chroot, unmount everything, and reboot.
exit
umount -a
reboot
If during the unmounting you receive some warning messages, simply ignore them.
Once rebooted, you should be able to log into a complete installation of Arch Linux on the tty. Of course, there are steps one can take to further set up an Arch installation (besides installing XMonad), but we will stop here. If you are interested in how I further configured my machines, have a look at arch-setup.org.
On Linux you can install a complete desktop environment such as GNOME or KDE, and you should have a graphical platform equivalent to something macOS or Windows offers. Computers with components from this (or the last) decade should be powerful enough to easily handle such a desktop environment. However, there is an alternative.
Tiling window managers are designed to complete just a single task: managing windows. Just like desktop environments, there are many different tiling window managers such as i3, AwesomeWM, dwm, Qtile, XMonad, and many more… they extend the functionality of the Xorg window system (also called X or X11). X is the most widely used window system on UNIX operating systems providing the basic framework for a GUI environment. Less widely used, with less compatibility, and still in development, Wayland is a modern window system alternative to X. The most popular tiling window manager for Wayland is Sway.
Most tiling window managers have to be extensively configured either through a configuration file or through patches before compilation. This makes them less attractive to less experienced users. The advantage of tiling window managers however, is that almost none are bloated, and most designed with simplicity and efficiency in mind. They typically consume less than 300 MB of RAM when idle, which is around 5 times less than full desktop environments, leaving more memory for programs which actually need it. Additionally, they provide a way to define custom keyboard shortcuts to control different aspects of your window systems, which - with a bit of muscle memory - makes your workflows blazingly fast.
While I haven’t tried many different tiling window managers, I am very happy with my current implementation of XMonad written and configured in the Haskell programming language.
Most people using tiling window managers like to pair them with a statusbar where arbitrary information can be displayed, e.g. window name, hardware usage, time and date, and volume. For XMonad, XMobar is a good choice for a statusbar, but in principle it is compatible with any other bar as well.
Before we move on to install and configure XMonad, we have to refresh the repositories first
sudo pacman -Syyu
Next, install all necessary xorg-drivers (check all available
drivers with sudo pacman -Ss xf86-video
; if in doubt simply
install a bunch, the system will choose the right one
automatically). My setup consists of an Intel processor and an
Nvidia GPU (proprietary drivers are a thorn in my eye, but for now
I’m stuck with what works)
sudo pacman -S xf86-video-intel nvidia nvidia-utils nvidia-settings
Make sure to regenerate the initcpio everytime an update of the
nvidia-driver is installed. If you don’t want to manually
regenerate it, add a pacman hook /etc/pacman.d/hooks/nvidia.hook
to ensure triggering the initramfs update automatically
[Trigger] Operation=Install Operation=Upgrade Operation=Remove Type=Package Target=nvidia Target=linux [Action] Description=Update Nvidia module in initcpio Depends=mkinitcpio When=PostTransaction NeedsTargets Exec=/bin/sh -c 'while read -r trg; do case $trg in linux) exit 0; esac; done; /usr/bin/mkinitcpio -P'
There are a few requirements XMonad and XMobar need before we can start with the compilation
sudo pacman -S stack xorg-server xorg-apps xorg-xinit xterm xorg-xmessage xorg-xrandr libx11 libxft libxinerama libxrandr libxss pkgconf wireless_tools
stack
is strictly speaking not necessary, but provides an easy
way to compile and install XMonad and xmobar from a sandboxed
environment.
While it is technically possible to install xmonad
,
xmonad-contrib
(a few community-driven extensions to xmonad
)
and xmobar
via pacman
, I prefer to compile them from source
myself in order to be able to update to the latest version at any
time. This may take a bit longer than simply installing it with
the package manager, but it opens up more options for
configurability.
So, first download the source code from GitHub and initialize a stack environment
mkdir ~/xmonad
cd ~/xmonad
stack setup
stack upgrade
git clone git@github.com:xmonad/xmonad.git
git clone git@github.com:xmonad/xmonad-contrib.git
git clone git@github.com:jaor/xmobar.git
stack init
this creates the file ~/xmonad/stack.yml
. It contains all the
information (external library paths, flags, etc.) needed for the
compilation. If there are no special flags you want to use for
the compilation, there are no changes necessary before
proceeding. I use several “plugins” for the XMobar statusbar
which require additional flags. My stack.yml
file therefore
looks like this
packages: - xmobar - xmonad - xmonad-contrib extra-deps: - netlink-1.1.1.0 flags: xmobar: all_extensions: true
Finally, we can compile and install XMonad and xmobar with
stack install
This should have installed the executables xmonad
and xmobar
in ~/.local/bin
.
sudo ln -s ~/.local/bin/xmonad /usr/bin
XMonad uses a haskell file for on-the-fly configuration. For
newer versions the configuration file is placed in
~/.config/xmonad/
, for older versions in ~/.xmonad/
.
For now, create xmonad.hs
in the configuration folder with the
following content
import XMonad main :: IO() main = xmonad def
this will use XMonad’s default configurations.
Since we used stack
to build XMonad, we have to tell it how to
recompile its executable with the current configuration. Place a
file with the name build
in the configuration folder with the
following content
#!/bin/bash exec stack --stack-yaml $HOME/xmonad/stack.yaml \ ghc -- \ --make xmonad.hs \ -i \ -ilib \ -dynamic \ -fforce-recomp \ -main-is main \ -v0 \ -o "$1"
If you would launch xmonad
in the current state, you would see
a blank screen, and have only basic functionality.
To add more functionality, you will have to change the configuration file. The best resource for this is the official guide on the xmonad.org website and the haskell package repository.
First thing I changed, was to remap the mod key to the Super (or “Windows”) key instead of Alt.
main :: IO () main = xmonad $ def { modMask = mod4Mask -- Rebind Mod to the Super key }
My full configuration is quite lengthy, but here is a snapshot of it anyways. If you’re interested, read through it and feel free to copy anything you find useful. It is a work in eternal progress… so if you’re reading this post long after it is published, rather have a look at the current version in my dotfiles/.config/xmonad/xmonad.hs. There you’ll also find my own color theme and xmobar configurations.
-------------------- -- XMonad configurations -------------------- -------------------- Imports -- Base import XMonad import System.IO import System.Exit import qualified XMonad.StackSet as W -- Data import Data.Monoid import qualified Data.Map as M -- Hooks import XMonad.Hooks.EwmhDesktops (ewmh, ewmhFullscreen) import XMonad.Hooks.ManageDocks (docks, manageDocks, avoidStruts, ToggleStruts(..)) import XMonad.Hooks.ManageHelpers (isFullscreen, isDialog, doFullFloat, doCenterFloat) import XMonad.Hooks.StatusBar import XMonad.Hooks.StatusBar.PP (wrap, shorten, xmobarColor, xmobarBorder, PP(..)) import XMonad.Hooks.RefocusLast (refocusLastLogHook) import XMonad.Hooks.SetWMName -- Layout import XMonad.Layout.Renamed (renamed, Rename(Replace)) import XMonad.Layout.ResizableTile (ResizableTall(..), MirrorResize(MirrorShrink, MirrorExpand)) import XMonad.Layout.GridVariants (Grid(Grid)) import XMonad.Layout.ResizableThreeColumns (ResizableThreeCol(ResizableThreeColMid)) import XMonad.Layout.NoBorders (noBorders, smartBorders, withBorder) import XMonad.Layout.Spacing (spacingRaw, Spacing(..), Border(..)) import XMonad.Layout.LayoutModifier (ModifiedLayout(..)) import XMonad.Layout.ShowWName -- Actions import XMonad.Actions.PhysicalScreens import XMonad.Actions.CycleWS (moveTo, shiftTo, nextScreen, prevScreen, anyWS, ignoringWSs, Direction1D(Next, Prev), WSType(WSIs, (:&:))) import XMonad.Actions.WithAll (sinkAll, killAll) -- Utils -- import XMonad.Util.Dmenu import XMonad.Util.EZConfig (mkKeymap, checkKeymap) import XMonad.Util.Run (safeSpawn, hPutStrLn) import XMonad.Util.SpawnOnce import XMonad.Util.Ungrab (unGrab) import XMonad.Util.NamedScratchpad --- Customized colors import Colors.PhDDark -- color[Trayer, Fore, Back, 01..15] -------------------- Variables -- Default programs myXMobar :: String myXMobar = "xmobar" myXMobarConf :: String myXMobarConf = " ~/.config/xmobar/xmobarrc " myXMobarConf2 :: String myXMobarConf2 = " ~/.config/xmobar/xmobarrc2 " myTerminal :: String myTerminal = "alacritty" myBrowser :: String myBrowser = "brave" myEmacs :: String myEmacs = "emacsclient -c --alternate-editor='emacs'" myEditor :: String myEditor = "emacsclient -c --alternate-editor='emacs'" -- Style config myBorderWidth :: Dimension myBorderWidth = 2 myFont :: String myFont = "xft:Fira Mono:regular:size=9:antialias=true:hinting=true" myNormalColor :: String myNormalColor = colorBack myFocusColor :: String myFocusColor = color06 -- Mouse controls myFocusFollowsMouse :: Bool myFocusFollowsMouse = False myClickJustFocuses :: Bool myClickJustFocuses = False -- Key controls myModMask :: KeyMask myModMask = mod4Mask -- Super-mod4Mask | L-Alt-mod1Mask | R-Alt-mod3Mask -------------------- Main main :: IO () main = do xmonad $ ewmhFullscreen . ewmh . docks $ dynamicSBs xmobarSpawn myConfigs myConfigs = def -- simple stuff { terminal = myTerminal , focusFollowsMouse = myFocusFollowsMouse , clickJustFocuses = myClickJustFocuses , borderWidth = myBorderWidth , modMask = myModMask , workspaces = myWorkspaces , normalBorderColor = myNormalColor , focusedBorderColor = myFocusColor -- key bindings , keys = myKeys , mouseBindings = myMouseBindings -- hooks, layouts , startupHook = myStartupHook , layoutHook = myLayoutHook -- showWName' myShowWNameTheme $ , manageHook = myManageHook , handleEventHook = myEventHook , logHook = myLogHook } -------------------- Startup hook myStartupHook :: X() myStartupHook = do spawn "killall trayer" spawnOnce "resolution_2x11 &" -- set screen resolution using xrandr spawnOnce "xsetroot -cursor_name left_ptr &" -- set cursor spawnOnce "xset r rate 180 35 &" -- increase scroll speed spawnOnce "xrgb -merge ~/.Xresources &" -- load x resources spawnOnce "xmodmap ~/.Xmodmap &" -- load x modmap spawnOnce "picom &" -- start compositor spawnOnce "~/.config/feh/fehbg &" -- set wallpaper spawnOnce "xscreensaver -no-splash &" -- xscreensaver daemon spawnOnce "/usr/bin/emacs --daemon &" -- Emacs daemon spawn ("sleep 2 && trayer --edge top --align right --widthtype request " ++ "--padding 6 --SetDockType true --SetPartialStrut true " ++ "--expand true --transparent true --alpha 0 --height 28 " ++ "--iconspacing 12 " ++ colorTrayer ++ "&" ) spawn "blueman-applet" spawn "nm-applet" spawnOnce "volumeicon" spawnOnce "licht -a &" setWMName "LG3D" -- Java hack return () >> checkKeymap myConfigs myKeymap -------------------- Workspaces myWorkspaces :: [WorkspaceId] myWorkspaces = ["wm", "tty", "dev", "web", "doc", "mu", "tx", "gx", "ls"] -- myWorkspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] -- myWorkspaces = ["<fn=3>\xf036</fn>", "<fn=3>\xf120</fn>", "<fn=3>\xf121</fn>", -- "<fn=3>\xf7a2</fn>", "<fn=3>\xf01c</fn>", "<fn=3>\xf1c0</fn>", -- "<fn=3>\xf56b</fn>", "<fn=3>\xf441</fn>", "<fn=3>\xf038</fn>"] myWorkspaceIndices = M.fromList $ zipWith (,) myWorkspaces [1..] myShowWNameTheme :: SWNConfig -- for indicators when switching workspaces myShowWNameTheme = def { swn_font = "xft:Ubuntu:bold:size=32" , swn_fade = 1.0 , swn_bgcolor = colorBack , swn_color = color07 } windowCount :: X (Maybe String) -- count open windows on workspaces windowCount = gets $ Just . show . length . W.integrate' . W.stack . W.workspace . W.current . windowset -------------------- Layouts mySpacing :: Integer -> l a -> XMonad.Layout.LayoutModifier.ModifiedLayout Spacing l a mySpacing i = spacingRaw True (Border i i i i) True (Border i i i i) True tall = renamed [Replace "tall"] $ avoidStruts $ smartBorders $ mySpacing 3 $ ResizableTall nmaster delta ratio [] where nmaster = 1 -- number of master pane windows ratio = 1/2 -- area ratio of master pane delta = 3/100 -- percentual resizing increment grid = renamed [Replace "grid"] $ avoidStruts $ smartBorders $ mySpacing 3 $ Grid (aspect) where aspect = 16/10 -- desired aspect ratio of windows mirr = renamed [Replace "mirr"] $ avoidStruts $ smartBorders $ mySpacing 3 $ Mirror $ ResizableTall nmaster delta ratio [] where nmaster = 1 ratio = 1/2 delta = 3/100 c3s = renamed [Replace "c3s"] $ avoidStruts $ smartBorders $ mySpacing 3 $ ResizableThreeColMid nmaster delta ratio [] where nmaster = 1 ratio = 1/2 delta = 3/100 full = renamed [Replace "full"] $ avoidStruts $ noBorders $ Full myLayoutHook = (tall ||| grid ||| mirr ||| c3s ||| full) -------------------- Scratchpads myScratchPads :: [NamedScratchpad] myScratchPads = [ NS "terminal" spawnTerm findTerm manageTerm , NS "calculator" spawnCalc findCalc manageCalc , NS "ranger" spawnRanger findRanger manageRanger ] where spawnTerm = myTerminal ++ " -t scratchpad" findTerm = title =? "scratchpad" manageTerm = customFloating $ W.RationalRect l t w h where h = 0.9 w = 0.9 t = 0.95 -h l = 0.95 -w spawnCalc = "qalculate-gtk" findCalc = className =? "Qalculate-gtk" manageCalc = customFloating $ W.RationalRect l t w h where h = 0.5 w = 0.4 t = 0.75 -h l = 0.70 -w spawnRanger = myTerminal ++ " --class ranger -t Ranger -e ranger" findRanger = appName =? "ranger" manageRanger = customFloating $ W.RationalRect l t w h where h = 0.9 w = 0.9 t = 0.95 -h l = 0.95 -w -------------------- Manage windows myManageHook :: XMonad.Query (Data.Monoid.Endo WindowSet) myManageHook = (composeAll . concat $ -- class-based management [ [className =? c <||> title =? c --> doShift (myWorkspaces !! 0) | c <- mywmShifts ] , [className =? c --> doShift (myWorkspaces !! 1) | c <- myttyShifts] , [className =? c --> doShift (myWorkspaces !! 2) | c <- mydevShifts] , [className =? c --> doShift (myWorkspaces !! 3) | c <- mywebShifts] , [className =? c --> doShift (myWorkspaces !! 4) | c <- mydocShifts] , [className =? c --> doShift (myWorkspaces !! 5) | c <- mymuShifts ] , [className =? c --> doShift (myWorkspaces !! 6) | c <- mytxShifts ] , [className =? c --> doShift (myWorkspaces !! 7) | c <- mygxShifts ] , [className =? c --> doShift (myWorkspaces !! 8) | c <- mylsShifts ] , [className =? c --> doFullFloat | c <- myfFloats ] , [className =? c --> doCenterFloat | c <- mycFloats ] , [resource =? r --> doIgnore | r <- myIgnores ] -- situational management , [ isFullscreen --> doFullFloat ] , [ isDialog --> doCenterFloat] ]) <+> namedScratchpadManageHook myScratchPads -- <+> fullscreenManageHook <+> manageDocks <+> manageHook def where -- I only use these on laptops mywmShifts = [ "" ] myttyShifts = [ "" ] mydevShifts = [ "" ] mywebShifts = [ "" ] mydocShifts = [ "" ] mymuShifts = [ "" ] mytxShifts = [ "" ] mygxShifts = [ "" ] mylsShifts = [ "" ] myfFloats = [ "" ] mycFloats = [ "feh", "Xmessage" ] myIgnores = [ "desktop_window", "kdesktop" ] -------------------- Event handling myEventHook = mempty -- mconcat [ fullscreenEventHook, handleEventHook def ] -------------------- xmobar myXMobarPP :: PP myXMobarPP = def { ppSep = wrap hair hair $ grey "|" --, ppWsSep = wrap hair hair $ blue "/" -- focused workspace , ppCurrent = red . xmobarBorder "Bottom" redHex 5 , ppVisible = red -- hidden workspace with windows , ppHidden = blue . xmobarBorder "Top" blueHex 3 . hideNSP -- hidden windows without windows , ppHiddenNoWindows = blue . hideNSP -- layout format map , ppLayout = cyan . (\layout -> case layout of "tall" -> "{|}" "grid" -> "[#]" "mirr" -> "}|{" "c3s" -> "|||" "full" -> "[X]") -- window count , ppExtras = [ windowCount ] -- xmobarColor color03 "" -- order pp fields -- , ppTitle = xmobarColor color07 "" . shorten 25 , ppOrder = \(ws:l:t:ex) -> [ws,l] ++ map red ex } where hideNSP :: WorkspaceId -> String hideNSP ws = if ws /= "NSP" then ws else "" greyHex, blueHex, redHex, cyanHex :: String greyHex = color08 blueHex = color04 redHex = color05 cyanHex = color06 blue, red, cyan, grey :: String -> String blue = xmobarColor blueHex "" red = xmobarColor redHex "" cyan = xmobarColor cyanHex "" grey = xmobarColor greyHex "" hair :: String hair = "<fn=1> </fn>" xmobar = statusBarPropTo "_XMONAD_LOG_1" (myXMobar++myXMobarConf++"-x 0") (pure myXMobarPP) xmobar2 = statusBarPropTo "_XMONAD_LOG_1" (myXMobar++myXMobarConf2++"-x 1") (pure myXMobarPP) xmobarSpawn :: ScreenId -> IO StatusBarConfig xmobarSpawn 0 = pure $ xmobar xmobarSpawn 1 = pure $ xmobar2 xmobarSpawn _ = mempty -- every additional monitor doesn't have a statusbar -------------------- Logging myLogHook = refocusLastLogHook >> nsHideOnFocusLoss myScratchPads -------------------- Keybindings myKeys :: XConfig l -> M.Map (KeyMask, KeySym) (X ()) myKeys conf = (mkKeymap conf myKeymap) <+> (defaultKeymap conf) myKeymap :: [(String, X ())] myKeymap = -- Launch/kill bindings [ ("M-<Return>" , spawn myTerminal) , ("M-/" , spawn "dmenu_run -c -l 15") , ("M-p" , spawn "passmenu") , ("M-S-c" , kill) , ("M-C-c" , kill) , ("M-S-C-c" , killAll) -- XMonad & system bindings , ("M-b" , sendMessage ToggleStruts) -- toggle status bar , ("M-S-b" , spawn "xmobar_toggle") -- kill status bar , ("M-q" , spawn "xmonad_restart") -- recompile & restart xmonad , ("M-S-x" , io (exitWith ExitSuccess)) -- exit XMonad , ("M-S-z" , spawn "xscreensaver-command --activate") -- suspend -- Window control , ("M-<Tab>" , windows W.focusDown) , ("M-j" , windows W.focusDown) , ("M-S-j" , windows W.swapDown) , ("M-k" , windows W.focusUp) , ("M-S-k" , windows W.swapUp) , ("M-m" , windows W.focusMaster) , ("M-S-m" , windows W.swapMaster) , ("M-n" , refresh) , ("M-h" , sendMessage Shrink) , ("M-l" , sendMessage Expand) , ("M-C-h" , moveTo Prev (nonNSP)) , ("M-C-l" , moveTo Next (nonNSP)) , ("M-S-h" , shiftTo Prev (nonNSP) >> moveTo Prev (nonNSP)) , ("M-S-l" , shiftTo Next (nonNSP) >> moveTo Next (nonNSP)) , ("M-<Down>" , windows W.focusDown) , ("M-S-<Down>" , windows W.swapDown) , ("M-<Up>" , windows W.focusUp) , ("M-S-<Up>" , windows W.swapUp) , ("M-<Left>" , sendMessage Shrink) , ("M-<Right>" , sendMessage Expand) , ("M-C-<Up>" , sendMessage MirrorExpand) , ("M-C-<Down>" , sendMessage MirrorShrink) , ("M-C-<Left>" , moveTo Prev (nonNSP)) , ("M-C-<Right>" , moveTo Next (nonNSP)) , ("M-S-<Left>" , shiftTo Prev (nonNSP) >> moveTo Prev (nonNSP)) , ("M-S-<Right>" , shiftTo Next (nonNSP) >> moveTo Next (nonNSP)) , ("M-," , nextScreen) , ("M-." , prevScreen) -- Toggle layouts , ("M-<Space>" , sendMessage NextLayout) , ("M-S-<Space>" , sendMessage FirstLayout) , ("M-f" , sendMessage (JumpToLayout "bfull") >> sendMessage ToggleStruts) , ("M-S-f" , withFocused $ float) , ("M-t" , withFocused $ windows . W.sink) , ("M-S-t" , sinkAll) -- Program bindings , ("M-d" , spawn "pcmanfm") , ("M-\\" , spawn myBrowser) , ("M-=" , unGrab *> spawn "scrot") , ("M-S-=" , unGrab *> spawn "scrot -s") -- Scratchpads , ("M-S-<Return>" , namedScratchpadAction myScratchPads "terminal") , ("M-S-y" , namedScratchpadAction myScratchPads "calculator") , ("M-S-d" , namedScratchpadAction myScratchPads "ranger") ] where nonNSP = anyWS :&: ignoringWSs [scratchpadWorkspaceTag] defaultKeymap conf@(XConfig {XMonad.modMask = modm}) = M.fromList $ [((m .|. modm, k), windows $ f i) | (i, k) <- zip (XMonad.workspaces conf) [xK_1 .. xK_9] , (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]] ++ [((m .|. modm, key), screenWorkspace sc >>= flip whenJust (windows . f)) | (key, sc) <- zip [xK_w, xK_e, xK_r] [0..] , (f, m) <- [(W.view, 0), (W.shift, shiftMask)]] ------------------------------------------------------------------------ -- Mouse bindings: default actions bound to mouse events myMouseBindings (XConfig {XMonad.modMask = modm}) = M.fromList $ -- mod-button1, Set the window to floating mode and move by dragging [ ((modm, button1), (\w -> focus w >> mouseMoveWindow w >> windows W.shiftMaster)) -- , ((0, button2), (\w -> focus w >> mouseMoveWindow w -- >> windows W.shiftMaster)) -- mod-button2, Raise the window to the top of the stack , ((modm, button2), (\w -> focus w >> windows W.shiftMaster)) -- mod-button3, Set the window to floating mode and resize by dragging , ((modm, button3), (\w -> focus w >> mouseResizeWindow w >> windows W.shiftMaster)) -- you may also bind events to the mouse scroll wheel (button4 and button5) ]