Operating on numbers with emacs hydra

Operating on numbers in place in any emacs buffer is one of the cool features that no one thinks they need, but is surprisingly handy, especially for converting bit-masks between decimal and hexadecimal during programming. Akinori Musha's operate-on-number package makes it very easy to set it up with a single keybinding. The only minor issue is that the instructions for setting it up refer to smartrep while my preferred package for setting up such keybindings is hydra. Assuming a recent enough version of hydra (where an escaping bug has been fixed), here is the elisp code needed to set up operate-on-number using hydra:

(defhydra hydra-operate-on-number ()
  "
Arithmetic operations: + - * /
Remainder: \\
Exponent: \\^
Arithmetic shift: < >
Base conversion: b o x X #
Format: %%%%
"
  ("+" apply-operation-to-number-at-point)
  ("-" apply-operation-to-number-at-point)
  ("*" apply-operation-to-number-at-point)
  ("/" apply-operation-to-number-at-point)
  ("\\" apply-operation-to-number-at-point)
  ("^" apply-operation-to-number-at-point)
  ("<" apply-operation-to-number-at-point)
  (">" apply-operation-to-number-at-point)
  ("b" apply-operation-to-number-at-point :exit t)
  ("o" apply-operation-to-number-at-point :exit t)
  ("x" apply-operation-to-number-at-point :exit t)
  ("X" apply-operation-to-number-at-point :exit t)
  ("#" apply-operation-to-number-at-point :exit t)
  ("%" apply-operation-to-number-at-point :exit t))

The only trick is to format the help string correctly with all the escape sequences required by hydra. I bind the generated function hydra-operate-on-number/body to M-g M-d.

Firestarter with project root directories

firestarter is an emacs package used for running arbitrary commands upon saving a file. Out of the box, it comes with support for running shell commands, elisp functions and arbitrary emacs lisp code. The most interesting and likely most common use case is to run shell commands. firestarter provides an interesting format specification for shell commands, e.g., %f is replaced by the name of the file backing the buffer, %p by the path to the file, etc.

As someone working with embedded devices for most of which are connected to some remote machine in some lab, and as one who prefers to edit code within emacs from files on a local filesystem (see below for reasons), I often run into the issue of copying local edits to a remote machine during normal edit-compile-run cycles. So my ideal set up would have been to use firestarter to copy the files over every time any files in such a project were saved from emacs. A directory local variable with contents along the lines of

(firestarter . "scp %p remotemachine:/path/to/project/%r")

in the root directory of the project would have been great, if %r could represent the path of the file relative to the project root. Unfortunately, firestarter does not provide such a format specifier. However, since this is emacs, it is easy to whip up a function to do what we want:

(defun ravi/find-path-name-relative-to-project-root ()
  "Find path of current buffer file relative to project root"
  (interactive)
  (let* ((root-dir (and (buffer-file-name)
                        (or (and (fboundp 'magit-get-top-dir) (magit-get-top-dir))
                            (and (fboundp 'vc-root-dir) (vc-root-dir))
                            (locate-dominating-file (buffer-file-name) ".git")
                            (locate-dominating-file (buffer-file-name) dir-locals-file))))
         (relative-name (and root-dir
                             (file-relative-name (buffer-file-name) root-dir))))
    relative-name))

(defun ravi/apply-shell-command-relative-to-project (command-template)
  "Apply shell command relative to project

COMMAND-TEMPLATE should be a string with the following substitutions allowed:

%p: full path to file name

%r: path relative to project root

%f: file name

%N: unquoted relative path name (should be used extremely carefully)
"
  (interactive "sEnter command template: ")
  (let* ((path (shell-quote-argument (or (buffer-file-name) "")))
         (rel-name (ravi/find-path-name-relative-to-project-root))
         (rel-path (shell-quote-argument (or rel-name "")))
         (file (shell-quote-argument (file-name-nondirectory (or path ""))))
         (cmd (and rel-name
                   (format-spec command-template
                                (format-spec-make ?p path ?r rel-path ?f file ?N rel-name)))))
    (if cmd
        (shell-command cmd)
      (if (not (buffer-file-name))
          (message "Not in a file buffer")
        (message "Unable to find project root")))))

Writing error messages and docstrings was actually harder than writing the code itself. There are two interesting bits:

  • try magit-get-top-dir before other methods to get the root directory since it works with files in the project yet to be added to version control
  • use dir-locals-file (normally .dir-locals.el) to set project root in case everything else fails

It is trivial to extend this to many other version control systems. With the above, we can use firestarter to copy files:

(firestarter . (ravi/apply-shell-command-relative-to-project
                "scp %p remotemachine:/path/to/project/%r"))

(The code can be found in my .emacs repository.)

Of course, one could say that remote editing with emacs tramp accomplishes the same goal by editing remote files. However, for large projects, essential tools such as helm-ls-git, git-messenger, and helm-ag are far more responsive and usable with local filesystems. A lot of the remote systems I use have ancient versions of tools which are painful to even contemplate using on a regular basis.

Make company-yasnippet play nice

Company mode is an excellent completion mechanism for emacs, much better than its documentation would lead one to believe. The best keybinding for company mode completion to common prefix is TAB, which, however, is also the best keybinding for yasnippet. The simplest solution would seem to be to use company mode to drive yasnippet. Such a back end for company mode already exists: company-yasnippet bundled with company mode. By default, company-yasnippet is not enabled since it is painful to use with other back ends. My solution has been to bind shift+TAB (called <backtab> in emacs keymap lingo) to company-yasnippet and not enable it by default, but the problem is that the default company mode completion is activated after typing in a snippet prefix, and sets up a transient keymap that simply ignores shift+TAB. We can work around the problem with a little bit of elisp:

(defun ravi/company-to-yasnippet ()
  (interactive)
  (company-abort)
  (call-interactively 'company-yasnippet))
(bind-key "<backtab>" 'ravi/company-to-yasnippet company-active-map)
(bind-key "<backtab>" 'company-yasnippet)

The trick is that we need to call company-abort before we call company-yasnippet, but only perform this extra step when company mode completion is active.

Emacs dummy headers mode without Objective C

Choosing between C mode and C++ mode in emacs for header files can be a bit of a chore if both types of header files have a .h extension. As usual, a package for solving this problem already exists: dummy-h-mode. One of the heuristics that it uses is to search for an implementation file and then to check the extension of that implementation file for a hint on the mode. This heuristic works very well, but with a minor annoyance that it tries to handle Objective C headers as well. In my use case, I sometimes have purely template-based C++ code and a test implementation in Matlab or octave:

mycode.h
mycode.m

dummy-h-mode unfortunately parses the Matlab/octave file as an Objective C file. Since I do not use Objective C, I used a quick and dirty hack to avoid it:

(defun ravi/do-not-allow-objc (mode-val)
  (if (eq mode-val 'objc-mode)
      nil
    mode-val))
(mapc (lambda (x) (advice-add x :filter-return #'ravi/do-not-allow-objc))
      '(dummy-h-mode-get-major-mode-by-source-file
        ;dummy-h-mode-get-major-mode-by-keywords
        dummy-h-mode-get-major-mode-by-files-directory))

We could simply avoid checking for .m files, but that would involve intrusive changes to dummy-h-mode. Instead, in the code above, we look at the result of obtaining the major mode by one of the file-based methods (checking the implementation, or checking the number of files), and set it to nil if the result is Objective C. When one method returns nil, dummy-h-mode goes on to the next heuristic. In my case, looking at keywords is almost always sufficient since most of my C++ code is in some namespace which clearly indicates the correct mode.

Text-based email setup with mbsync and mu

Composing mail without the assistance of emacs is painful for serious emacs users. KMail (supplemented with emacsclient) had been my email client of choice for more than a decade. While KMail continues to be an excellent mail client, I started looking more into moving my entire email flow into emacs. Sending mail from emacs is pretty easy with smtpmail; so the only issue was finding a way to fetch and display email. After trawling The Interwebz, mu4e seemed to fit in best with my workflow.

Initial setup

Setting up mu4e was pretty easy since its parent package mu can be set up as a git submodule (or as a git subtree) of an emacs initialization directory and can then be compiled in place. Most of the initial setup is straightforward since the built in manual is pretty comprehensive; Rob Stewart's excellent guide is a handy summary for the impatient. The only other initial setup choice was between offlineimap and mbsync. After initial attempts at setting up both, I ended up choosing mbsync.

Fetching email with mbsync

While the mbsync man page is pretty comprehensive, it can be a little intimidating to set it up for use with mu4e. The basic setup for one email account is similar to the following:

IMAPAccount MyAccountName
Host imap.gmail.com
User MyAccountName@gmail.com
PassCmd "gpg2 -q --for-your-eyes-only --no-tty -d ~/.authinfo.gpg | awk '/machine imap.gmail.com login MyAccountName@gmail.com/ {print $NF}'"
# Use SSL
UseIMAPS yes
CertificateFile /etc/pki/tls/certs/ca-bundle.crt

IMAPStore MyAccountName-remote
Account MyAccountName

MaildirStore MyAccountName-local
# The trailing "/" is important
Path ~/.mail/MyAccountName/
Inbox ~/.mail/MyAccountName/Inbox

Channel MyAccountName
Master :MyAccountName-remote:
Slave :MyAccountName-local:
# Exclude everything under the internal [Gmail] folder, except the interesting folders
Patterns * ![Gmail]* "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail" "[Gmail]/Drafts"
# Or include everything
#Patterns *
# Automatically create missing mailboxes, both locally and on the server
Create Both
# Save the synchronization state files in the relevant directory
SyncState *

The set up above is typical for Google mail. There are a couple of interesting bits, however. First, the use of SSL requires that certificate bundles be available to mbsync. On Fedora Linux 19 and later, the easiest way is to install ca-certificates.noarch using the package manager. The location of installed certificates is provided to mbsync via the CertificateFile directive.

The second and more interesting part is the handling of passwords.

Using GPG for mbsync passwords

The basic idea is that every time a password is needed, an particular file is decrypted and loaded. The key for the decryption can be prompted for and be stashed by gpg-agent. The first step is to create a GPG key, which is covered very well elsewhere. The standard authentication mechanism for gnus and smtpmail can be reused to store login information for mbsync. For any one account, the password for IMAP access and the password for sending email (usually the same) can be added to ~/.authinfo.gpg:

machine imap.gmail.com login MyAccountName@gmail.com password MYPASSWORD
machine smtp.gmail.com login MyAccountName@gmail.com password MYPASSWORD

The first line is used by mbsync and the second by smtpmail. The line:

PassCmd "gpg2 -q --for-your-eyes-only --no-tty -d ~/.authinfo.gpg | awk '/machine imap.gmail.com login MyAccountName@gmail.com/ {print $NF}'"

automatically fetches the password for the given account from authinfo.gpg. It is easy now with a couple of simple modifications to ~/.gnupg/gpg-agent.conf to make sure that the password prompt appears under X and that the key is stashed:

pinentry-program /usr/bin/pinentry-qt4
default-cache-ttl 720000
max-cache-ttl 720000

The stash time for passwords is set to 200 hours.

Initial mu4e setup

I chose to set up mu4e sources as a git submodule in my emacs directory with path site-lisp/mu, which necessitated the following changes to my emacs start up code:

(add-to-list 'Info-additional-directory-list (ravi/emacs-file "site-lisp/mu/mu4e"))
(setq mu4e-mu-binary (ravi/emacs-file "site-lisp/mu/mu/mu"))

The next part ensures that the main mu4e window is quickly accesible from a key binding without opening duplicate buffers:

(defun ravi/switch-to-mu4e ()
  (interactive)
  (let ((buf (or (and (boundp 'mu4e~headers-buffer-name)
                       (get-buffer mu4e~headers-buffer-name))
                 (get-buffer "*mu4e-main*"))))
    (if buf
        (if (get-buffer-window buf t)
            (select-window (get-buffer-window buf t))
          (switch-to-buffer buf))
      (mu4e))))
(bind-key "C-'" 'ravi/switch-to-mu4e)

That's all for now. We will cover setting up multiple accounts with different polling intervals in a future post.

Setting up optimized ATLAS libraries for development on Fedora 17

Fedora 17 (Beefy Miracle) provides optimized BLAS and LAPACK libraries using ATLAS. ATLAS libraries should be tuned to the machine on which they are used. However, as Fedora supports a wide variety of hardware with the same x86_64 binaries, a more complex mechanism is needed for enabling optimized ATLAS libraries. The scheme consists of installing two ATLAS packages:

  • generic package (atlas and atlas-devel) for unoptimized libraries
  • machine-specific packages (atlas-XXX and atlas-XXX-devel) for optimized libraries

The choices for optimized ATLAS packages (at the time of writing) include:

atlas-3dnow
atlas-sse
atlas-sse2
atlas-sse3

For the rest of this tutorial, we will assume that atlas-sse3.x86_64 is the best match for the machine of interest. The ATLAS package can be installed using yum:

yum install atlas atlas-devel atlas-sse3 atlas-sse3-devel

Upon installing the packages, one can see the correct include directory set up:

$ alternatives --display atlas-inc
atlas-inc - status is auto.
 link currently points to /usr/include/atlas-x86_64-sse3
/usr/include/atlas-x86_64-base - priority 64
/usr/include/atlas-x86_64-sse3 - priority 68
Current `best' version is /usr/include/atlas-x86_64-sse3.

However, if compiling an application that uses blas, the linker complains that libcblas.so and friends are not found in the linker search path. For some reason, the Fedora maintainers do not provide automatic symbolic links for the compiled libraries. The fix is to manually make the alternatives visible:

alternatives --install /usr/lib64/libatlas.so atlas-lib /usr/lib64/atlas/libatlas.so 64 \
  --slave /usr/lib64/libcblas.so atlas-libcblas /usr/lib64/atlas/libcblas.so \
  --slave /usr/lib64/libclapack.so atlas-libclapack /usr/lib64/atlas/libclapack.so \
  --slave /usr/lib64/libf77blas.so atlas-libf77blas /usr/lib64/atlas/libf77blas.so \
  --slave /usr/lib64/liblapack.so atlas-liblapack /usr/lib64/atlas/liblapack.so \
  --slave /usr/lib64/libptcblas.so atlas-libptcblas /usr/lib64/atlas/libptcblas.so \
  --slave /usr/lib64/libptf77blas.so atlas-libptf77blas /usr/lib64/atlas/libptf77blas.so

alternatives --install /usr/lib64/libatlas.so atlas-lib /usr/lib64/atlas-sse3/libatlas.so 68 \
  --slave /usr/lib64/libcblas.so atlas-libcblas /usr/lib64/atlas-sse3/libcblas.so \
  --slave /usr/lib64/libclapack.so atlas-libclapack /usr/lib64/atlas-sse3/libclapack.so \
  --slave /usr/lib64/libf77blas.so atlas-libf77blas /usr/lib64/atlas-sse3/libf77blas.so \
  --slave /usr/lib64/liblapack.so atlas-liblapack /usr/lib64/atlas-sse3/liblapack.so \
  --slave /usr/lib64/libptcblas.so atlas-libptcblas /usr/lib64/atlas-sse3/libptcblas.so \
  --slave /usr/lib64/libptf77blas.so atlas-libptf77blas /usr/lib64/atlas-sse3/libptf77blas.so

Note that root permissions are required to issue the commands above. For an i686 machine which uses a different optimized ATLAS package, you will need to modify the library path to lib (replacing lib64) and will also need to pick the correct library directory names, e.g., via

rpm -ql atlas-devel atlas-XXX-devel

Now, the choice of the ATLAS library packages can be configured easily:

$ alternatives --display atlas-lib
atlas-lib - status is auto.
 link currently points to /usr/lib64/atlas-sse3/libatlas.so
/usr/lib64/atlas/libatlas.so - priority 64
 slave atlas-libcblas: /usr/lib64/atlas/libcblas.so
 slave atlas-libclapack: /usr/lib64/atlas/libclapack.so
 slave atlas-libf77blas: /usr/lib64/atlas/libf77blas.so
 slave atlas-liblapack: /usr/lib64/atlas/liblapack.so
 slave atlas-libptcblas: /usr/lib64/atlas/libptcblas.so
 slave atlas-libptf77blas: /usr/lib64/atlas/libptf77blas.so
/usr/lib64/atlas-sse3/libatlas.so - priority 68
 slave atlas-libcblas: /usr/lib64/atlas-sse3/libcblas.so
 slave atlas-libclapack: /usr/lib64/atlas-sse3/libclapack.so
 slave atlas-libf77blas: /usr/lib64/atlas-sse3/libf77blas.so
 slave atlas-liblapack: /usr/lib64/atlas-sse3/liblapack.so
 slave atlas-libptcblas: /usr/lib64/atlas-sse3/libptcblas.so
 slave atlas-libptf77blas: /usr/lib64/atlas-sse3/libptf77blas.so
Current `best' version is /usr/lib64/atlas-sse3/libatlas.so.

We have opted to keep the same priority values (64 and 68 in the example above) that the Fedora maintainers used for atlas-inc; we recommend that you do the same.

Using udev to configure Fedora multi-seat automatically

When using a multi-seat configuration on Fedora 17, we need a method to automatically assign devices to seats. The simplest method is to use udev as explained rather tersely in the multiseat documentation. Following the setup in multi-seat configuration on Fedora 17, this article provides an example of setting up the correct udev rules.

If you followed the guidelines in multi-seat configuration on Fedora 17 to set up your configuration, you will find some rules in /etc/udev/rules.d/72-* that Fedora automatically sets up for you. Those rules are rather cryptic and hard to maintain; please move those rules to a backup location (outside rules.d).

We will continue the example by developing our own udev rules. To recap, here is the list of hardware that corresponds to seat2 in this example:

         Devices:
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1
                  │ (drm:card1)
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/graphics/fb1
                  │ (graphics:fb1) "radeondrmfb"
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2
                  │ (sound:card2) "HDMI"
                  │ └ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2/input16
                  │   (input:input16) "HDA ATI HDMI HDMI/DP,pcm=3"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2
                  │ (input:input2) "CHESEN PS2 to USB Converter"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.1/input/input3
                  │ (input:input3) "CHESEN PS2 to USB Converter"
                  └ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-2/4-2:1.0/input/input4
                    (input:input4) "Microsoft Microsoft 3-Button Mouse with IntelliEye(TM)"

We need to identify some feature of these devices that can be used to specify udev rules assigning them to seat2. In order to do so, we will first need to see the attributes of these devices and their parents. We will tackle the keyboard first (a few lines were stripped for brevity):

[root@kaito]$ udevadm info -a -p /devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2

...snip...

  looking at device '/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2':
    KERNEL=="input2"
    SUBSYSTEM=="input"
    DRIVER==""
    ATTR{name}=="CHESEN PS2 to USB Converter"
    ATTR{phys}=="usb-0000:00:12.1-1/input0"
    ATTR{uniq}==""
    ATTR{properties}=="0"

  looking at parent device '/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0':
    KERNELS=="4-1:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="usbhid"
    ATTRS{bInterfaceClass}=="03"
    ATTRS{bInterfaceSubClass}=="01"
    ATTRS{bInterfaceProtocol}=="01"
    ATTRS{bNumEndpoints}=="01"
    ATTRS{supports_autosuspend}=="1"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bInterfaceNumber}=="00"

...snip...

The keyboard can be identified easily by its name and there are no other devices on the machine with the same name. The udev rule needs to assign an environment variable called ID_SEAT to the correct device:

TAG=="seat", ATTRS{name}=="CHESEN PS2 to USB Converter", ENV{ID_SEAT}="seat2"

Note that we have also added a match on the TAG attribute to ensure that we do not try to use devices that are not assignable to a seat (such as hard disks). A similar solution works for the mouse as well. However, the video card is a little more complex. The udev attributes for the video card is as follows:

[root@kaito]$ udevadm info -a -p /devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1

...snip...

  looking at device '/devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1':
    KERNEL=="card1"
    SUBSYSTEM=="drm"
    DRIVER==""

  looking at parent device '/devices/pci0000:00/0000:00:04.0/0000:02:00.0':
    KERNELS=="0000:02:00.0"
    SUBSYSTEMS=="pci"
    DRIVERS=="radeon"
    ATTRS{irq}=="45"
    ATTRS{subsystem_vendor}=="0x174b"
    ATTRS{broken_parity_status}=="0"
    ATTRS{class}=="0x030000"
    ATTRS{power_method}=="profile"
    ATTRS{consistent_dma_mask_bits}=="40"
    ATTRS{dma_mask_bits}=="40"
    ATTRS{local_cpus}=="00000000,00000000,00000000,0000000f"
    ATTRS{device}=="0x9588"
    ATTRS{power_profile}=="default"
    ATTRS{msi_bus}==""
    ATTRS{local_cpulist}=="0-3"
    ATTRS{vendor}=="0x1002"
    ATTRS{subsystem_device}=="0x2e42"
    ATTRS{boot_vga}=="0"
    ATTRS{numa_node}=="0"

  looking at parent device '/devices/pci0000:00/0000:00:04.0':
    KERNELS=="0000:00:04.0"
    SUBSYSTEMS=="pci"
    DRIVERS=="pcieport"
    ATTRS{irq}=="41"
    ATTRS{subsystem_vendor}=="0x1849"
    ATTRS{broken_parity_status}=="0"
    ATTRS{class}=="0x060400"
    ATTRS{consistent_dma_mask_bits}=="32"
    ATTRS{dma_mask_bits}=="32"
    ATTRS{local_cpus}=="00000000,00000000,00000000,0000000f"
    ATTRS{device}=="0x597a"
    ATTRS{msi_bus}=="1"
    ATTRS{local_cpulist}=="0-3"
    ATTRS{vendor}=="0x1002"
    ATTRS{subsystem_device}=="0x5957"
    ATTRS{numa_node}=="0"

...snip...

There does not seem to be an obvious property on which to match. After trawling through various devices, we find that the one property that is different between the two video cards: they both have the same vendor id, but different subsystem device ids. (This makes sense since they are both RadeonHD cards, albeit with different model numbers.) Then the udev rules follow easily:

# Radeon 2600XT video and HDMI audio
TAG=="seat", ATTRS{vendor}=="0x1002", ATTRS{subsystem_device}=="0x2e42", ENV{ID_SEAT}="seat2"
TAG=="seat", ATTRS{vendor}=="0x1002", ATTRS{subsystem_device}=="0xaa08", ENV{ID_SEAT}="seat2"

Putting all of the rules into a udev rules file completes the process:

# Radeon 2600XT video and HDMI audio
TAG=="seat", ATTRS{vendor}=="0x1002", ATTRS{subsystem_device}=="0x2e42", ENV{ID_SEAT}="seat2"
TAG=="seat", ATTRS{vendor}=="0x1002", ATTRS{subsystem_device}=="0xaa08", ENV{ID_SEAT}="seat2"

# Keyboard
TAG=="seat", ATTRS{name}=="CHESEN PS2 to USB Converter", ENV{ID_SEAT}="seat2"

# Mouse
TAG=="seat", ATTRS{name}=="Microsoft Microsoft 3-Button Mouse with IntelliEye(TM)", ENV{ID_SEAT}="seat2"

On reboot, devices will be assigned correctly to their respective seats. In theory, everything should work fine. However, our current testing reveals a need to boot into runlevel 3 and then manually invoke runlevel 5. We believe that this may have something to do with a udev race in dracut. Until it is fixed, the manual step of booting into runlevel 3 and changing manually into runlevel 5 will be needed.

Occasionally, we find that the GDM login screen turns blank. Once logged in, this does not occur (at least with KDE). Our current workaround for this case is to restart the X server for that particular seat by pressing Ctrl-Alt-Backspace.

Simple multiseat setup on Fedora 17

Fedora 17 (Beefy Miracle) adds rudimentary support for multi-seat configurations. However, documentation on setting up multi-seat configurations is rather sparse. The following guide should get you started. This guide assumes that you are comfortable working on the command line and that you know how to change init levels.

Multi-seat support on Linux is essentially at a beta state. While we think that the risk is small, please be aware that you can damage your system.

Hardware requirements

  • Two video cards
  • Two mice
  • Two keyboards

On the test system, we will use an ASRock motherboard with two ATI/AMD RadeonHD video cards: HD6850 and HD2400XT. The 6850 is on the PCIe x16 slot and the 2400 is on the PCIe x4 slot.

Step 1: Identifying hardware

Once all hardware is installed and Fedora 17 is running on your primary monitor, reboot and enter into run-level 3 by adding the number 3 in grub2 (note artificial line-breaks inserted for formatting and not needed in actual command-line):

linux   /vmlinuz-3.4.0-1.fc17.x86_64 \
  root=UUID=c0db7f1b-8622-4d30-9f98-47c806f6c79f ro rd.md=0 rd.lvm=0 \
  rd.dm=0  KEYTABLE=us quiet SYSFONT=latarcyrheb-sun16 rhgb rd.luks=0 \
  LANG=en_US.UTF-8 3

Upon booting into a virtual terminal, login as root. By default, all hardware is assigned to a special seat called seat0. We can now list all the hardware available on the machine:

[root@kaito ~]$ loginctl seat-status seat0
seat0
        Sessions: *11
         Devices:
                  ├ /sys/devices/LNXSYSTM:00/LNXPWRBN:00/input/input1
                  │ (input:input1) "Power Button"
                  ├ /sys/devices/LNXSYSTM:00/device:00/PNP0C0C:00/input/input0
                  │ (input:input0) "Power Button"
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.0/drm/card0
                  │ (drm:card0)
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.0/graphics/fb0
                  │ (graphics:fb0) "radeondrmfb"
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.1/sound/card1
                  │ (sound:card1) "Generic"
                  │ └ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.1/sound/card1/input15
                  │   (input:input15) "HD-Audio Generic HDMI/DP,pcm=3"
                  ├ /sys/devices/pci0000:00/0000:00:11.0/ata4/host3/target3:0:0/3:0:0:0/block/sr0
                  │ (block:sr0)
                  ├ /sys/devices/pci0000:00/0000:00:11.0/ata4/host3/target3:0:0/3:0:0:0/scsi_generic/sg2
                  │ (scsi_generic:sg2)
                  ├ /sys/devices/pci0000:00/0000:00:12.0/usb3
                  │ (usb:usb3)
                  │ ├ /sys/devices/pci0000:00/0000:00:12.0/usb3/3-1/3-1:1.2/0003:046D:C52B.0006/input/input5
                  │ │ (input:input5) "Logitech Unifying Device. Wireless PID:101b"
                  │ └ /sys/devices/pci0000:00/0000:00:12.0/usb3/3-1/3-1:1.2/0003:046D:C52B.0006/input/input6
                  │   (input:input6) "Logitech Unifying Device. Wireless PID:200a"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4
                  │ (usb:usb4)
                  ├ /sys/devices/pci0000:00/0000:00:12.2/usb1
                  │ (usb:usb1)
                  ├ /sys/devices/pci0000:00/0000:00:13.0/usb5
                  │ (usb:usb5)
                  ├ /sys/devices/pci0000:00/0000:00:13.1/usb6
                  │ (usb:usb6)
                  ├ /sys/devices/pci0000:00/0000:00:13.2/usb2
                  │ (usb:usb2)
                  ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0
                  │ (sound:card0) "SB"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input10
                  │ │ (input:input10) "HDA ATI SB Front Headphone"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input11
                  │ │ (input:input11) "HDA ATI SB Line Out Side"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input12
                  │ │ (input:input12) "HDA ATI SB Line Out CLFE"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input13
                  │ │ (input:input13) "HDA ATI SB Line Out Surround"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input14
                  │ │ (input:input14) "HDA ATI SB Line Out Front"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input7
                  │ │ (input:input7) "HDA ATI SB Line"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input8
                  │ │ (input:input8) "HDA ATI SB Front Mic"
                  │ └ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input9
                  │   (input:input9) "HDA ATI SB Rear Mic"
                  ├ /sys/devices/pci0000:00/0000:00:14.5/usb7
                  │ (usb:usb7)
                  ├ /sys/devices/virtual/misc/kvm
                  | (misc:kvm)
                  ├ -----------------------------------------------------------------------
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1
                  │ (drm:card1)
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/graphics/fb1
                  │ (graphics:fb1) "radeondrmfb"
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2
                  │ (sound:card2) "HDMI"
                  │ └ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2/input16
                  │   (input:input16) "HDA ATI HDMI HDMI/DP,pcm=3"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2
                  │ (input:input2) "CHESEN PS2 to USB Converter"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.1/input/input3
                  │ (input:input3) "CHESEN PS2 to USB Converter"
                  └ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-2/4-2:1.0/input/input4
                    (input:input4) "Microsoft Microsoft 3-Button Mouse with IntelliEye(TM)"

The list above has been rearranged and modified for ease of presentation: the hardware list has been separated into two with a separator line. We will use all the hardware below the line as part of a second seat that we will seat2.

We will assume that we are initially connected to the system on seat0 via the 6850 and using the Logitech Unifying Device as our keyboard and mouse. Please ensure that you are not currently using hardware on seat0 that will be mapped to seat2. Note that the session index may be different on your system.

Step 2: Assigning devices to the second seat

In order to test that multi-seat deployment works correctly, we will first assign hardware manually to the second seat; eventually, we will automate it using udev. Seats are created based on graphics cards. Hence we will start by assigning a graphics card:

loginctl attach seat2 /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1
loginctl attach seat2 /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/graphics/fb1

Let us then add the sound card and the input devices:

loginctl attach seat2 /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2/input16
loginctl attach seat2 /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2
loginctl attach seat2 /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2
loginctl attach seat2 /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.1/input/input3
loginctl attach seat2 /sys/devices/pci0000:00/0000:00:12.1/usb4/4-2/4-2:1.0/input/input4

Now, if everything went well, you should be able to see that the hardware has been partitioned into two seats:

[root@kaito ~]$ loginctl seat-status seat0
seat0
        Sessions: *11
         Devices:
                  ├ /sys/devices/LNXSYSTM:00/LNXPWRBN:00/input/input1
                  │ (input:input1) "Power Button"
                  ├ /sys/devices/LNXSYSTM:00/device:00/PNP0C0C:00/input/input0
                  │ (input:input0) "Power Button"
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.0/drm/card0
                  │ (drm:card0)
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.0/graphics/fb0
                  │ (graphics:fb0) "radeondrmfb"
                  ├ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.1/sound/card1
                  │ (sound:card1) "Generic"
                  │ └ /sys/devices/pci0000:00/0000:00:02.0/0000:03:00.1/sound/card1/input15
                  │   (input:input15) "HD-Audio Generic HDMI/DP,pcm=3"
                  ├ /sys/devices/pci0000:00/0000:00:11.0/ata4/host3/target3:0:0/3:0:0:0/block/sr0
                  │ (block:sr0)
                  ├ /sys/devices/pci0000:00/0000:00:11.0/ata4/host3/target3:0:0/3:0:0:0/scsi_generic/sg2
                  │ (scsi_generic:sg2)
                  ├ /sys/devices/pci0000:00/0000:00:12.0/usb3
                  │ (usb:usb3)
                  │ ├ /sys/devices/pci0000:00/0000:00:12.0/usb3/3-1/3-1:1.2/0003:046D:C52B.0006/input/input5
                  │ │ (input:input5) "Logitech Unifying Device. Wireless PID:101b"
                  │ └ /sys/devices/pci0000:00/0000:00:12.0/usb3/3-1/3-1:1.2/0003:046D:C52B.0006/input/input6
                  │   (input:input6) "Logitech Unifying Device. Wireless PID:200a"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4
                  │ (usb:usb4)
                  ├ /sys/devices/pci0000:00/0000:00:12.2/usb1
                  │ (usb:usb1)
                  ├ /sys/devices/pci0000:00/0000:00:13.0/usb5
                  │ (usb:usb5)
                  ├ /sys/devices/pci0000:00/0000:00:13.1/usb6
                  │ (usb:usb6)
                  ├ /sys/devices/pci0000:00/0000:00:13.2/usb2
                  │ (usb:usb2)
                  ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0
                  │ (sound:card0) "SB"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input10
                  │ │ (input:input10) "HDA ATI SB Front Headphone"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input11
                  │ │ (input:input11) "HDA ATI SB Line Out Side"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input12
                  │ │ (input:input12) "HDA ATI SB Line Out CLFE"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input13
                  │ │ (input:input13) "HDA ATI SB Line Out Surround"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input14
                  │ │ (input:input14) "HDA ATI SB Line Out Front"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input7
                  │ │ (input:input7) "HDA ATI SB Line"
                  │ ├ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input8
                  │ │ (input:input8) "HDA ATI SB Front Mic"
                  │ └ /sys/devices/pci0000:00/0000:00:14.2/sound/card0/input9
                  │   (input:input9) "HDA ATI SB Rear Mic"
                  ├ /sys/devices/pci0000:00/0000:00:14.5/usb7
                  │ (usb:usb7)
                  └ /sys/devices/virtual/misc/kvm
                    (misc:kvm)
[root@kaito ~]$ loginctl seat-status seat2
seat2
        Sessions: *12
         Devices:
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/drm/card1
                  │ (drm:card1)
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.0/graphics/fb1
                  │ (graphics:fb1) "radeondrmfb"
                  ├ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2
                  │ (sound:card2) "HDMI"
                  │ └ /sys/devices/pci0000:00/0000:00:04.0/0000:02:00.1/sound/card2/input16
                  │   (input:input16) "HDA ATI HDMI HDMI/DP,pcm=3"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.0/input/input2
                  │ (input:input2) "CHESEN PS2 to USB Converter"
                  ├ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-1/4-1:1.1/input/input3
                  │ (input:input3) "CHESEN PS2 to USB Converter"
                  └ /sys/devices/pci0000:00/0000:00:12.1/usb4/4-2/4-2:1.0/input/input4
                    (input:input4) "Microsoft Microsoft 3-Button Mouse with IntelliEye(TM)"

If your display manager is set to GDM, start runlevel 5, and you should see an independent login screen on each monitor. GDM does tend to be flaky and you may need to switch a couple of times between runlevels 3 and 5 before you see login screens on both seats.

Next steps

If everything above worked, we can automate the process.