xmonad 設定ガイド

はじめに

このドキュメントは xmonad を触ってみたいという方への設定ガイドです。まだドキュメントとして完結していないですが、living document として継続的にアップデートしていきたいと思います。

タイル型ウィンドウマネージャと xmonad

Linux をはじめとする UNIX 系 OS においては Windows や macOS と異なり標準の GUI というものが存在せず、アプリケーションのジャンルとして GUI 環境をつかさどるウィンドウマネージャ (WM) がごく当然のように存在します。このことは OS 固有の統一された GUI と操作感を提供しない代わりに、多様な GUI の開発、ユーザが自分の好みに合わせてそれらを選択できることを可能としています。以下は、このガイドで紹介する xmonad を使用している環境のスクリーンショットです。

xmonad 環境

タイル型 WM は複数存在する WM の種類のひとつで、画面全体を使用しウィンドウをタイル状に配置する種類のものです。通常 Windows や macOS で使用される WM はコンポジット型 WM と呼ばれる種類のもので、もちろん UNIX 系 OS でもコンポジット型 WM も多数存在し、むしろ多数派なのですが、そんな中でもタイル型 WM を愛好するユーザも一定数います。その魅力などについては日本タイル型ウィンドウマネージャ推進委員会 (略称は日本タイル) のサイトに素晴らしい導入があるのでそちらも参照してください: https://ja.osdn.net/projects/tilingwm/wiki/FrontPage

xmonad はタイル型 WM のひとつです。かなり難解というイメージを持たれていますが、それは以下の特徴が原因です。

  • 関数型プログラミング言語 Haskell で書かれていて、設定ファイルも Haskell の書式、というより Haskell プログラムそのもの

設定するのにプログラムを書くというだけでとっつきづらいのですが、さらに言語がまた Haskell だなんて……と言いたくなりますが、救いもあります。

  • Haskell がわからなくても、見様見真似でなんとかなる
  • 拡張性は無限。ライブラリとして公開されている極めて豊富な機能を選択して組み込み、自分だけの WM を作ることができる

ということで、このガイドが「やってみたいけど、とっつきづらい」という方の役に少しでも立てば幸いです。

ここまで書いておいてなのですが、とりあえずタイル型 WM を試してみたいという方には awesome をお勧めします。私も昔は awesome を使っていましたが、とても優れたタイル型 WM で、デフォルトの状態で多くのユーザが必要とする機能を使用可能です。

本ガイドの方針

本ガイドは、読者が xmonad の設定をある程度できるようになることを目標とします。そのため xmonad の使用方法といった内容について深入りはしません。xmonad のサイトに優れたチュートリアルがあるので、そちらに委ねることとします: https://xmonad.org/tour.html

動作確認環境は私の環境です。実際に環境が影響するのはパッケージのインストールくらいかと思いますが。以下に本ガイドを書いている時点での環境を示します。

  • Gentoo Linux
  • ghc-8.0.2
  • xmonad-0.13
  • xmonad-contrib-0.13

インストールから最初の設定まで

インストールと初回起動

まずは xmonad 関連のパッケージをシステムのパッケージ管理システムからインストールしてください。私の使用している Gentoo では x11-wm/xmonad, x11-wm/xmonad-contrib パッケージが該当します。

# emerge -av x11-wm/xmonad x11-wm/xmonad-contrib

xmonad パッケージが xmonad のコアで、xmonad-contrib パッケージがサードパーティ拡張をまとめたものです。デフォルト設定で xmonad を使ということであれば xmonad パッケージのみで動かせますが、すぐに xmonad-contrib の拡張を利用することになるので、最初からインストールしておきます。

まずは何も考えずに ~/.xmonad というディレクトリを作り、以下の内容の xmonad.hs を作成してください。

-- ~/.xmonad/xmonad.hs
import XMonad
main = xmonad def

$PATH に置かれた xmonad コマンドを実行すると、この xmonad.hs がコンパイル・実行されるという動作になります。xmonad.hs がコンパイルされると、私の環境では ~/.xmonad/xmonad-x86_64-linux という実行ファイルが生成されます。通常の感覚とは異なりますが、この xmonad-x86_64-linux こそが WM として動作する xmonad の本体であり、$PATHxmonad コマンドはこの本体をコンパイルしたり起動したりするためのヘルパーコマンドであると言えます。

それでは xmonad が起動されるように (こちらは $PATH に置かれた xmonad コマンド) が設定を行ってください。コンソールから startx コマンドで X を起動している場合は .xinitirc.xsessionexec xmonad と記述することになるでしょうし、ディスプレイマネージャを使用している場合にはそちらの設定を行ってください。また、xmonad 起動の前に xterm が入っていることを確認することをお勧めします。

デフォルトの xmonad

いざ xmonad を起動してみてください。エラーなど発生せずうまく起動できたでしょうか。(壁紙を設定していなければ) 真っ黒な画面がディスプレイ上に出てきて故障かな? と思ったならば、おそらく正常に xmonad が起動できています。

まずはこの状態でチュートリアル (https://xmonad.org/tour.html) に従い基本的な操作を行ってみてください。また、チートシート (https://wiki.haskell.org/File:Xmbindings.png) はしばらく手元に追いておくと有用です。xmonad の感触と、いくつかのことがわかると思います。

  • ウィンドウを開く・閉じるごとに、タイル状に並べられたウィンドウが自動で配置・リサイズされる
  • ウィンドウフォーカスの移動、ウィンドウの配置・サイズ調整を (レイアウトのルールに則ったかたちで) キーボードで行うことができる
  • タイル状のレイアウトに従わないフローティングウィンドウも作成可能
  • タスクバーやステータスバーがない
  • キーボード操作を知らないと何もできない

最初の設定変更: Mod キーの変更

デフォルトの設定では Alt キーが Mod キーとして全ての操作に使用されます。Alt キーは一般的なアプリケーションでショートカットキーとして使用されていたり、Compose キーとして使用している方も多いでしょう。これをもっと役に立っていないキーに割り当てなおすことにしましょう。そう、Mod4 (Windows キーもしくは command キー) です。

-- ~/.xmonad/xmonad.hs
import XMonad
main = xmonad def {
           modMask = mod4Mask
           }

ファイルを保存し Mod+q (この時点ではまだ Mod キーは Alt キー) を押すと、xmonad.hs の再コンパイルと xmonad の (新しい設定での) 再起動ができます。このとき、X 上で実行しているセッションはそのまま維持できます。これは本当に素晴らしいことです。

これで、まずは最初の設定変更を行うことができました。次からはこのコードの構造を確認し、その後のさらなるカスタマイズに移っていきたいと思います。

設定

xmonad.hs の基本的な構造

まず、最初の xmonad.hs がどういう構造になっているかを見ていきます。

import XMonadXMonad モジュールを使用するための宣言です。前述のとおり、xmonad は厳密には WM を実装するためのライブラリでした。これが XMonad モジュールとして提供されています。main = xmonad def はプログラムの実際の部分です。Haskell においてもプログラムの最初に実行されるのは main 関数です。関数型プログラミング言語である Haskell では、関数は数学における関数と同じ概念で、与えられた入力 (引数) に一意に対応する出力 (戻り値) を返すというものであり、C 言語などのように処理を列挙していくものではありません (いったいそんなのでどうやってプログラムを書くんだ? と不思議に思う方もいるかもしれませんが、心配いりません。すぐに見た目上では手続き型のような処理の列挙がでてきます)。この文において main 関数は、xmonad 関数に引数 def を渡したものとして定義されています。このあといろいろと設定を変更していきますが、基本的な概念として「main 関数は xmonad 関数である」ということは不変です。引数として渡すコンフィグの内容を変更したり、適宜必要な処理を付け足していくことによって、デフォルトの無愛想な xmonad から自分好みの xmonad に変化していきます。

ここで xmonad 関数、および def のプログラミング的な情報を確認しておきます。これらは API ドキュメントとしてまとまっているので、そちらから参照可能です。また、型の情報は GHCi から確認することも可能です。

$ ghci
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Prelude> :m +XMonad
Prelude XMonad> :i xmonad
xmonad ::
  (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
        -- Defined in ‘XMonad.Main’

ちょっとややこしくなってきましたが、xmonad 関数は XConfig l という型を引数として取り、戻り値が IO () であるということを示しています。意味わからないし他にも何か出てるんだけと……となりますが、あまり気にしないことにします。引数である XConfig l についてもここで見ておきましょう。

Prelude XMonad> :i XConfig
data XConfig (l :: * -> *)
  = XConfig {normalBorderColor :: !String,
             focusedBorderColor :: !String,
             terminal :: !String,
             layoutHook :: !l Window,
             manageHook :: !ManageHook,
             handleEventHook :: !Event -> X Data.Monoid.All,
             workspaces :: ![String],
             modMask :: ! {-# UNPACK #-}(Foreign.C.Types.N:CUInt[0])KeyMask,
             keys :: !XConfig Layout
                      -> containers-0.5.7.1:Data.Map.Base.Map
                           (ButtonMask, KeySym) (X ()),
             mouseBindings :: !XConfig Layout
                               -> containers-0.5.7.1:Data.Map.Base.Map
                                    (ButtonMask, Button) (Window -> X ()),
             borderWidth :: {-# UNPACK #-}Dimension,
             logHook :: !X (),
             startupHook :: !X (),
             focusFollowsMouse :: !Bool,
             clickJustFocuses :: !Bool,
             clientMask :: {-# UNPACK #-}EventMask,
             rootMask :: {-# UNPACK #-}EventMask,
             handleExtraArgs :: ![String]
                                 -> XConfig Layout -> IO (XConfig Layout)}
        -- Defined in ‘XMonad.Core’
instance a ~ Choose Tall (Choose (Mirror Tall) Full) =>
         Default (XConfig a)
  -- Defined in ‘XMonad.Config’

長い出力となりましたが、これが xmonad 関数に渡すべき引数です。正確には代数的データ型と呼ばれるもののようですが、とりあえず C 言語の構造体のように使うことができます。XConfig { … } で囲まれている部分に列挙されているのが XConfig l 変数の中身となり、それぞれのフィールドについて名前と型が示されています。先ほどは Mod キーの指定を行うために modMask というフィールドを使用しました。

この変数を一から組み上げていっても良いのですが、多くの場合デフォルトから必要な設定を変更していくというアプローチが効率的で、そのために def という定数が用意されています。なお、defXConfig l 専用の定数というわけではなく、オブジェクト指向プログラミングにおける継承のような形で、「デフォルトの値が用意されている型なら def という名前でアクセス可能」という仕組みになっています。後々再登場します。それで、もともとある def に対し、書き換えたいフィールドだけ変更したものを xmonad 関数に渡すということをするのですが、そのための記法が先の設定変更の例に示したものでした。もう一度同じコードをを示します。

-- ~/.xmonad/xmonad.hs
import XMonad
main = xmonad def {
           modMask = mod4Mask
           }

ターミナルアプリケーションの変更

もうひとつ基本的な例を見ておきます。XConfig l 中の terminal を指定することにより、Mod+Shift+Return で起動するターミナルアプリケーションを変更することができます。

-- ~/.xmonad./xmonad.hs
import XMonad

main = xmonad def {
             modMask  = mod4Mask
           , terminal = "urxvt"
           }

また、これは以下のようにも書くことができます。

-- ~/.xmonad/xmonad.hs
import XMonad

main = xmonad def {
             modMask  = myModMask
           , terminal = myTerminal
           }

myModMask = mod4Mask
myTerminal = "urxvt"

さすがプログラミング言語、変数を使用できます。以降、def { … } の中に書くには長い指定する必要がある時や、複数の箇所から同じ値を参照したい可能性がある場合にはこういった変数を使用し、全体の見通しや保守性の向上を図ります。

xmonad 起動時の処理

startupHook を使用して、xmonad 起動時の処理を追加することができます。ここでは xmonad 起動時に以下の処理を行うようにしたいと思います。

  1. カーソルのポインタを設定する
  2. Chromium を起動する

これらはもちろん、.xinitrc などに対応するコマンドを記述することによっても実現可能です。これらの方法の違いとしては、.xinitrc などに記述した場合は X の起動時に一度だけ処理が実行されることに対し、startupHook を利用した場合は Mod+q での xmonad 再起動でも処理が再度実行されることがあります。

-- ~/.xmonad/xmonad.hs
import           XMonad
import           XMonad.Util.Cursor (setDefaultCursor)
import           XMonad.Util.Run    (unsafeSpawn)

main = xmonad def {
             modMask     = mod4Mask
           , startupHook = myStartupHook
           }

myStartupHook = do
    setDefaultCursor xC_left_ptr
    unsafeSpawn "chromium"

ここで XMonad.Util.Cursor, XMonad.Util.Run というふたつのモジュールを追加しています。また、import 文の最後に (setDefaultCursor) といったものを付けているのはそのモジュールから特定の関数 (や定数や型) のみを使用できるようにするものです。これらのモジュールは xmonad-contrib の一部です。startupHook には xmonad 起動時に実行したい処理を記述します。この時に使用できるのが do キーワードで、このキーワードのもと、命令型言語のように処理を列挙できます。

ステータスバーをつける

デフォルト状態の xmonad を触ったときに、おそらくすぐに使用したくなるものがステータスバーだと思います。xmonad にはステータスバーの機能はありません (ステータスバーに連携するための機能はあります)。外部のステータスバー アプリケーションを利用して、ウィンドウなどの情報を表示することにします。ステータスバーのアプリケーションには dzen や xmobar が挙げられます。ここでは xmonad 用に作られたという xmobar を使用することにします。xmobar も Haskell で書かれています。

まずは xmobar をインストールしてください。Gentoo の場合は標準でパッケージが存在します (ぜひ xft USE フラグを有効にしてください)。この記事を書いている時点ではバージョン 0.23.1 を使用しています。

ステータスバーを動作させるためにはいくつか考慮すべきことがあります。

  • ステータスバーに必要なタイミングで必要な情報をアップデートする必要がある
  • ウィンドウはステータスバーに覆い被さらないようにレイアウトされる必要がある

確かに WM に統合されたステータスバーではなく外部のアプリケーションになるので、このようなことをユーザが気にしなくてはいけないのです。面倒ですが、その分自由を得る、例えば複数のステータスバーを配置し使いわけるといったことができます。

-- ~/.xmonad/xmonad.hs
import           XMonad
import           XMonad.Hooks.DynamicLog  (dynamicLogWithPP, ppOutput, xmobarPP)
import           XMonad.Hooks.ManageDocks (avoidStruts, docks)
import           XMonad.Util.Run          (hPutStrLn, spawnPipe)

main = do
    myStatusBar <- spawnPipe "xmobar"
    xmonad $ docks def {
          modMask         = mod4Mask
        , layoutHook      = myLayoutHook
        , logHook         = myLogHook myStatusBar
        }

myLayoutHook = avoidStruts $ layoutHook def

myLogHook h = dynamicLogWithPP xmobarPP {
                    ppOutput = hPutStrLn h
                  }

実際にこれで起動してみると、画面上部にステータスバーが表示され、ウィンドウのタイトルや時刻といった情報が表示されるかと思います。少し不細工かもしれませんが、とりあえず動きました。

カスタマイズを行う前にどのようなコードになっているのかを見てみたいと思います。まず、myStatusBar <- spawnPipe "xmobar" の部分で xmobar を起動して、そのハンドル (パイプ) を myStatusBar に格納しています。spawnPipe に渡している引数はシェルの文字列です。xmobar コマンドは引数に設定ファイルを取るので、後で使用しますが例えば spawnPipe "xmobar ~/.xmonad/xmobarrc" として設定ファイルを指定した xmobar を spawnPipe することができます。<- というのは do ブロックの中でモナドに「包まれた」値を取り出すための記法です。起動時に xmobar を起動するだけであれば startupHook に記述すれば良いのですが、その後そのハンドルを使用して xmobar プロセスへの情報の受け渡しを行うにあたり、こちらに記述する必要がありました。

xmobar のハンドルは logHook で使用します。logHook は、ウィンドウに関するステータスが変化したときに実行されるアクションです。ですので、logHook に xmobar に対して情報をアップデートする (xmobar プロセスの標準入力に文字列を送信する) 処理を記述できれば良いということになります。そのための処理はあらかじめ用意されていて、XMonad.Hooks.DynamicLog モジュールとして提供されています。当該モジュールにはいくつかの方法が用意されているのですが、今回は dynamicLogWithPP という関数を使用しました。もっともシンプルなものではないのですが、必要に応じて xmobar への出力をカスタマイズできる、そして xmonad を使おうという方であればカスタマイズしたくなる、ということで実用的な方法を選んでいます。

dynamicLogwithPP, およびその引数の型 PP は以下です。

ghci> :i dynamicLogWithPP
dynamicLogWithPP :: PP -> X ()
        -- Defined in `XMonad.Hooks.DynamicLog'
ghci> :i PP
data PP
  = PP {ppCurrent :: WorkspaceId -> String,
        ppVisible :: WorkspaceId -> String,
        ppHidden :: WorkspaceId -> String,
        ppHiddenNoWindows :: WorkspaceId -> String,
        ppUrgent :: WorkspaceId -> String,
        ppSep :: String,
        ppWsSep :: String,
        ppTitle :: String -> String,
        ppTitleSanitize :: String -> String,
        ppLayout :: String -> String,
        ppOrder :: [String] -> [String],
        ppSort :: X ([WindowSpace] -> [WindowSpace]),
        ppExtras :: [X (Maybe String)],
        ppOutput :: String -> IO ()}
        -- Defined in `XMonad.Hooks.DynamicLog'
instance Default PP -- Defined in `XMonad.Hooks.DynamicLog'

PP というたくさんのフィールドを備えた型が出てきてしまいましたが、デフォルト値 def をはじめそのまま利用可能な値があらかじめ用意されています。ここでは xmobar 用に準備されている xmobarPP を使用しました。ほとんどそのままで使えるのですがただ一点、ppOutput だけは使用してやる必要があります。xmonad.hs 内で作成される xmobar プロセスへのハンドルを xmobarPP は知らないためです。そのため、xmonad 関数への引数 def と同じ記法で、xmobarPPppOutput を上書きしています。

さて、xmobar の起動と logHook の設定だけでもステータスバーは表示され情報は出力されますが、この状態で何かウィンドウを表示させるとステータスバーの上にウィンドウが覆い被さってしまい、ステータスバーが隠れてしまいます。これを解決するための仕組みが XMonad.Hooks.ManageDocks に用意されています。上記のコードでは以下を行っています。

  1. XConfig ldefdocks 関数を適用する
  2. layoutHookavoidStruts を指定する (追加する) ことにより、ステータスバーに他のウィンドウが覆い被さらないようにウィンドウのレイアウトを行う

defdocks 関数を適用するなんて何の説明にもなっていないようですが、まずは型から見ていきます。

ghci> :i docks
docks :: XConfig a -> XConfig a
        -- Defined in `XMonad.Hooks.ManageDocks'

docks 関数は XConfig a (XConfig l と書いても同じ) を引数に取り、XConfig a を返す関数です。ですので、「docks 関数を適用した defxmonad 関数に渡す」ということで xmonad (docks def) とやることが可能です。これは「defdocks 関数がもたらす機能を追加する」と解釈することができます。具体的に docks 関数が何をやっているかというと、以下 3 つです。

  1. startupHook にドックアプリケーションに関する何らかの処理を追加する
  2. handleEventHookdocksEventHook を追加し、ドックアプリケーションが表示されたタイミングで再レイアウトが発生するようにする
  3. manageHookmanageDocks を追加し、ドックアプリケーションがタイルレイアウトの対象から外すようにする

それぞれ def の中身に記述していっても良いのですが (昔はそうしていた気がします)、少し分量があるうえにカスタマイズの必要性が低いからか、docks という XConfig l 全体に対する関数にこの処理をまとめているようです。

もうひとつの layoutHookavoidStruts を追加しているところを見ていきます。これも型を見てみると、実は docks 関数と同様の構図です。

ghci> :i avoidStruts
avoidStruts ::
  LayoutClass l a =>
  l a -> XMonad.Layout.LayoutModifier.ModifiedLayout AvoidStruts l a
        -- Defined in `XMonad.Hooks.ManageDocks'

詳細はややこしい型の話になるため割愛しますが、layoutHook の型 l a に対して avoidStruts 関数を適用すると、その機能が追加された l a が得られるのです。ここの機能というのが何かというと、ウィンドウのレイアウトを、ステータスバーをはじめとするドックアプリケーションに覆い被さらないように行うということになります。

キーバインディングを変更する

キーバインディングは XConfig l 中の keys フィールドに設定されています。デフォルトで使用される Mod+j 等のキーもすべて def に記述されているので、一度 XMonad.Config のソースを眺めてみることをお勧めします。

keys の型は以下です。

ghci> :i keys
data XConfig (l :: * -> *)
  = XConfig {...,
             keys :: !XConfig Layout
                      -> containers-0.5.7.1:Data.Map.Base.Map
                           (ButtonMask, KeySym) (X ()),
             ...}
        -- Defined in `XMonad.Core'

中核となっているのは Map (ButtonMask, KeySym) (X ()) の部分で、キーの組み合わせとアクションの羅列となっています。想像できるとおり、設定したキーが押されたときに、対応するアクションが実行されるということになります。

def を参考に一からキーバインディングを作成しても良いでのすが、デフォルト設定を使用しつつバインディングを追加・削除するための仕組みが XMonad.Util.EZConfig に用意されているため、それを利用することにします。

~/.xmonad/xmonad.hs
import           XMonad
import           XMonad.Util.EZConfig (additionalKeysP)
import           XMonad.Util.Run      (unsafeSpawn)

main = do
    xmonad $ def {
          modMask = mod4Mask
        }
        `additionalKeysP` myAdditionalKeysP

myAdditionalKeysP = [
      ("M-S-l", unsafeSpawn "alock -auth pam -bg blank")
    , ("M-c",   unsafeSpawn "chromium --incognito")
    ]

アクションの例が unsafeSpawn だけで少々貧弱ですが、レイアウト変更等幅広い操作を行うことが可能です。例えば先の例でステータスバー (ドックアプリケーション) を起動している場合には、sendMessage ToggleStruts をアクションに設定することでその表示有無を切り替えたりすることができます。

additionalKeysP が今回使用している関数で、引数に XConfig l(String, X ()) のリストを取り XConfig l を返す関数です。

ghci> :i additionalKeysP
additionalKeysP :: XConfig l -> [(String, X ())] -> XConfig l
        -- Defined in `XMonad.Util.EZConfig'

今までのパターン通り、「XConfig l に機能を追加する」関数であることがわかるかと思います。ここで本来であれば additionalKeysP (def { … }) myAdditionalKeysP と書くことになるのですが、今回は中置記法という記法を使用して同じことを def { … } `additionalKeysP` myAdditionalKeysP と書いています。今回の場合中括弧の中身が大きくなるため、この記法のほうが見通しが良くなるかと思います。

レイアウトを変更する

レイアウトは XConfig l 中の layoutHok で指定します。def では以下のように定義されています。

-- | The available layouts.  Note that each layout is separated by |||, which
-- denotes layout choice.
layout = tiled ||| Mirror tiled ||| Full
  where
     -- default tiling algorithm partitions the screen into two panes
     tiled   = Tall nmaster delta ratio

     -- The default number of windows in the master pane
     nmaster = 1

     -- Default proportion of screen occupied by master pane
     ratio   = 1/2

     -- Percent of screen to increment by when resizing panes
     delta   = 3/100

ここで、where 句は変数などをうしろから定義するための記法です。上記コード例において tiledTall 1 (1/2) (3/100) と展開されます。また、Mirror tiledtiled を 90 度回転させたレイアウトになります。layout には tiled, Mirror tiled, Full の 3 つのレイアウトが定義されているのですが、これは演算子 (|||) で結合されています。これは複数のレイアウトをユーザが選択できるようにまとめるための演算子として定義されています。

それでは、デフォルト以外のレイアウトを設定してみます。ここでは tiled (Tall) の代わりに、ヒンティングを行う HintedTile を使用することにします。

-- ~/.xmonad/xmonad.hs
import           XMonad                   hiding (Tall)
import           XMonad.Layout.HintedTile (Alignment (..), HintedTile (..),
                                           Orientation (..))
main = do
    xmonad $ def {
          modMask    = mod4Mask
        , layoutHook = myLayoutHook
        }

myLayoutHook = hintedTile Tall ||| hintedTile Wide ||| Full
    where
        hintedTile = HintedTile nmaster delta ratio TopLeft
        nmaster    = 1
        ratio      = 1/2
        delta      = 3/100