framegroups.el icon indicating copy to clipboard operation
framegroups.el copied to clipboard

Workspaces for emacs using frames

  • Motivation Emacs already has more workspace management packages than I can count. In the past, I exclusively used workgroups2 because no other package had all of the functionality I consider important. These are the main features supported by this package:
  • Workspace-local/independent window layout history/undo (this is the feature that no package I am aware of besides workgroups2 supports and is the primary reason I created this package)
  • Named, not (just) numbered, workspaces; each represent some context (e.g. book notes, programming project, etc.)
  • Hooks for creating and switching between workspaces to allow default session setup and so that workspace-specific keybindings/settings can be applied
  • No limit to the number of workspaces (elscreen is the main and possibly only offender)
  • Persistence (this is generally not an issue as desktop.el can be used to store sessions)

While workgroups2 has all of the above functionality, it has serious issues as well:

  • It is unmaintained and cannot save layouts on the latest Emacs
  • Extra configuration is necessary to fix some annoyances:
    • Golden ratio/zoom must be temporarily disabled when switching workgroups to prevent "window too small for splitting" errors when switching to a workgroup with more than two or three windows (this is also an issue for some alternative packages I've tried)
    • Window resizing (e.g. automatically done by golden ratio/zoom) is recorded by default (moving through window size history is pretty useless, especially if you are using a package like golden ratio; =winner-mode=, on the other hand, doesn't record window resizing)

Workgroups2 is a fairly large package, and I don't want to attempt to maintain/fix it when I only need some of its functionality. The goal of this package is to be as simple as alternatives while also allowing for the useful functionality they lack. The main principle is to use multiple frames as workspaces instead switching between window layouts inside a single frame. This has several advantages:

  • =winner-mode= keeps track of window configuration changes separately for each frame
  • =desktop.el= can be used to persist and restore all frames
  • The user can change keybindings based on the frame's =fg-name= property (e.g. by using =fg-after-switch-hook=) and make use of frame-local variables
  • No "window too small for splitting" issues
  • Requirements and Limitations The primary issue with using frames for workgroups-like functionality is that emacs (by itself) has no way of displaying only one frame at a time and hiding the others. This isn't an issue if you are willing to dedicate different desktops to different frames. On the other hand, frames aren't great if you want a lot of contexts/groups or would prefer to use keys bound in emacs for switching between them instead of always using global hotkeys (since ~select-frame-set-input-focus~ does not work across desktops).

By default, this package attempts address these issues issue by using =xdotool= to hide the old frame and switch to a new one. If [[https://github.com/ch11ng/xelb][xelb]] is able to easily map/unmap windows, I may switch to using it in the future.

Note that this package will only attempt to use xdotool if the user is using X11, xdotool is installed, and the related settings (see below) are non-nil. Otherwise, it will not attempt to hide frames and will use emacs' builtin frame switching function.

To clarify, if you are not using Linux/X11 or do not wish to use xdotool, most of this package's functionality is still available:

  • ~fg-rename-frame~ works
  • ~fg-create-frame~ works
  • ~fg-create-hook~ works
  • the mode line indicator works
  • ~fg-switch-to-frame~ and ~fg-switch-to-last-frame~ will likely not work /across desktops/ (you'll need to use global WM keys to switch between desktops); they should work for frames located on the same desktop
  • ~fg-after-switch-hook~ is only triggered from ~fg-switch-to-(last-)frame~ (you can use ~focus-in-hook~ instead if you want to dedicate different WM desktops to different frames)
  • ~fg-desktop-setup~ will not work

In summary, reliable frame switching when using multiple desktops requires xdotool (basically a hack). Using xdotool to hide/switch windows can also cause flicker especially if you are not using a compositor. That said, if all your frames are on a single desktop, using xdotool is not necessary. You can instead have only one frame visible by using the monocle/fullscreen layout for that desktop.

  • Terminology The following sections use the term "framegroup" to refer to a frame that this package is managing (i.e. it was named/focused using this package's commands).

  • Settings/Hooks

  • =fg-hide-with-xdotool= (default t): whether to hide the old frame when switching to a new one (only has an effect when using X11 and xdotool is installed)
  • =fg-switch-with-xdotool= (default t): whether to switch frames using xdotool instead of emacs' ~select-frame-set-input-focus~ (only has an effect when using X11 and xdotool is installed and =fg-hide-with-xdotool= is nil; when =fg-hide-with-xdotool= is non-nil, xdotool must be used to remap hidden windows)
  • =fg-auto-create= (default t): whether to automatically create a framegroup when attempting to switch to a non-existent one
  • =fg-create-hook=: hook run when a framegroup is first created (renaming also triggers this hook)
  • =fg-after-switch-hook=: hook run after switching framegroups; functions are run with the name of the new framegroup as an argument
  • Provided Functions/Macros Commands:
  • ~fg-rename-frame~: set the name of the current framegroup
  • ~fg-create-frame~: choose a name and create a new framegroup
  • ~fg-switch-to-frame~: switch to another framegroup
  • ~fg-switch-to-last-frame~: switch to the previously focused framegroup

Helper functions/macros:

  • ~fg-switch~: a convenience macro that returns a function that will switch to a framegroup (e.g. ~(define-key "key" (fg-switch "notes"))~)
  • ~fg-desktop-setup~: will add to =desktop-read-hook= to hide all but the last focused frame when restoring a session
  • ~fg-mode-line-string~: return the current framegroup name formatted for the mode line

This package does not provide numbered workspaces or directional switching commands as I personally prefer to have named contexts. You could of course simply name your frames with numbers and create directional switching keybindings yourself with ~fg-switch~, but I do not plan to this functionality directly.

  • Example Configuration #+begin_src emacs-lisp ;; enable `winner-mode' for undo (winner-mode) (global-set-key "C-c l" 'winner-undo) (global-set-key "C-c L" 'winner-redo)

;; enable `desktop-save-mode' for persistence ;; NOTE: It seems Emacs occasionally hangs when restoring a lot of frames with ;; desktop.el (fg-desktop-setup) (desktop-save-mode)

;; binding keys to switch to specific framegroups (global-set-key "C-c e" (fg-switch "emacs")) (global-set-key "C-c p" (fg-switch "prog")) ;; ...

;; default layouts for framegroups (defun my-framegroup-setup (name &rest _) "Set up default framegroup layouts." (interactive) (pcase name ;; emacs configuration ("emacs" (find-file "~/.emacs.d/other.el") (split-window-right) (find-file "~/.emacs.d/init.el")) ;; programming projects ("prog" (find-file "~/src")) ;; dotfiles ("config" (find-file "~/dotfiles")) ("mail" (mu4e)) ("music" (mingus))))

(add-hook 'fg-create-hook #'my-framegroup-setup)

;; binding keys for the current framegroup (defmacro my-ff (file) "Wrapper for creating find-file' commands." (lambda () (interactive) (find-file ,file)))

(defun my-framegroup-keybindings (name &rest _) (pcase name ("emacs" (global-set-key "C-c , i" (my-ff "~/.emacs.d/init.el"))) ("prog" (global-set-key "C-c , r" (my-ff "README.org")) (global-set-key "C-c , d" #'projectile-edit-dir-locals))))

(add-hook 'fg-after-switch-hook #'my-framegroup-keybindings) #+end_src

For mode line integration, you can insert ~fg-mode-line-string~ into the mode line: #+begin_src emacs-lisp (setq-default mode-line-format ;; ... '(:eval (when (fboundp 'fg-mode-line-string) (fg-mode-line-string))) ;; ... ) #+end_src