Xmonad, mon nouveau window manager

Une nouvelle catégorie d'articles : les Dotfiles !

Pour ceux qui ne savent pas, les fichiers de configuration propres aux utilisateurs sous GNU/Linux sont planqués dans leur /home sous la forme de fichiers (et répertoires) cachés. Hors, comme il suffit de précéder le nom du fichier par un "." pour le cacher, on les appelle les dotfiles (dot voulant dire point en anglais).

Donc dans cette catégorie, vous trouverez des fichiers de configuration pour mes applications que je me suis amusé à customiser. On va commencer par un bon gros paquet de fichiers : mon bureau !

Mon nouvel environnement de bureau

Comme je l'ai déjà évoqué précédemment, j'ai changé d'environnement de bureau et me suis tourné vers Xmonad.

Il s'agit, comme Wmii et d'autres, d'un tiling window manager, ce qui veut dire en gros qu'il gère la disposition des fenêtres de manière optimale. Lorsque j'ouvre une nouvelle fenêtre (un terminal, Iceweasel, Icedove, ce que vous voulez), il ne la met pas à la dernière taille et place utilisées mais de façon à ce que, collée aux autres fenêtres, tout l'écran soit pris. Donc plus de fenêtre dont un bout (forcément important) est caché par une autre application ou hors du bureau virtuel (enfin, si, on peut le faire mais il faut faire exprès).

Un petit screenshot :

Il faut aussi dire que, de par sa légéreté, Xmonad est très, très réactif !

Tout ça pour dire que je le kiffe (merci à Clém de me l'avoir fait découvrir).

Bon par contre, ça n'est qu'un window manager, rien d'autre. Il faut donc le meubler un peu, ce que j'ai fait avec dmenu, trayer et dzen2 pour faire tenir le tout dans une barre. Les informations du son, de la batterie, etc. ne sont qu'un simple script shell pipé dans dzen2.

Le terminal est rxvt-unicode, plus connu sous le nom de urxvt et le logiciel permettant d'avoir un fond d'écran s'appelle nitrogen.

L'installation

Bon, tout le monde commence à avoir l'habitude : un bon petit apt-get et roule ma poule !

# apt-get install ghc6 xmonad dwm-tools trayer rxvt-unicode nitrogen

Je crois qu'il y a là tout ce qu'il faut. Si ça ne marche pas comme il faut, essayer d'installer les paquets libghc6..., c'est peut-être ça qu'il manque.

La configuration du bureau

Là c'est plutôt funky parce que le fichier de configuration est en Haskell ! Je vous avouer, je n'y comprends pas grand chose mais on devine ce qu'il faut faire en allant voir les exemples de configuration sur http://haskell.org/haskellwiki/Xmonad/Config_archive.

Sinon, vous pouvez toujours prendre le mien et le mettre dans ~/.xmonad/xmonad.hs :

-- Imports {{{
-- A lot of imports. Actually, I don't know which of them are truely useful
import XMonad
-- Layouts
import XMonad.Layout
import XMonad.Layout.NoBorders
import XMonad.Layout.PerWorkspace
import XMonad.Layout.LayoutHints
import XMonad.Layout.ThreeColumns
import XMonad.Layout.ToggleLayouts
-- Hooks
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.DynamicLog (PP(..), dynamicLogWithPP, wrap, defaultPP)
import XMonad.Hooks.UrgencyHook
-- Actions
import XMonad.Actions.CycleWS
import XMonad.Actions.CycleRecentWS
import XMonad.Actions.CopyWindow
import XMonad.Actions.DwmPromote
import XMonad.Actions.GridSelect
import XMonad.Actions.MouseResize
import XMonad.Actions.OnScreen
import XMonad.Actions.SinkAll
import XMonad.Actions.WindowGo
import XMonad.Util.Run (spawnPipe)
import qualified XMonad.StackSet as W
import qualified Data.Map as M
import Data.Bits ((.|.))
import System.IO (hPutStrLn)
-- }}}

-- Control Center {{{
-- Colour scheme {{{
myNormalBGColor = "#2e3436"
myFocusedBGColor = "#414141"
myNormalFGColor = "#babdb6"
myFontColor = "#73d216"
myFocusedFGColor = "#73d216"
myUrgentFGColor = "#f57900"
myUrgentBGColor = myNormalBGColor
mySeperatorColor = "#2e3436"
-- }}}

-- Icon packs can be found here:
-- http://robm.selfip.net/wiki.sh/-main/DzenIconPacks
myBitmapsDir = "/home/luc/.share/icons/dzen"
myFont = "-misc-fixed-*-*-*-*-10-*-*-*-*-*-iso10646-1"
-- }}}

-- Workspaces {{{
myWorkspaces :: [WorkspaceId]
myWorkspaces = map show [1..9 :: Int]
-- }}}

-- Define Modifier key
-- mod1Mask : Alt, mod4Mask : Super
-- myModMask = mod1Mask
myModMask = mod4Mask

-- Define terminal
myTerminal = "urxvt -pe tabbed"
-- Define borderwidth
myBorderWidth = 1

-- Keybindings {{{
myKeys conf@(XConfig {modMask = modm}) = M.fromList $
[
((modm , xK_p), spawn ("exec `dmenu_path | dmenu -fn '" ++ myFont ++ "' -nb '" ++ myNormalBGColor ++ "' -nf '" ++ myNormalFGColor ++ "' -sb '" ++ myFocusedBGColor ++ "' -sf '" ++ myFontColor ++ "'`"))
, ((modm , xK_i), spawn ("exec iceweasel"))
, ((modm , xK_f), sendMessage ToggleStruts >> sendMessage ToggleLayout)
, ((modm , xK_n), spawn ("liferea"))
, ((modm , xK_m), spawn ("icedove"))
, ((0 , 0x1008ff59), spawn ("xrandr --auto"))
, ((0 , 0x1008ff11), spawn ("amixer -q set Master 1- unmute"))
, ((0 , 0x1008ff13), spawn ("amixer -q set Master 1+ unmute"))
, ((0 , 0x1008ff12), spawn ("amixer -c 0 set Master toggle"))
]
++

-- Remap switching workspaces to M-[&é"'(-è_ç]
-- I get these codes with xev. With this codes, I'm sure to get the key I want
[((m .|. modm, k), windows $ f i)
| (i, k) <- zip (XMonad.workspaces conf) [0x26,0xe9,0x22,0x27,0x28,0x2d,0xe8,0x5f,0xe7]
, (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]]
-- }}}

statusBarCmd= "dzen2 -p -h 16 -ta l -bg '" ++ myNormalBGColor ++ "' -fg '" ++ myNormalFGColor ++ "' -w 300 -sa c -fn '" ++ myFont ++ "'"

-- Main {{{
main = do
statusBarPipe <- spawnPipe statusBarCmd
xmonad $ withUrgencyHook dzenUrgencyHook { args = ["-bg", "darkgreen", "-xs", "1"] } $ defaultConfig {
modMask = myModMask
, borderWidth = myBorderWidth
, terminal = myTerminal
, normalBorderColor = myNormalBGColor
, focusedBorderColor = myFocusedFGColor
, manageHook = manageHook defaultConfig <+> myManageHook
, layoutHook = smartBorders $ avoidStruts $ toggleLayouts Full $ layoutHook defaultConfig
, workspaces = myWorkspaces
, logHook = dynamicLogWithPP $ myPP statusBarPipe
, keys = \c -> myKeys c `M.union` keys defaultConfig c
}
-- }}}

-- Window rules (floating, tagging, etc) {{{
myManageHook = composeAll . concat $
[ -- The applications that go to Workspace 1
-- [ className =? b --> doF (W.shift "1") | b <- myClassWebShifts]
-- The applications that float
[ className =? i --> doFloat | i <- myClassFloats]
-- The applications to ignore
, [ className =? j --> doIgnore | j <- myClassIgnore]
, [ title =? k --> doIgnore | k <- myTitleIgnore]
]
where
-- The applications that go to Workspace 1
-- myClassWebShifts = ["Iceweasel", "Liferea"]
myClassFloats = ["Mplayer", "Gimp"]
myClassIgnore = ["stalonetray", "trayer"]
myTitleIgnore = ["xfce4-notifyd"]
-- }}}

-- Dzen Pretty Printer {{{
-- Stolen from Rob [1] and modified
-- [1] http://haskell.org/haskellwiki/Xmonad/Config_archive/Robert_Manea%27s_xmonad.hs
myPP handle = defaultPP {
ppCurrent = wrap ("^fg(" ++ myFontColor ++ ")^bg(" ++ myFocusedBGColor ++ ")^p(4)") "^p(4)^fg()^bg()",
ppUrgent = wrap ("^fg(" ++ myUrgentFGColor ++ ")^bg(" ++ myUrgentBGColor ++ ")^p(4)") "^p(4)^fg()^bg()",
ppVisible = wrap ("^fg(" ++ myNormalFGColor ++ ")^bg(" ++ myNormalBGColor ++ ")^p(4)") "^p(4)^fg()^bg()",
ppSep = "^fg(" ++ mySeperatorColor ++ ")^r(3x3)^fg()",
ppLayout = (\x -> case x of
"Tall" -> " ^i(" ++ myBitmapsDir ++ "/tall.xbm) "
"Mirror Tall" -> " ^i(" ++ myBitmapsDir ++ "/mtall.xbm) "
"Full" -> " ^i(" ++ myBitmapsDir ++ "/full.xbm) "
"ThreeCol" -> " ^i(" ++ myBitmapsDir ++ "/threecol.xbm) "
"Hinted Tall" -> " ^i(" ++ myBitmapsDir ++ "/tall.xbm) "
"Hinted Mirror Tall" -> " ^i(" ++ myBitmapsDir ++ "/mtall.xbm) "
"Hinted Full" -> " ^i(" ++ myBitmapsDir ++ "/full.xbm) "
"Hinted ThreeCol" -> " ^i(" ++ myBitmapsDir ++ "/threecol.xbm) "
_ -> " " ++ x ++ " "
),
ppTitle = wrap ("^fg(" ++ myFontColor ++ ")") "^fg()" ,
ppOutput = hPutStrLn handle
}
-- }}}

Je pense que vous arriverez à comprendre quelle partie de code fait quoi (sauf peut-être la partie des layouts, mais je n'ai pas cherché à comprendre moi non plus). Pour connaître les combinaisons de touches par défaut, je ne peux que vous encourager à consulter la cheat sheet Xmonad.

Bon, maintenant qu'on a un beau fichier de conf, on peut le compiler avec

$ xmonad --recompile

Ok, ça c'est fait, Xmonad est prêt... mais pas le reste

Configuration de la barre d'informations

Pour avoir une belle barre pleine d'informations (et surtout avec les bureaux et le titre de la fenêtre en cours), il va falloir mettre un peu les mains dans le cambouis (encore !).

Sur mon screenshot, vous pouvez voir un ensemble d'information comme l'état de la batterie, le volume, etc. Tout ceci est généré par un script shell dont le résultat est envoyé dans dzen. Et juste à côté, il y a une zone de notification (le tray) qui me permet de toujours avoir mes applications préférées sous la main.

En gros, vous pouvez afficher ce que vous voulez, du moment que vous pouvez l'obtenir par des commandes du terminal.

Pour le mien (oui, il est en zsh, c'est comme ça qu'était le script de base que j'ai récupéré et je me suis pas amusé à essayer de voir si ça marchait toujours en bash), c'est :

#!/bin/zsh

# main interval in seconds
INTERVAL=0.5

# path to the icons
DZEN_ICONPATH='/home/luc/.share/icons/dzen'

# colors definition
RED='#FF0000'
ORANGE='#FF9900'
GREEN='#00FF00'
WHITE='#FFFFFF'

#SEPARATOR='^p(3)^r(3x3)^p(3)'
SEPARATOR=' - '
###################
## Date and Hour ##
###################

HOUR_FORMAT='%H:%M:%S'
DATE_FORMAT='%A %d %B %Y'

fdate() {
date +$DATE_FORMAT
}

fhour() {
date +$HOUR_FORMAT
}

##############
## Cpu temp ##
##############

fcputemp() {
# print -n `sensors | grep -A 1 'Core0 Temp' | cut -c15-16 | sed '/^$/d'`
TEMP=`sensors | grep -A 1 'temp1' | cut -c15-16 | sed '/^$/d'`
if [[ $TEMP > 69 ]]
then
ICOTEMP="${DZEN_ICONPATH}/temp_high.xbm"
TEMPCOLOR=$RED
else
if [[ $TEMP > 54 ]]
then
ICOTEMP="${DZEN_ICONPATH}/temp_med.xbm"
TEMPCOLOR=$ORANGE
else
ICOTEMP="${DZEN_ICONPATH}/temp_low.xbm"
TEMPCOLOR=$GREEN
fi
fi
print -n `echo "^fg(${TEMPCOLOR})^i($ICOTEMP)^fg() $TEMP C"`
}

##################
## Volume gauge ##
##################

fvolume() {
if [[ `amixer -c0 get Master | awk '/^ Mono/ { print $6 }'| sed -e "s/\[//" | sed -e "s/\]//"` == "on" ]]
then
VOLUME=`amixer -c0 get Master | awk '/^ Mono/ { print $4 }' | sed -e "s/\[//" | sed -e "s/\]//" | sed -e "s/%//"`
else
VOLUME=0
fi
if [[ $VOLUME > 66 || $VOLUME == 100 ]]
then
ICOVOLUME="${DZEN_ICONPATH}/vol-hi.xbm"
else
if [[ $VOLUME > 33 ]]
then
ICOVOLUME="${DZEN_ICONPATH}/vol-med.xbm"
else
if [[ $VOLUME > 0 ]]
then
ICOVOLUME="${DZEN_ICONPATH}/vol-low.xbm"
else
ICOVOLUME="${DZEN_ICONPATH}/vol-mute.xbm"
fi
fi
fi
print -n `echo "^i($ICOVOLUME) ${VOLUME}%"`
}

###############
## Bandwidth ##
###############

# network interface
finterface() {
if [[ `sudo ethtool eth0 | awk '/Link detected/ { print $3 }'` == "no" ]]
then
if [[ `sudo iwconfig wlan0 | awk '/ESSID:off/ { print $4 }'` == "ESSID:off/any" ]]
then
LINK=0
INTERFACE=""
else
INTERFACE="wlan0"
LINK=1
fi
else
INTERFACE="eth0"
LINK=1
fi
}

fbwup() {
if [[ $LINK == 1 ]]
then
BW_UP=`ifstat -i $INTERFACE 0.1 1 | awk '/[0-9]*\.[0-9]*/ { print $2 }'`
else
BW_UP=""
fi
}

fbwdown() {
if [[ $LINK == 1 ]]
then
BW_DOWN=`ifstat -i $INTERFACE 0.1 1 | awk '/[0-9]*\.[0-9]*/ { print $1 }'`
else
BW_DOWN=""
fi
}

#############
## Battery ##
#############
fac() {
if [[ `acpi -a | awk '/Adapter/ { print $3 }'` == "on-line" ]]
then
ICOAC="${DZEN_ICONPATH}/ac.xbm"
else
ICOAC=""
fi
print -n `echo "^i($ICOAC)"`
}

fbat() {
BAT=`acpi -b | awk '/Battery/ { print $4 }' | sed -e "s/[%,]//g"`
BAT=$(print -n $BAT)
if [[ $BAT > -1 ]]
then
if [[ $BAT > 70 || $BAT == 100 ]]
then
ICOBAT="${DZEN_ICONPATH}/battery_full.xbm"
BATCOLOR=$GREEN
BAT="${BAT}%"
else
if [[ $BAT > 45 ]]
then
ICOBAT="${DZEN_ICONPATH}/battery_good.xbm"
BATCOLOR=$ORANGE
BAT="${BAT}%"
else
if [[ $BAT > 10 ]]
then
ICOBAT="${DZEN_ICONPATH}/battery_low.xbm"
BATCOLOR=$RED
BAT="${BAT}%"
else
ICOBAT="${DZEN_ICONPATH}/battery_empty.xbm"
BATCOLOR=$RED
BAT="${BAT}%"
fi
fi
fi
print -n `echo " ^fg(${BATCOLOR})^i($ICOBAT)^fg() ${BAT}"`
else
print -n ""
fi
}

##########
## Main ##
##########

while true; do
HOUR=$(fhour)
DATE=$(fdate)
CPUTEMP=$(fcputemp)
VOLUME=$(fvolume)
finterface
fbwup $INTERFACE
fbwdown $INTERFACE
AC=$(fac)
PBAT=$(fbat)

print "${AC} ${PBAT} ${SEPARATOR} ${VOLUME} ${SEPARATOR} ${INTERFACE}: ^fg(${GREEN})^p(3)^i(${DZEN_ICONPATH}/arr_down.xbm)^fg(white)${BW_DOWN} kB/s^fg(${RED})^i(${DZEN_ICONPATH}/arr_up.xbm)^fg(white)${BW_UP} kB/s^fg() ${SEPARATOR} ${CPUTEMP} ${SEPARATOR} ${HOUR} ${SEPARATOR} ${DATE}"

sleep $INTERVAL
done

C'est bien beau, mais comment je fais pour mettre tout ça dans la barre ?

Patience, jeune Padawan !

Maintenant, il faut piper ce résultat par le programme dzen. Moi je le fais dans le .xinitrc, d'autres dans le script lui-même. C'est une question de goût.

Mon .xinitrc :

#!/bin/sh
#
# ~/.xinitrc
#
# Executed by startx (run your window manager from here)
#

# Start URxvt daemon so we can quickly open the other clients
urxvtd -q -o -f
[[ -x "/usr/bin/unclutter" -a -z "`pidof unclutter`" ]] && \
unclutter -idle 5 -root&

# Set Wallpaper with nitrogen
eval `nitrogen --restore` &

# Set mouse cursor and background colour
xsetroot -cursor_name left_ptr -solid '#090909' &

# Launch tray and statusbar
~/.dzen.sh | dzen2 -e 'onstart=lower' -p -ta r -fn '-misc-fixed-*-*-*-*-8-*-*-*-*-*-*-*' -bg '#2e3436' -fg '#babdb6' -h 16 -x 300 -y 0 -w 624 &
trayer --edge top --align right --widthtype pixel --width 100 --SetPartialStrut true --SetDockType true --heighttype pixel --height 16 --tint 0x2e3436 --transparent true --alpha 0 &
# Launch applications on boot :
xfce4-clipman &
icedove &
liferea &
irexec &
xfce4-notifyd &
wicd-client &
# Launch WM
exec xmonad

Vous pouvez voir dans ce script le lancement d'un certain nombre d'applications :

  • le démon urxvt (le terminal),
  • unclutter qui permet de cacher la souris au bout de 5 secondes,
  • nitrogen pour avoir un fond d'écran,
  • ~/.dzen.sh pour la barre d'infos,
  • trayer pour la barre de notification,
  • xfce4-clipman pour l'historique des copier,
  • icedove pour mes mails,
  • liferea pour mes flux rss,
  • irexec pour ma télécommande (voir ce post)
  • xfce4-notifyd pour recevoir les notifications de mes applis (genre : nouveau mail, flux mis à jour, etc.)
  • wicd-client pour le network-manager

Configuration d'Urxvt

Plus qu'à configurer Urxvt et c'est fini ! Mettez ceci dans le .Xdefaults :

URxvt*transparent:true
URxvt*tintColor:white
URxvt*shading:30
URxvt*saveLines:12000
URxvt*foreground:White
URxvt*background:black
URxvt*font:xft:DejaVu Sans Mono:pixelsize=11
URxvt*scrollBar:false
URxvt*scrollBar_right:false
URxvt*scrollstyle:rxvt
URxvt*fading:30
URxvt*iso14755_52:false
URxvt*perl-ext-common:default,matcher
URxvt*urlLauncher:iceweasel
URxvt.colorUL:#86a2be
URxvt.keysym.Control-Up:\033[1;5A
URxvt.keysym.Control-Down:\033[1;5B
URxvt.keysym.Control-Left:\033[1;5D
URxvt.keysym.Control-Right:\033[1;5C
URxvt.keysym.M-Down:\033[1;3B
URxvt.keysym.M-Up:\033[1;3A
URxvt.keysym.M-Left:\033[1;3D
URxvt.keysym.M-Right:\033[1;3C

Beaucoup de trucs ésotériques dans ce fichier mais retenez bien les 8 derniers : il permettent d'avancer/reculer d'un mot dans vim et dans le terminal avec Ctrl+flèche. Sans ça, point de salut ! Et pour avoir utilisé mon pc pendant quelques mois sans cette fonctionnalité, je peux vous dire qu'elle est vraiment utile !

Conclusion

Ouf ! Si ce n'est pas le plus long post que j'ai jamais fait, il est au moins dans le top 3 ! Ça doit bien faire un mois que je le prépare et puis, vous savez ce que c'est, on rentre du boulot, on est crevé, on a envie de rien faire...

J'espère que cet article aura donné envie à quelques un de tester Xmonad et de faire mumuse avec. Je suis conscient que ma configuration et mes scripts sont perfectibles mais là, sur mon netbook, ils sont amplement suffisants.

Ah ! J'oubliais ! Il y a un problème relativement génant dans ma configuration (ou alors c'est un bug, je n'en suis pas sûr) : pas moyen d'afficher une vidéo flash en plein écrant à partir d'internet ! (Mplayer le fait très bien.) Sachant que de toute façon, flash çapusaypalibre, ça ne me dérange pas trop, mais je ne voudrais pas que des testeurs de ma conf me reproche ce comportement. D'ailleurs, si quelqu'un a une idée (moi j'ai rien trouvé), qu'il me le fasse savoir.

Amusez-vous bien.

Mes fichiers :

EDIT : Apparemment, la conf que j'avais mis ne compilait pas. J'ai remanié et j'ai remis la bonne version. Et aussi, pensez à vous baser sur les fichiers plutôt que sur ce qui est écrit : la remise en page est plutôt fastidieuse pour ce genre de chose dans Dotclear dont peut amener un certain nombre d'erreurs.

RE-EDIT : Je viens de passer à WordPress pour mon moteur de blog et je n'ai pas trouvé comment lui faire accepter des fichiers sans extensions, donc le .xinitrc et le .Xdefaults ont une extension en .sh si vous les téléchargez. Pensez à les renommer.