搬回平铺式窗口管理器 – XMonad
Moving Back to a Tiling WM – XMonad

原始链接: https://wssite.vercel.app/blog/moving-back-to-a-tiling-wm-xmonad

## XMonad 配置摘要 本文详细介绍了高度定制化的 XMonad 平铺式窗口管理器设置,它从 Manjaro 上的 i3wm 配置演变而来,目前运行在 Fedora 40 环境中。作者最初切换到 Gnome,但怀念精细的控制,因此采用了 XMonad,因为它具有可配置性,并且作者同时正在学习 Haskell。 该配置使用 Haskell 构建,利用其强大的类型系统来实现更安全、更易于维护的按键绑定和模块化结构,以便轻松地在虚拟机之间移植。主要功能包括自定义的 xmobar 插件和同样用 Haskell 编写的配置,针对特定任务(如使用放大列进行写作)定制的每个工作区布局,以及顶部栏中的视觉布局指示器。 该设置使用 `stack` 进行依赖管理和编译,并提供一个简单的构建脚本进行安装。定制化围绕 `Preferences.hs` 文件展开,控制终端模拟器、浏览器和启动应用程序。 暂存板(Scratchpads)用于频繁访问的应用程序,如终端、看板和电子书阅读器,可通过专用按键绑定访问。整个项目作为一个自包含的桌面环境运行,易于克隆和部署。

## Hacker News 上关于平铺式窗口管理器的讨论 最近 Hacker News 上出现了一个关于平铺式窗口管理器(WM),特别是 XMonad,及其在现代桌面环境中的地位的争论。一些用户赞扬 XMonad 的可扩展性和独特的多显示器工作区处理方式(每个显示器拥有自己的一组工作区),而另一些用户则质疑它的实用性,认为用 Haskell 进行配置需要投入大量时间,不如 Niri、Sway 或 i3 等更简单的替代方案。 Niri 是一种滚动式 WM,因其响应速度和直观的工作流程而受到赞扬,一些用户认为它可以减少工作区混乱。然而,另一些用户更喜欢传统的平铺方式以及 AwesomeWM 或 dwm 等 WM 提供的控制力。 这场对话凸显了乐于深度定制的重度用户与优先考虑易用性的人群之间的分歧。许多人认为,KDE 等现代桌面环境已经提供了足够的可平铺功能,从而降低了对专用 WM 的需求。人工智能工具(如 Claude Code)的兴起也被提及,可以简化平铺式 WM 常常复杂的配置过程,弥合定制化与易用性之间的差距。最终,“最佳”WM 仍然取决于个人偏好和工作流程。
相关文章

原文

Here are my dotfiles.

When I was still using Manjaro Linux back in 2019, I got a nudge to try i3wm. It was my first experience with any window manager. And I spent nearly 5 years with it, enjoying the absolute control over my workflow. Nearing the end of 2023, when I finally decided to leave Manjaro (for good), I had a bunch of options on my hand. Fedora looked really promising at that time. But even then, I wasn’t sure I was going to be using any tiling window manager. I happily switched to Gnome in Fedora 40. I ran it along with XOrg so that I could make my capslock key act as a ctrl when held and as an escape when pressed once, using setxkbmap and xcape. But only after spending a few months there, I realized I missed that finer control at my fingertips. So, I resumed searching for a newer tiling window manager. I was also learning Haskell at that time, so picking up XMonad was natural.

XMonad Fastfetch

There are a lot of things that I like about XMonad apart from its standard tiling manager features. I enjoy writing the configuration in Haskell. Where ever possible, I try to leverage the benefits of Haskell’s strong type system. Defining keybindings with a strong type system ensures that I cannot go very wrong with it. Using stack for building my configuration allows me to port the entire configuration easily to other systems, which are my various virtual machines. I have split configuration in various modules.

src
├── Keybindings.hs
├── Layout.hs
├── Plugins
│   ├── Bluetooth.hs
│   ├── Pomodoro.hs
│   └── Soundtrack.hs
├── Preferences.hs
├── Theme
│   ├── Dmenu.hs
│   ├── Font.hs
│   ├── Theme.hs
│   └── Xresources.hs
├── Workspaces.hs
├── xmobar.hs
└── xmonad.hs

3 directories, 13 files

If you want to poke around the config according to your needs, go through Preferences.hs. It contains lots of variables which can be customized like terminal emulator, browser, scratchpads, window gap size etc. It also contains a list of applications which you would like to start automatically at boot.

Overall, the modularization has turned out to be pretty in terms of categorizing things. I tried writing a few xmobar plugins for my own needs. The guide for writing them was straightforward to begin with. I also wrote my entire xmobar configuration in Haskell itself, keeping this executable in the same project. In the end, the project itself became a one-shot way for an entire desktop environment which I can easily clone, compile and install on any system.

I will go briefly over the stack-based setup. The only thing needed is to have a build script at the root of your xmonad project. Everything else is simply a normal stack project with modules and a few executables. I have 2 executables in my project: xmonad and xmobar.

A detailed description and example build files can be found here. My build script is simple enough.

#!/bin/sh

SRC_DIR=$HOME/.config/xmonad
WM=xmonad

unset STACK_YAML
FAIL=0

cd $SRC_DIR
stack install 2>.log || FAIL=1

ln -f -T $(stack exec -- which $WM) $1 2>.log || FAIL=2

XMonad Workflow

Stack is a package manager for Haskell projects and it will be used to compile the package. Install stack either via GHCup or your distribution’s package manager.

mkdir -p $HOME/.config/xmonad
git clone --branch release https://github.com/weirdsmiley/xmonad $HOME/.config/xmonad/
cd $HOME/.config/xmonad
./install.sh

The installation script will install a few fonts and other tools which are default for this setup. It will also write .xinitrc and .Xresources files.

After the installation is complete, and you are logged into xmonad, pressing alt+shift+/ or alt+? will open up a dialog box containing all available keybindings.

3.1. Layouts and per-workspace layouts

XMonad provides a very easy way to describe various layouts that workspaces can follow. I found it useful to constrain only a few layouts on each workspace. I used PerWorkspace for this. This allows me to only switch between specified set of layouts. So for example, my workspace 2 is my writing workspace, in which I have 3 applications. A browser, a pdf reader and a terminal with a tmux session attached to it. This can simply be arranged as a three column layout. But sometimes certain pdfs may have smaller font size which can be tough to read in a column. If I zoom in the pdf it spills sideways, and I have to use arrow keys or h,l to move left and right.

Three column layout

To tackle this, I have another layout with added magnification on top of the three column layout. It magnifies the focused window by a certain limit. And having only these two layouts in my layout set helps me in easily cycling between layouts. I don’t have to skip through 4 different layouts which I would never use in this workspace.

Three column layout with magnification

3.2. Topbar modification

By default, XMonad adds a border to the tiled window which is in focus. I took this idea from here. This adds a title bar with formatted colors. This looks nicer that having a border surrounding the window. The focused window is colored blue while unfocused is colored black. Also, having the title names in topbar looks nice, and in a way removes the need of using XMonadLog’s application names in xmobar itself.

Window topbar

Window topbar unfocused

3.3. Type safety in keybindings

This is something which I truly adore about XMonad and writing its configuration in Haskell. I can write my keybindings in a functional manner and leverage Haskell’s type system to ensure safety. Arranging keybindings in this way, seems more fruitful than having them represented via strings.

myKeys :: XConfig Layout -> M.Map (KeyMask, KeySym) (X ())
myKeys conf@XConfig {XMonad.modMask = modm} =
  M.fromList 
    $ [
      
      ( (modm, xK_q), safeSpawn "xmonad" ["--restart"])
      
      , ((modm, xK_f), sendMessage $ Toggle NBFULL)
      
      , ((modm, xK_l), unGrab *> safeSpawn "env" myLockscreen)
      ]

Each keybinding is comprised of two values of types: KeyMask and KeySym, followed by an X () action. If you don’t want to set a keymask simply pass a 0 or noModMask.

3.4. Submap keybindings and makeChords

Using submaps in xmonad-contrib, I can write a utility function to easily generate a set of keybindings with an added description.

import XMonad.Actions.Submap
import qualified Data.Map as M

makeChords :: a -> [((KeyMask, KeySym), String, X ())] -> [(a, X ())]
makeChords majorKey subKeys =
  (majorKey, submap . M.fromList $ map ((k, _, a) -> (k, a)) subKeys)
    : [ ( majorKey
        , visualSubmap myVisualSubmapDef
            $ M.fromList
            $ map ((k, d, a) -> (k, (d, a))) subKeys)
      ]

soundChords modm =
  makeChords
    (modm, xK_a)
    [ ( (0, xK_a), "open alsamixer"
      , spawn $ myNamedTerminal "alsamixer" ++ " -e alsamixer")
    , ( (0, xK_m), "toggle music playing"
      , getRunningPlayer' >>= player ->
          spawn $ myMusicCtrl ++ " -p "" ++ player ++ "" play-pause")
    ]

myKeys conf@XConfig {XMonad.modMask = modm} =
  M.fromList $ [] ++ soundChords modm

The makeChords adds two distinct sets of keybindings, one normal set and another a visual set, which creates a dialog box when you press the main submap key. In the example above, the soundChords submap is enabled with alt+a, then you can see a dialog box containing two keybindings with their descriptions. Pressing either a or m will launch the first or the second action. The documentation also contains an example which you can read to see the actual code that will be appended to your myKeys.

3.5. Xmobar configuration in Haskell

Writing the Xmobar configuration inside the same project really allows me to keep everything in one place. I create another executable alongside the xmonad executable, in my package.yaml. And then xmonad launches xmobar in the startup apps section.

executables:
  xmonad:
    main: xmonad.hs
    dependencies:
      - xmonad
      - xmonad-contrib
      - containers
  xmobar:
    main: xmobar.hs
    dependencies:
      - xmobar
    ghc-options: -rtsopts -threaded -with-rtsopts=-N

You may have noticed a small icon beside my layout icons on the left side of xmobar. The represent the current layout in a visual form. Try switching layouts with alt+space and see the icons change.

myXmobarPP =
  def
    { ppLayout =
        case
          "Columns" -> "<icon=Columns.xpm/>"
          "MagnifiedColumns" -> "<icon=MagnifiedColumns.xpm/>"
          "Full" -> "<icon=Full.xpm/>"
          "Tall" -> "<icon=Tall.xpm/>"
          "ThreeCol" -> "<icon=MagnifiedColumns.xpm/>"
          "2-by-3 (left)" -> "<icon=TwoByThreeLeft.xpm/>"
          "2-by-3 (right)" -> "<icon=TwoByThreeRight.xpm/>"
          "2x3 LT" -> "<icon=TwoByThreeLeftWithTabs.xpm/>"
          "2x3 RT" -> "<icon=TwoByThreeRightWithTabs.xpm/>"
          "Tiled" -> "<icon=Tiled.xpm/>"
          _ -> "<icon=Unknown.xpm/>"
    }

main =
  xmonad
    . withEasySB (statusBarProp "xmobar" (pure myXmobarPP)) defToggleStrutsKey
    $ myConfig

3.6. Scratchpads in action

I am using 4 scratchpads in total. Each scratchpad is mapped to a keybinding.

  [
  
  ((modm, xK_Return), namedScratchpadAction myScratchpads "terminal")
  
  , ((modm, xK_x), namedScratchpadAction myScratchpads "Kanboard")
  
  , ((modm, xK_z), namedScratchpadAction myScratchpads "CalibreWeb")
  
  , ((modm, xK_m), namedScratchpadAction myScratchpads "Anki")
  ]

myScratchpads
 =
  [ NS "terminal" spawnTerm findTerm manageTerm
  , NS "Kanboard" spawnKanboard (className =? "Kanboard") doFullFloat
  , NS "Anki" spawnAnki (className =? "Anki") doFullFloat
  , NS "CalibreWeb" spawnCalibreWeb (className =? "CalibreWeb") doFullFloat
  ]
  ...

I realized that I don’t really open new terminals that often because I use tmux (with tmux-resurrect and tmux-continuum). So I remapped alt+enter with showing the terminal scratchpad, instead of the usual, open a new terminal.

I can open up the calibre-web instance with alt+z, and immediately resume whatever I was reading.

If you have any questions for me, head over to this discussion page.

联系我们 contact @ memedata.com