2013年4月24日水曜日

Haskell に興味がある人向け xmonad 設定ガイド



2016/03/12 追記: あらためて読み返してみると,Haskell に関連する記述はだいたい間違っていることがわかりました.xmonad の設定をするうえではあまり困らないことだとは思いますが,その旨ご了承ください.



xmonad は UNIX 系 OS で動作する (タイル型) ウィンドマネージャ。非常に拡張性が高く、基本タイル型ウィンドマネージャなのだけど、設定次第では全くタイル型でなくすことも可能なほど。素晴らしいソフトウェアなのだが、設定方法が独特でなかなかとっつきにくいのも事実。
最近 Haskell 学習をしており、ようやく少し設定が理解できてきたので、まとめる。
動作環境は以下のとおり。
  • ghc-7.4.2
  • xmonad-0.10, xmonad-contrib-0.10

1. 概要、インストール

xmonad は Haskell で書かかれたソフトウェアである。通常ユーザはソフトウェアが使用している言語を気にすることはあまりないが、xmonad の独特なところは、設定をこの Haskell で記述する必要があること。
厳密に言うと、パッケージ化されている xmonad はウィンドウマネージャを実装するためのライブラリであり、各ユーザはライブラリの機能を使用して、自分のウィンドウマネージャをプログラム、コンパイルして使用する。

インストールは各 OS / ディストリビューションのパッケージ管理システムから。Gentoo の場合は Portage からインストール。
# emerge -av x11-wm/xmonad x11-wm/xmonad-contrib
xmonad が xmonad 本体であり、xmonad-contrib はサードパーティの拡張をまとめたもの。xmonad-contrib もすぐに使いたくなるのでこのタイミングでインストールしておく。

まずは何も考えずに ~/.xmonad というフォルダを作り、そこに以下の内容の xmonad.hs を作成する。これが $PATH に置かれた xmonad 実行時に (新しい xmonad.hs があった場合には) コンパイルされ実行される。
-- ~/.xmonad/xmonad.hs
import XMonad

main = xmonad defaultConfig
まずはこれで終了。~/.xinitrc で exec xmonad してやれば真っ黒な画面がでてきて素晴らしき xmonad ワールドご対面。
操作方法は xmonad : a guided tour に譲るとして、とりあえず、終了は Alt+Shift+q. X の設定で alt_shift_toggle とか設定してると何もできなくて泣けます。

2. 設定を変更する

上に示した xmonad.hs はなんとなく見てのとおり、main 関数で xmonad を実行してデフォルトのコンフィグを渡しているものとなっている。
この xmonad というのも関数で、引数にコンフィグを取るようになっている。型をチェック。
ghci> :m +XMonad
ghci> :i xmonad
xmonad ::
  (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
        -- Defined in `XMonad.Main'
これは XConfig l -> IO () の部分が XConfig の型の引数 l を取り、返り値が IO () であることを示している。返り値が IO () って何? というのは、多分今回はここ以外には出てこないと思うので気にしないことにする。(LayoutClass l Window, Read (l Window)) の部分は引数 l の性質を示すものだが、これも気にしないことにする。
とにかく、main で xmonad 関数を呼ぶ。xmonad 関数にはコンフィグを引数として渡せば良い。
ところで、この XConfig はどんなものかというと、以下のパラメータを持つデータ。
ghci> :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 #-} !KeyMask,
             keys :: !XConfig Layout
                      -> Data.Map.Map (ButtonMask, KeySym) (X ()),
             mouseBindings :: !XConfig Layout
                               -> Data.Map.Map (ButtonMask, Button) (Window -> X ()),
             borderWidth :: {-# UNPACK #-} !Dimension,
             logHook :: !X (),
             startupHook :: !X (),
             focusFollowsMouse :: !Bool}
        -- Defined in `XMonad.Core'
{} 内に示されているのがパラメータで、名前と型が示されている。例えばnormalBorderColor は String の型のパラメータで、ウィンドウの縁の線の色を指定できる。これらを全部自分の好きなように定義して、xmonad 関数に渡してやれば自分好みのウィンドウマネージャができあがる。

ただ、これを一から全部書いていくのは面倒だし、だいたいの動作はデフォルトで良いもの。ということで、以下のように書くことによって、デフォルトのコンフィグをベースに必要なパラメータだけ変更することができる。
-- ~/.xmonad/xmonad.hs
import XMonad

main = xmonad defaultConfig {
      modMask = mod4Mask
    }
これは mod key を mod4 (Windows キーとか、command キーとか) に変更するための設定。デフォルトだと Alt が割り当てられており、他のショートカットキーと被ることが多いと思う。ちなみに modMask の型は KeyMask で、これは Graphics.X11.Types で定義されている。
それから、defaultConfig は Xmonad.Config で定義されている。ソースを見ると、デフォルトの設定や、設定方法の確認ができる。

設定を変更したら、mod + q で xmonad.hs の再コンパイル、および xmonad の再起動ができる。この時、X 上で実行しているセッションはそのまま維持できる (!!) awesome など他のウィンドウマネージャだと、設定ファイルを変更したら一度ウィンドウマネージャを終了し、再度一から起動という流れになるため設定ファイルをいじるのが面倒なのだが、xmonad であればブラウザで設定方法を調べながら気軽に設定変更できる。

3. ステータスバーをつける

まずステータスバーをつけてみる。xmonad 自体にステータスバーの機能はなく、外部のアプリケーションを利用してウィンドウタイトルなどの情報を表示する。
ステータスバーアプリケーションは dzen, xmobar が挙げられる。前まで dzen を使っていたけれど最近 xmobar にしたので、xmobar の例を示す。xmobar は xmonad 用にということで作られているようなので、きっと相性が良い。ちなみに xmobar も Haskell で書かれている。

xmobar のインストール。Gentoo の場合は stable になっていないので、keyword を許可してから emerge. 2013 年 4 月現在 0.13, 0.14, 0.16-r1 が Portage tree にあるが、0.16 は xmonad との連携がうまくいかないという xmobar 側のバグがあるので、明示的に 0.14 をインストール。
# echo '=x11-misc/xmobar-0.14' >> /etc/portage/package.accept_keywords
# emerge -av =x11-misc/xmobar-0.14

xmonad.hs は以下のとおり。
-- ~/.xmonad/xmonad.hs
import XMonad
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Util.Run                  -- spawnPipe, hPutStrLn

main = do
    myStatusBar <- spawnPipe "xmobar"
    xmonad defaultConfig {
          modMask         = myModMask
        , layoutHook      = myLayoutHook
        , manageHook      = myManageHook
        , logHook         = myLogHook myStatusBar
        }

myModMask = mod4Mask

myLayoutHook = avoidStruts $ layoutHook defaultConfig
myManageHook = manageDocks <+> manageHook defaultConfig

myLogHook h = dynamicLogWithPP xmobarPP {
                  ppOutput = hPutStrLn h
                }
一気にコードが増えたが、まず main 関数から見ていく。
今回 do というキーワードが追加された。これは IO などの処理を複数続けて記述するためのキーワード。次の myStatusBar <- spawnPipe "xmobar" で xmobar を起動し、myStatusBar にそのハンドラを格納している。
myStatusBar の部分は myStatusBar = spawnPipe "xmobar" ではない。spawnPipe は以下の型を持っている。
spawnPipe :: MonadIO m => String -> m Handle
ここで m というキーワードは返り値がモナドであることを示している。ここから Handle の値を取り出すための演算子が <- である。この仕組みがどんな役に立つのかは勉強中。ちなみに、spawnPipe が定義されている XMonad.Util.Run は xmonad-contrib パッケージに含まれているモジュール。早速使用。

とにかく、spawnPipe によって xmobar を起動したら、残りは先程の xmonad 関数。まずステータスバーに関係のあるところは、logHook = myLogHook myStatusBar. logHook はウィンドウマネージャのステータス (選択中のウィンドウなど) に変更があった時に実行するアクションを設定する。このアクションは XMonad.Hooks.DynamicLog にお膳立てされており、今回は dynamicLogWithPP という関数を使用する。この関数は PP というデータを引数として取り、これによってステータス出力のフォーマットや、出力のためのアクションを規定できる。この PP にもナイスなデフォルト PP があり、xmobar 用に用意された xmobarPP を使用することとする。
ただし xmobarPP を使用する場合にも最低限、出力先を指定する必要がある (自分で起動させた xmobar の存在をあらかじめ用意された xmobarPP は知る術がない) ので、そこだけ上書きをしてやる。パラメータの書き方は defaultConfig のパラメータを上書きした方法と同じ。

ppOutput と、使用している hPutStrLn の型は以下のとおり。
ppOutput :: String -> IO ()
hPutStrLn :: Handle -> String -> IO ()
ppOutput は String を引数に取って IO のアクションを実行する関数。Haskell ではデータに関数も含めることができる。指定したハンドラに対して文字列を出力する hPutStrLn を使用して出力先を先ほど起動させた xmobar に向けてやったものを ppOutput にセットすれば良い。
ただし、ここで ppOutPut = hPutStrLn myStatusBar とはできない。main 関数の中で定義した myStatusBar は main 関数の外から参照できない。なので、myLogHook はハンドラ h を引数として取り、main 関数の logHook 設定時に引数として myStatusBar を渡している。もしくは、myLogHook を使用せずに main 関数の中で直接 logHook = dynamicLogWithPP xmobarPP { ppOutput = hPutStrLn myStatusBar } としてやれば直接 myStatusBar が参照できるが、今後 PP のカスタマイズ項目が増えることを考えると前者のほうが良いと思う。

以下がとりあえずの ~/.xmobarrc のサンプル。
-- ~/.xmobarrc
Config { font = "-misc-fixed-*-*-*-*-10-*-*-*-*-*-*-*"
       , bgColor = "black"
       , fgColor = "grey"
       , position = Top
       , lowerOnStart = True
       , commands = [ Run Weather "EGPF" ["-t",": C","-L","18","-H","25","--normal","green","--high","red","--low","lightblue"] 36000
                    , Run Network "eth0" ["-L","0","-H","32","--normal","green","--high","red"] 10
                    , Run Network "eth1" ["-L","0","-H","32","--normal","green","--high","red"] 10
                    , Run Cpu ["-L","3","-H","50","--normal","green","--high","red"] 10
                    , Run Memory ["-t","Mem: %"] 10
                    , Run Swap [] 10
                    , Run Com "uname" ["-s","-r"] "" 36000
                    , Run Date "%a %b %_d %Y %H:%M:%S" "date" 10
                    , Run StdinReader
                    ]
       , sepChar = "%"
       , alignSep = "}{"
       , template = "%StdinReader% }{ %cpu% | %memory% * %swap% | %eth0% - %eth1% %date%| %EGPF% | %uname%"
       }
これは xmobar-0.14 に付属のコンフィグ (/usr/share/doc/xmobar-0.14/xmobar.config.bz2) に、標準入力から受け取った文字列を表示するための StdinReader を足しただけのもの。xmonad からの情報は hPutStrLn により xmobar の標準入力へと渡る。
ちなみに、dzen を使用する場合も同様にしてステータスバーを実現できる。ただし dzen は標準入力から受け取った文字列を表示するだけの機能なので、時間や CPU 利用率といった情報を表示させたい場合には dzen を複数起動するか、xmonad と dzen の間に情報をマージするスクリプトを挟んでやる必要がある。

ここまでの内容 (layoutHook, manageHook を設定しない) でもステータスバーは表示され適切な情報が出力されるのだが、この状態で何かウィンドウを表示させるとステータスバーの上にウィンドウが覆い被さってしまい、ステータスバーが見えなくなってしまう。
この点を解決するための仕組みが XMonad.Hooks.ManageDocks に用意されている。その名のとおり、ステータスバーなどのドックタイプのアプリケーションをうまく制御してくれる。

まず最初が myManageHook = manageDocks <+> manageHook defaultConfig の部分。これが xmonad に渡すコンフィグの manageHook にセットされる。manageHook はウィンドウが開いた時に呼び出されるアクションで、アプリケーションに応じて Floating にしたり特定のワークスペースに移動させたりといった処理が行える。今回 manageDock はウィンドウがドックアプリケーションであるかの判別を行うために追加する。
追加のベースは defaultConfig のもの。manageHook defaultConfig と記述することで defaultConfig の当該パラメータを取得できる。それに <+> 演算子で manageDocks の処理を追加する形になっている。<+> はモナドの結合を行うための演算子だと思われるがよくわかっていない。とにかく manageHook には <+> 演算子で関数を繋げていくことにより必要な処理を追加してくことができる。

次に myLayoutHook = avoidStruts $ layoutHook defaultConfig の部分。これはコンフィグの layoutHook にセットされる。layoutHook はウィンドウの配置やサイズの調整といったロジックをセットするもので、通常のタイル以外の様々なレイアウトもここで設定することができる。今回は avoidStruts を追加することによって、ドックアプリケーションのウィンドウには被らないようにウィンドウを配置する処理を追加する。
今回の演算子 $ は単純な関数適用であり、myLayoutHook = avoidStruts (layoutHook defaultConfig) と記述しても同様の結果が得られる。
layoutHook は関数を重ねて適用していくことにより処理を追加することができる。こういった場合に、この演算子を使用すると見やすい。

それからついでに、modMask の設定を myModMask = mod4Mask として外に出した。これで本章冒頭に示した xmonad.hs の内容となった。

4. キーバインディングを設定する

キーバインディングはコンフィグの keys パラメータになる。keys の型は以下の関数。
keys :: !(XConfig Layout -> Map (ButtonMask, KeySym) (X ()))
なぜ関数かというと、keys の第 1 引数にコンフィグそれ自体を渡すため。主に modkey の設定になるかと思うが、コンフィグを引数に取りその modMask を参照することにより、modkey の設定を変更した場合に keys に手を加えることなく、変更を反映させることができる。defaultConfig では keys は以下のように定義されている。
keys :: XConfig Layout -> M.Map (KeyMask, KeySym) (X ())
keys conf@(XConfig {XMonad.modMask = modMask}) = M.fromList $
    -- launching and killing programs
    [ ((modMask .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf) -- %! Launch terminal
--- snip ---
conf@(XConfig {XMonad.modMask = modMask}) の部分は引数に対するパターンマッチで、与えられた引数 conf から modMask パラメータの値を modMask 変数に取り出している。この modMask を利用して、キーバインド (KeyMask, KeySym) と アクション X () の Map を作成している。

defaultConfig を参考に一からキーバインディングの Map を作成しても良いが、デフォルトで設定されているバインディングは可能な限り使いたい。上の例のように defaultConfig から keys を取り出したうえで Map の操作でカスタムのバインディングを結合しても良いが、記述が複雑になる。ということで、これを簡単に行うための関数が XMonad.Util.EZConfig に準備されている。これを使用してキーバインディングのカスタマイズを行う例を示す。
-- ~/.xmonad/xmonad.hs
import XMonad
import XMonad.Util.EZConfig             -- removeKeys, additionalKeys

main =
    xmonad defaultConfig {
          modMask         = myModMask
        , logHook         = myLogHook myStatusBar
        }
        `removeKeys` myOverriddenKeys
        `additionalKeys` myAdditionalKeys

myModMask = mod4Mask
myFont = "VL Gothic:size=6"

myAdditionalKeys = [
    -- launch dmenu
      ((myModMask,               xK_p     ), spawn ("dmenu_run -fn '" ++ myFont ++ "' -nf grey"))

    -- Custom commands
    , ((myModMask,               xK_c     ), spawn "chromium --incognito")
    ]

myOverriddenKeys = [
      (myModMask,               xK_p     )
    ]
ここで使用している removeKeys, additionalKeys が XMonad.Util.EZConfig の関数で、いずれも第 1 引数に XConfig, 第 2 引数に (KeyMask, KeySym) と X () の Map を取り、XConfig を返す。
additionalKeys :: XConfig a -> [((ButtonMask, KeySym), X ())] -> XConfig a
removeKeys :: XConfig a -> [(ButtonMask, KeySym)] -> XConfig a
上記 xmonad.hs で defaultConfig { ... } `removeKeys` myOverriddenKeys という表記をしているのは Hsakell での関数適用の表記方法のひとつで、removeKeys (defaultConfig { ... }) myOverriddenKeys と書くのと同じ。この表記をすれば、「defaultConfig から myOverriddenKeys を抜いて、myAdditionalKeys を追加する」 ということが読み取りやすい。

さて、キーバインディングカスタマイズの具体的な中身は myAdditionalKeys を参照するとわかる。以下の 2 点の変更を行おうとしている。
  1. mod-c で choromium --incognito を実行
  2. mod-p で dmenu_run をフォント指定のオプション付きで実行
1. については defaultConfig で設定されていないキーのため、普通に Map を記述すれば良い。一方、2. はもともと defaultConfig で設定されているキーであり、defaultConfig に対しこの Map を additionalKeys で追加しようとすると、キー設定を上書きできずにカスタム設定が反映されない。そのため、こういった設定については、myOverriddenKeys に記載をしているように一度 defaultConfig から removeKeys で設定を削除したうえで、そのコンフィグに対し additionalKeys で設定を追加する。

そういえば、dmenu_run は Gentoo では x11-misc/dmenu でパッケージされている。Xft を使用する時には USE="xft" を忘れずに。

5. コンフィグサンプル

以下が現在の私の設定。
-- ~/.xmonad/xmonad.hs
import XMonad
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.ManageHelpers       -- isFullscreen, isFullFloat
import XMonad.Layout.Maximize
import XMonad.Layout.Minimize
import XMonad.Layout.NoBorders          -- smartBorders, noBorders
import XMonad.Util.EZConfig             -- removeKeys, additionalKeys
import XMonad.Util.Run                  -- spawnPipe, hPutStrLn
import XMonad.Util.WorkspaceCompare     -- getSortByXineramaRule

main = do
    myStatusBar <- spawnPipe "xmobar"
    xmonad $ ewmh defaultConfig {
          modMask         = myModMask
        , handleEventHook = myHandleEventHook
        , layoutHook      = myLayoutHook
        , manageHook      = myManageHook
        , logHook         = myLogHook myStatusBar
        }
        `removeKeys` myOverriddenKeys
        `additionalKeys` myAdditionalKeys


myModMask = mod4Mask
myFont = "VL Gothic:size=6"

myLayoutHook =   (smartBorders $ avoidStruts $ maximize $ minimize (tiled ||| Mirror tiled))
             ||| (noBorders Full)
    where
        tiled   = Tall nmaster delta ratio
        nmaster = 1
        ratio   = 1/2
        delta   = 3/100

myHandleEventHook =   docksEventHook
                  <+> fullscreenEventHook

myManageHook =   manageDocks
             <+> composeAll [
                      isFullscreen                      --> doFullFloat
                    , className =? "MPlayer"            --> doFloat
                    , className =? "Gimp"               --> doFloat
                    , className =? "Thunderbird"        --> doShift "9"
                    , className =? "Pidgin"             --> doShift "9"
                    ]

myLogHook h = dynamicLogWithPP xmobarPP {
                  ppSep    = " | "
                , ppTitle  = xmobarColor "green" "" . shorten 80
                , ppOutput = hPutStrLn h
                , ppSort   = getSortByXineramaRule
                }

myAdditionalKeys = [
    -- launch dmenu
      ((myModMask,               xK_p     ), spawn ("dmenu_run -fn '" ++ myFont ++ "'"))

    -- maximize
    , ((myModMask,               xK_m     ), withFocused (sendMessage . maximizeRestore))

    -- minimize and restore
    , ((myModMask,               xK_n     ), withFocused minimizeWindow)
    , ((myModMask .|. shiftMask, xK_n     ), sendMessage RestoreNextMinimizedWin)

    -- toggle dock visibility
    , ((myModMask,               xK_b     ), sendMessage ToggleStruts)

    -- Custom commands
    , ((myModMask,               xK_c     ), spawn "chromium --incognito")
    ]

myOverriddenKeys = [
      (myModMask,               xK_p     )
    ]
-- ~/.xmobarrc
Config { font = "xft:VL Gothic:size=6"
       , bgColor = "black"
       , fgColor = "grey"
       , position = Top
       , lowerOnStart = True
       , commands = [ 
        Run StdinReader
      , Run Com "cut" ["-d ' '","-f -3","/proc/loadavg"] "loadavg" 10
      , Run Network "wlan0" ["-L","0","-H","32","--normal","green","--high","red"] 10
      --, Run Battery ["-t","% "] 300
      , Run Com "acpi" [] "battery" 300
          , Run Date "%a %b %_d %Y %H:%M:%S" "date" 10
                    ]
       , sepChar = "%"
       , alignSep = "}{"
       , template = "%StdinReader% }{ Load: %loadavg% | %battery% | %wlan0% | %date%"
       }

皆様素敵な xmonad ライフを。

0 件のコメント:

コメントを投稿