Arch Linux and XMonad

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.

Table of Contents



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 origin story (with Linux)

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.

Arch installation

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

Preparations

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

Boot mode

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

Internet connection

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.

Update the mirrorlist

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

Disk partitioning and formatting

Here, you have to make several decisions:

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.

Base install

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 host

First chroot into the installation to finish setting up the host.

arch-chroot /mnt

This changes the root and effectively replaces /mnt with /.

Time clock, locale, and keyboard layout

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

Set the hostname

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

Initramfs

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

Package installs

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).

Add another user

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

Boot loader

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

Final touches

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.

Reboot

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.

XMonad

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.

Installing requirements

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.

Compiling from source

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

Configuring XMonad

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)
    ]