Extensible Guile bindings for SwayWM
Find a file
2024-06-26 19:23:37 +03:00
examples rename project to guile-swayer 2024-06-26 19:01:58 +03:00
modules fix general dependency in which-key 2024-06-26 19:23:37 +03:00
sjson init commit 2024-06-15 10:57:11 +03:00
swayipc export all modules under sjson and swayipc 2024-06-26 00:45:38 +03:00
README.org rename project to guile-swayer 2024-06-26 19:01:58 +03:00
sjson.scm export all modules under sjson and swayipc 2024-06-26 00:46:06 +03:00
swayipc.scm export all modules under sjson and swayipc 2024-06-26 00:46:06 +03:00

Guile Swayer

I am an Emacs user and previously used StumpWM, an X11 window manager written in Common Lisp. I believe window managers should be scriptable because the level of workflow customization required by users often exceeds what can be achieved with simple configuration parameters (see my workflow below for a clearer understanding of why this is the case). Unfortunately, Sway/i3 lacks a straightforward programmable interface for customization. This project provides complete control over Sway/i3 using Guile!

Why Sway?

I had to migrate to Wayland at some point. Being a big fan of StumpWM, I tried to replicate a similar environment in one of the Wayland window managers. I made some progress with hyprland using a set of Guile bindings I developed called hypripc, but I found that Hyprland isn't as stable as Sway.

Quick Test

After cloning this repository, you can immediately test it using the provided examples/playground/example.scm file in the directory, which demonstrates some of the features available in this package.

The examples/playground/example.scm file will:

  • Print the current focused workspace
  • Add a keybinding (Super+t) that launches Alacritty
  • Print a message when a workspace change event occurs
guile ./examples/playground/example.scm

Quick Overview

Query Sway

You can retrieve information about Sway, such as list of available workspaces or outputs. The response will be in Guile records, which you can easily manipulate! (refer to swayipc/info.scm)

;; get focused workspace from a list of workspaces
(define (focused-workspace-name workspaces)
  (cond
    ((null? workspaces) #f)
    ((equal? #t (sway-workspace-focused (car workspaces)))
     (sway-workspace-name (car workspaces)))
    (else (focused-workspace-name (cdr workspaces)))))

(format #t "output record from function #sway-get-workspaces:\n ~a\n"
        (sway-get-workspaces))

(format #t "current focused workspace is [~a]\n"
        (focused-workspace-name (sway-get-workspaces)))

Assign Keybindings

You can assign keybindings that execute Guile code! Obviously, running shell commands is straightforward since you're operating within Guile. Additionally, you have full access to Sway/i3 specific commands (refer to swayipc/dispatcher.scm).

  ;; normal sway keybindings (limited and can't easily execute guile code)
  (sway-bindsym "Mod4+t" "exec alacritty")

  ;; general.scm interface for sway keybindings
  ;; this uses sway-bindsym behind the scenes, but provides a much
  ;; user friendly interface to create complex keybindings structure
  ;; it also allows you to execute guile expressions on trigger.
  ;; refer to modules/general.scm for more about how this is done.

  ;; define leader keymap
  (define (exec command)
    "execute given shell command"
    (format #t "running: ~a\n" command)
    (system command))

  (general-define-keys
   #:prefix "s-Space" #:wk "Leader"
   `("o" (exec "rofi -show drun"))
   `("C-g" (sway-mode "default") #:wk "abort")

   ;; rofi keymap
   `(general-define-keys
     #:prefix "r" #:wk "Rofi"
     ("p" (exec "~/.config/rofi/bin/password-manager"))
     ("m" (exec "rofi-mount"))
     ("u" (exec "rofi-unmount"))
     ("w" (exec ".config/rofi/bin/wifi"))
     ("b" (exec "~/.config/rofi/bin/bluetooth"))
     ("f" (exec "~/.config/rofi/bin/finder"))
     ("k" (exec "~/.config/rofi/bin/keyboard-layout"))
     ("P" (exec "~/.config/rofi/bin/powermenu"))
     ("s" (exec "~/.config/rofi/bin/sound-input"))
     ("S" (exec "~/.config/rofi/bin/sound-output")))

   ;; window management
   `(general-define-keys
     #:prefix "w" #:wk "Window"
     ("v" (sway-layout SWAY-LAYOUT-SPLITV))
     ("h" (sway-layout SWAY-LAYOUT-SPLITH))
     ("f" (sway-fullscreen SWAY-FULLSCREEN-TOGGLE))
     ("d" (sway-layout SWAY-LAYOUT-DEFAULT))
     ("t" (sway-layout SWAY-LAYOUT-TABBED))))

Subscribe to Events

Certain scenarios necessitate subscribing to events. One example from my workflow described below requires this capability. With guile-swayer, you have the ability to listen for events and execute actions in response.

  ;; subscribe to events
  (define (workspace-changed workspace-event)
    (let* ((current-tree (sway-workspace-event-current workspace-event))
           (workspace (sway-tree-name current-tree)))

      (format #t "workspace changed to ~a!\n" workspace)))

  (add-hook! sway-workspace-hook workspace-changed)

Note: To receive any events, you must subscribe to them. You can subscribe to individual events that interest you or to all available events. Without subscribing and running the event listener in your init.scm, your hooks will not receive any events.

The event listener thread is a Unix socket that waits for sway events. This must be executed, preferably as the last expression in your init.scm file, because thread-join will block execution. This blocking is necessary to keep the listener active and prevent the script from exiting.

  ;; subscribe to all events
  (sway-subscribe-all)

  (sway-start-event-listener-thread)
  (thread-join! SWAY-LISTENER-THREAD)

Documentation (WIP)

Most of the source code is documented. You can refer to examples/stumpwm-like/init.scm for a complex stumpwm like configuration example. Here are some important points to consider before hacking your Sway setup

Quick Start

Clone this repository to your ~/.config/sway It's important to know where you clone the repo as you will have to reference it later by path to make a perfect setup.

Project Structure

Root Directory
File Description
examples Examples of configurations the you can refer to for inspiration
modules Directory containing modules for extending Sway using guile-swayer.
sjson A patched version of guile-json (temporarily).
swayipc Directory containing the core code for swayipc.
README.org This readme file
swayipc Directory
File Description
connection Establishes IPC connection for handling events and commands with Sway.
dispatcher Provides Guile functions for all available Sway commands.
events Provides Gulie Hooks for all available Sway events.
info Provides Guile functions for querying Sway's current state and information.
records Provides Guile records representing Sway's data structures.
Modules Directory
File Description
auto-reload.scm Watcher to automatically reload Sway when Guile files change.
general.scm Inspired by Emacs general package; provides an easy interface for keybindings.
kbd.scm Translates Emacs-like keybindings to be compatible with Sway.
which-key.scm Inspired by Emacs which-key package; enhances keybinding discovery.
workspace-grid.scm Configures workspaces in a grid (see workflow below).
workspace-groups.scm Spans/synchronizes workspaces across monitors (see workflow below).

1- You can start your swayipc configurations from the REPL, terminal, or a configuration file. Remember: for debugging or displaying output, it's best to run Guile from the REPL or terminal. You can also pipe the output to a file if you desire.

# good idea to kill all current guile swayipc instances first
exec_always "pkill -f '.*guile.*sway/init.scm'"

# then run a fresh instance, sleeping ensures a more reilable execution
exec_always "sleep 0.5 && ~/.config/sway/init.scm"

2- I plan to publish a module for guile-swayer, it's currently not hosted anywhere. You'll need to add the module to your load path. Additionally, swayipc includes another patched Guile library called guile-json, which is embedded for now. In the future, this will be included as a separate dependency rather than embedded.

(add-to-load-path
 (dirname (or (current-filename)
              (string-append (getenv "HOME") "/.config/sway/init.scm"))))

Workflow

Workspace Grid

I arrange my workspaces in a grid format. Typically, workspaces are laid out horizontally. With nine workspaces, navigating from workspace 1 to 9 using only horizontal directions can be cumbersome. Assigning a key to each workspace would be efficient but would clutter default mode keybindings. Some might create another mode or submap, but pressing multiple keys to move between workspaces becomes inefficient . I find the optimal solution is organizing workspaces in a grid format, enabling both horizontal and vertical navigation. Currently, I use a 3x3 grid with wraparound navigation.

Horizontal vs Grid 9 workspaces

Horizontal

  1 2 3 4 5 6 7 8 9

Grid (3x3)

  1 2 3
  4 5 6
  7 8 9

Example navigation in a grid (cs#idx is current workspace):

  cs#1> go right
  cs#2> go down
  cs#5> go down
  cs#8> go down (notice wraparound behavior)
  cs#2> go right
  cs#3> ..

Note: this behavior is achieved via modules/workspace-grid.scm

Workspace Groups

My workspaces function as groups or tasks that span across all three monitors in my setup. For example, if I switch to my communication workspace on one monitor, I want all monitors to switch to their respective communication workspaces. This means if I have WhatsApp on monitor #1, Discord on monitor #2, and IRC on monitor #3, they should all align to their designated communication workspace when I switch tasks.

Similarly, this setup extends to projects I work on. If I focus on my dotfiles, I want all monitors to switch to the workspace dedicated to that task. The same principle applies to game development or any other specific task or project workspace I engage with.

Normal workspaces

  | ws#1 | ws#2 | ws#3 | ws#4 | ws#5 | ws#6 |

Grouped workspaces (3 monitors)

  |           ws#1           |           ws#2           |
  |-----------------------------------------------------|
  | ws#1-1 & ws#1-2 & ws#1-3 | ws#2-1 & ws#2-2 & ws#2-3 |

Example of navigation into a workspace (same behavior regardless of the method used to switch workspaces):

  ws#1> go to ws#2-1
  ws#2> go to ws#2-2 (same group, no switching)
  ws#2> go to ws#1-3
  ws#1> ..

You can partially configure workspace groups to span or sync only some workspaces. This allows you to have workspaces that do not span and others that do, with the ability to pin specific workspaces to their monitors when focused.

Note: this behavior is achieved via modules/workspace-groups.scm