XMonadでカスタマイズ可能なマルチカラムレイアウトを

Table of Contents

はじめに

XMonadで3列以上のレイアウトを提供するモジュールとして、xmonad-contribThreeColumnsMultiColumnsが存在する。ThreeColumnsは3列固定のレイアウトで、MultiColumnsは列数が動的に増減するレイアウトだ。しかし、いずれのレイアウトも筆者の要求を満たすものではなく、新しいレイアウトを独自に作成するに至った。

それがStableColumnsというレイアウトで、事前に定義した列のキャパシティに応じてウィンドウを配置する。配置するための列が動的に増減せずStableであるという意味で名付けた。

本稿では、筆者が作成したマルチカラムレイアウトStableColumnsを紹介するとともに、どのような要求の下でこれを作ったのかを述べる。

要求1:列へのウィンドウの配置順をカスタマイズ

まず、3カラムレイアウトを使う時の筆者の代表的な使い方は以下のようなものだ。

  • 左列:ターミナルを配置する作業用の領域
  • 中央列:Webブラウザを配置する資料閲覧用の領域
  • 右列:動画再生用の領域

そのためには、以下のような順序でウィンドウが配置されて欲しい。

A 1 2 3 4 5
理想とする3カラムレイアウトのウィンドウ配置

しかし、ThreeColumnsではこのような配置順にはならない。もし、ThreeCol 1 (3/100) (1/2)のようにレイアウトを定義したなら、最初のウィンドウは一番左のメイン列に配置され、以降のウィンドウは右二つの列に交互に配置される。

A 1 2 4 3 5
ThreeColumnsのウィンドウ配置

そこで、理想の配置順を実現する設計として、列ごとに収納できるウィンドウの数(キャパシティ)を設定できようにすることを考えた。具体的には、多くともN個までのウィンドウを配置するStaticColumn少なくともN個のウィンドウを配置するDynamicColumnという2つの列の変種を考えた。コード中の列の定義は以下のようになっている。

data Column = Column
  { columnKind :: ColumnKind
  , columnRatio :: Rational
  , columnRowRatios :: [Rational]
  }
  deriving (Read, Show)

data ColumnKind
  = ColumnKindStatic Int
  | ColumnKindDynamic Int
  deriving (Read, Show)

staticColumn :: Int -> Rational -> [Rational] -> Column
staticColumn n = Column (ColumnKindStatic n)

dynamicColumn :: Int -> Rational -> [Rational] -> Column
dynamicColumn n = Column (ColumnKindDynamic n)
StableColumnsの列の定義

この時、列には以下のような手順でウィンドウが配置される。

  1. 定義されたすべての列を左から順に走査して、列のキャパシティが0になるまでウィンドウを配置する。
  2. 未配置のウィンドウをDynamicColumnの列に対して左から順に1枚ずつ配置する。
  3. 未配置のウィンドウがなくなるまで手順2を繰り返す。

この手順による実際のウィンドウの配置の流れを示す。まず、レイアウトに以下のような3つの列が設定されているとする。

columns =
  [ staticColumn 1 (2/1)
  , dynamicColumn 1 (1/4)
  , dynamicColumn 3 (1/4)
  ]
StableColumnsの列の設定

この時、4つのウィンドウを配置することを考える。まず、手順1によって左列から順にキャパシティが0になるまでウィンドウを配置する。この場合、右列のみキャパシティを1残してすべてのウィンドウは配置される。未配置のウィンドウはないのでここで配置は終了し、配置は以下のようになる。

A 1 2 3 4
StableColumnsで4つのウィンドウを配置した例

続いて、同じ設定で6つのウィンドウを配置することを考える。まず、手順1によってキャパシティの空いてる列に5つのウィンドウが配置される。すべてのキャパシティは消費されたので、残る1つのウィンドウは手順2によってDynamicColumnの最初の列(中央列)に配置される。

A 1 2 6 3 4 5
StableColumnsで6つのウィンドウを配置した例

このアルゴリズムによって、ThreeColumns相当のレイアウトも、2列や4列以上のレイアウトも実現できる柔軟性を手に入れることができた。なお、列のキャパシティはIncMasterNメッセージによって、動的に増減させることができる。

要求2:列間の境界線をリサイズ

次は、列のリサイズについて考える。列が二つしかないならリサイズする方法は自明だが、列が三つ以上ある時にはリサイズする方法は二つ考えられる。一つは列そのものをリサイズする方法で、もう一つが列の境界をリサイズ方法だ。その違いを示したのが以下の図で、左列 を半分の幅に縮小した時のリサイズ結果の違いを表している。

cluster1 Original cluster3 Result B cluster2 Result A O 1 2 3 4 R1 1 2 3 4 O->R1 R2 1 2 3 4 O->R2 Resize
3カラムレイアウトにおける2つのリサイズ方法

図中のResult Aが左列そのものをリサイズした結果で、左列が縮小した分だけ中心と右の列の大きさが拡張されている。Result Bは左の列と中心の列の境界をリサイズした結果で、この場合は中心列の大きさのみが拡張されている。

この点について、ThreeColumnsではどの列にフォーカスが当たっているかは関係なく、左列(ThreeColMidなら中心列)しかリサイズできない。リサイズ方法としては前者の「列そのものをリサイズする方法」を取っている。

一方、StableColumnsでは現在フォーカスのある列に対して、後者の「列の境界をリサイズする方法」でリサイズを実行する。これによってすべての列がリサイズ可能になるとともに、列をリサイズした時の動作が直感的に感じられるようになる。

StableColumnsを使用する

XMonadの設定ファイルはデフォルトではGHCによって直接コンパイルされる。この時、GHCにはコマンドラインオプションとして-ilibが与えられるので、設定ディレクトリ内のlibにHaskellソースを置くことで外部モジュールを読み込むことができる。

したがって、StableColumnsのようなパッケージ管理されていない外部モジュールを使用するには、libディレクトリにソースを直接コピーすればいい。

$ mkdir -p ~/.xmonad/lib/XMonad/Layout
$ curl https://raw.githubusercontent.com/emonkak/config/master/xmonad/lib/XMonad/Layout/StableColumns.hs -o ~/.xmonad/lib/XMonad/Layout/StableColumns.hs

XMonad.Layout.StableColumnsがimportできるようになったなら、レイアウトは以下ように定義できる。

import XMonad.Layout.StableColumns

-- 3カラムレイアウト
threeColumnLayout = stableColumns
  (3/100)  -- 列方向のリサイズ量
  ((3/100) * (16/9))  -- 行方向のリサイズ量
  [ staticColumn 1 (1/2) []  -- 多くとも1つのウィンドウを表示
  , dynamicColumn 1 (1/4) []  -- 少なくとも1つのウィンドウを表示
  , dynamicColumn 3 (1/4) []  -- 少なくとも3つのウィンドウを表示
  ]

-- 2カラムレイアウト
twoColumnLayout = stableColumns
  (3/100)  -- 列方向のリサイズ比率
  ((3/100) * (16/9))  -- 行方向のリサイズ比率
  [ staticColumn 1 (1/2) []  -- 多くとも1つのウィンドウを表示
  , dynamicColumn 1 (1/4) []  -- 少なくとも1つのウィンドウを表示
  ]
StableColumnsのレイアウト定義例

レイアウトに対してメッセージを送信するキーバインドは以下のように定義できる。ShrinkRowExpandRowStableColumnsが独自に定義したResizeRow型のコンストラクターで、他のメッセージはXMonad.Layoutからimportしたものだ。

import XMonad
import XMonad.Layout.StableColumns
import qualified Data.Map as M

myLayoutKeys conf@(XConfig { XMonad.modMask = modMask }) = M.fromList $
  [ ((modMask,               xK_comma),  sendMessage $ IncMasterN (-1))  -- 列のキャパシティを1減らす
  , ((modMask,               xK_period), sendMessage $ IncMasterN (1))  -- 列のキャパシティを1増やす
  , ((modMask,               xK_h),      sendMessage Shrink)  -- 列を縮小する
  , ((modMask,               xK_l),      sendMessage Expand)  -- 列を拡大する
  , ((modMask .|. shiftMask, xK_h),      sendMessage ShrinkRow)  -- 行を縮小する
  , ((modMask .|. shiftMask, xK_l),      sendMessage ExpandRow)  -- 行を拡大する
  ]
メッセージ送信のキーバインド設定例

まとめ

XMonadではxmonad-contribを利用することで様々なレイアウトを利用することができるが、細部で自分の好みに合わない所がある場合もある。XMonadのレイアウト関数はRectangleWindowのペアの一覧を返すだけの単純なものなので、Haskellが書けるなら(それが難しいかもしれないが)レイアウトをフォークして改造することは容易だ。StableColumnsの実装もThreeColumnsをフォークする所から始まった。

今回のStableColumnsについてもあくまで作者の使い方に合うよう作られた実装なので、読者の好みには合わない部分があるかもしれない。それでも、今回の実装が改造のベースとして役立てれば嬉しい。

筆者のデスクトップ(XMonad on Gentoo Linux)
筆者のデスクトップ(XMonad on Gentoo Linux)

You may also like...

  • Vimのfoldexprで新たに折り畳みを定義する方法とその活用法

    Vimで十分に活用されていない組み込み機能の一つは折り畳み(Folding)だと思います。私は以前から折り畳みをいくつかの方法で活用しており、Vimのお気に入りの機能の一つになっています。この記事では、私がどのように折り畳みを設定しているのか、さらには折り畳みを活用するためのプラグインについてみなさんに共有します。

  • A keyboard-oriented system tray for X11 tiling window managers

    Typically, tiling window managers like XMonad do not have a system tray. If you want a system tray, you can use a standalone implementation like stalonetray or a status bar implementation with built-in system tray support, such as polybar.

  • Why you should use wcwidth() to calculate a character width

    Character width depends on the font, and Unicode Consortium does not provide explicit width definitions for all characters. There are characters that have ambiguous widths other than those defined as "Ambiguous (A)" in EastAsianWidth.txt. For example, "☀ (U+2600)" is defined as "Neutral (N)" in EastAsianWidth.txt, but its width may be full-width in a CJK font or an emoji font. This means that a non-East Asian character may be also the ambiguous-width character. Character width tables in default locales is problematic for both CJK and non-CJK users. You can create a custom locale to define a better character width table. wcwidth() respects defined by the locale. All TUI applications should consistently use wcwidth() to calculate the width of the character without an embedded character width table. If there is a mismatch in character width between applications, the screen will be broken.

  • 調和の取れたカラーパレット生成をデザインシステムに向けて

    近年、サービス全体の一貫性を確保するためにデザインシステムを構築する例が多い。この時、特に難しいのがカラーパレットの設計で、その理由は以下のような要件があるからだ。