A DWIM fullscreen function for Emacs and Sway

A consistent keybinding for my text editor and my window manager

📅 6 Nov 2023 | ~4 min read
Tags: #emacs #linux

For several years now, I have been using the method that Pavel Korytov describes in this post to gain a consistent set of keybindings between Emacs and Sway. I have been extremely happy with the setup and I decided that if I ever move away from Sway, I will need to replicate this setup in another window manager.

I mostly work on a 14 inch laptop screen, so being able to make applications fullscreen is something I find very useful. Previously I had been using the below function for fullscreening windows in Emacs, but having seen how frictionless window management between Emacs and Sway could be, I decided to try a new approach.

(defun toggle-maximize-buffer ()
  "Toggle maximize buffer"
  (interactive)
  (if (one-window-p)
      (let ((my-saved-point (point)))
        (set-window-configuration my-saved-window-configuration)
        (goto-char my-saved-point))
    (progn
      (setq my-saved-window-configuration (current-window-configuration))
      (delete-other-windows))))

To achieve the desired results, it is first necessary to to know what window is active at any time and if it is fullscreen. I will be referring to Emacs as fullscreen if there is only one window in the current frame. To tell if Emacs meets my definition of fullscreen, we can use the built-in one-window-p function, and I have also written functions to determine if Emacs is the active window, and if Sway is in fullscreen mode.

(defconst my/sway-get-fullscreen
  "swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).fullscreen_mode'")

(defconst my/sway-get-active-app
  "swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).app_id'")

(defun my/sway-get-active-app ()
  "Return name of active window in Sway Window Manager as a string."
  (let ((app (shell-command-to-string my/sway-get-active-app)))
    (and (string-match "\"\\(.+\\)\"" app) (match-string 1 app))))

(defun my/sway-emacs-active-p ()
  "Return non-nil if Emacs is active window in Sway Window Manager."
  (equal (my/sway-get-active-app) "emacs"))

(defun my/sway-fullscreen-p ()
  "Return non-nil if current Window is fullscreen in Sway Window Manager."
  (let ((fullscreen (shell-command-to-string my/sway-get-fullscreen)))
    (eq (string-to-number fullscreen) 1)))

The Sway IPC protocol allows for commands to be run from the shell, which means that we can call them from Emacs. I am using the call-process function to toggle Sway’s fullscreen mode and I also wrote a simple function that toggles fullscreen for both Sway and Emacs at the same time as this is the desired outcome when neither are in fullscreen mode or both are in fullscreen mode.

(defun my/sway-toggle-fullscreen ()
  "Toggle fullscreen mode in Sway Window Manager."
  (call-process "swaymsg" nil nil nil "fullscreen"))

(defun my/toggle-emacs-and-sway-fullscreen ()
  "Toggles fullscreen for both Emacs and Sway window manager."
  (my/sway-toggle-fullscreen)
  (toggle-maximize-buffer))

Finally, it’s just a simple matter of putting everything together in the main function.

The logic is as follows:

(defun my/toggle-fullscreen-dwim ()
  "Fullscreen function to be called from Sway Window Manger.

If the active window does not belong to Emacs, then call Sway's
inbuilt fullscreen command.

If the active window belongs to Emacs, then do one of the following
depending on combinations of Sway and Emacs being fullscreened:

If both Emacs and Sway are fullscreened or neither are fullscreened,
toggle fullscreen for both.

If Emacs is fullscreened and Sway is not, make Sway fullscreen.

If Sway is fullscreened and Emacs is not, make Emacs fullscreen."
  (let ((ea (my/sway-emacs-active-p))
        (ef (one-window-p))
        (sf (my/sway-fullscreen-p)))
    ;; emacs not active
    (cond ((not ea)
           (my/sway-toggle-fullscreen))
          ;; both fullscreen
          ((and ef sf)
           (my/toggle-emacs-and-sway-fullscreen))
          ;; emacs fullscreen, sway not fullscreen
          ((and ef (not sf))
           (my/sway-toggle-fullscreen))
          ;; sway fullscreen, emacs not fullscreen
          ((and sf (not ef))
           (toggle-maximize-buffer))
          ;; neither fullscreen
          ((and (not sf) (not ef))
           (my/toggle-emacs-and-sway-fullscreen)))))

I then made sure to bind the function in my Sway config.

bindsym $mod+f exec emacsclient -e '(my/toggle-fullscreen-dwim)'

I have been using this approach for a few days now and I am happy with how it has turned out. As much as I like doing everything in Emacs, there are definitely times where it is impractical. With this approach, I am getting consistent binding across all my applications and I no longer really need to consider what the application is. I can treat them all the same now.

Caveats

The only problem that I have had, is one that I have had ever since I started combining keybindings between Sway and Emacs. When I first turn my PC on, Emacs is not able to locate the Sway socket. In fact, I am unable to run any swaymsg command from within Emacs. I’m sure that it could be fixed, but for now I simply restart Emacs once before I start working in it.

If anyone has found ways of getting around this or has any further suggestions, I would love to hear them by email or on Mastodon.

✉️ Respond by Email.