[March, 2022] While using Emacs only for text editing I've switched between it, the Terminal Emulator and the Browser pretty much like on a Windows system: Alt+Tab. But after slowly incorporating both eshell and then exwm into my configuration and workflow, I kinda wanted that behavior back. In short, keeping all shell stuff in a separate window and instantly switching to it. Same with the browser and the rest of the UI stuff. I've tried tab-mode and knew instantly that it's what I want. Used straight out of the box though, it doesn't fulfill all my needs: switching to a buffer or browsing an url doesn't automatically change the tab, for example. So I've added that functionality and some others goodies to ease the management of buffers. Below is a setup, that though highly opinionated, is completely functional. You can adapt it for your own needs.
The tab-bar mode enables one to have as many tabs as one wants on each Emacs frame. This is similar to what we have in standard text editors or in browsers. The great thing is that each tab contains not just a single buffer but a window configuration. That is, I can split my window into multiple buffers, save it as a tab and give it a name. Switch to a different tab and I have a different set of buffers split differently. It is the perfect place to have a tab containing all my shells, a tab containing code and org files and one containing the browser, pdf viewers or music players. I am using exwm, but you don't need to. What follows still applies to a non-exwm setup, only with different tabs and functionalities. With that, let's start coding.
Since, for some reason or another, my tabs are not saved after I exit Emacs,
even though I have the desktop-save-mode
enabled, I'm making sure I have the
tabs I need every time I start Emacs,
;; Ensure the needed tabs are created or exist; add extra to your liking (progn (tab-create "files") (tab-create "eshells") (tab-create "exwm")) (defun tab-create (name) "Create the NAME tab if it doesn't exist already." (condition-case nil (unless (equal (alist-get 'name (tab-bar--current-tab)) name) (tab-bar-rename-tab-by-name name name)) (error (tab-new) (tab-bar-rename-tab name)))) ;; Hide the tabs since I don't want to click on them. (setf tab-bar-show nil)
I have only three tabs, one for all the eshells, one for UI buffers, like browsers, pdf viewers, video players, etc. and one for the "normal" buffers, like code, org files and the like. I'm also hiding the tab bar so that after setting all up it doesn't even matter what package I'm using to organize all of this. I don't want it to get in my way. I just want to switch buffers seamlessly without even thinking about it.
How to quickly switch to a different tab? I've defined some extra interactive functions that are just wrappers around the build-in method for changing the tabs, since I will use this feature in other places as well,
(defun tab-bar-files () "Select the files tab-bar. Force return `t'." (interactive) (tab-bar-select-tab-by-name "files") t) (defun tab-bar-eshells () "Select the eshells tab-bar. Force return `t'." (interactive) (tab-bar-select-tab-by-name "eshells") t) (defun tab-bar-exwm () "Select the wm tab-bar. Force return `t'." (interactive) (tab-bar-select-tab-by-name "exwm") t) (bind-keys ("s-F" . tab-bar-files) ("s-W" . tab-bar-exwm) ("s-Z" . tab-bar-eshells))
I'm also binding some keys to these functions. I'm using a programmable
keyboard, so I have plenty of keys to choose from. My binding is actually C-x t
F
but the action I need to take is just pressing one of the big thumb cluster
keys and f
. I want my most used commands to be reachable with the fewest keys
possible.
The previous setup takes care of grouping the buffers in their respective tabs and switching to the tabs whenever I need to switch between code, shell and browser.
The next step is to open buffers into their correct tab regardless of the tab
that I'm currently located in. That is, if I'm using the browser and I'm in the
exwm
tab, for example, and I want to switch buffers with C-x b
(ivy-switch-buffer
in my case), and select an eshell buffer from the list of
available buffers, I want it to open in the eshell
tab automatically and not
in the exwm
tab I'm currently in. Otherwise I will soon get an ugly mix of
buffers everywhere or I'll have to pay attention and switch to the correct tab
every time I want to switch buffers. Not so pretty. So how do I do it?
I'm advising the function that does the buffer switching. How does advising
work? Let's say I have a package defining a function myf
,
(defun myf (n) (message "The number: %d" 5)) ;; Eval and check the *Messages* buffer (myf 5) The number: 5
Assume I want to modify this function and have it print out my own message before its message. Something like this,
(defun myf (n) (message "I've called myf with argument %d" n) (message "The number: %d" 5)) ;; Eval and check the *Messages* buffer (myf 5) I’ve called myf with argument 5 The number: 5
It gets the job done. But if the package owner updates myf
function in the
future, and the said package depends on this new updated function for its
behavior, I will be left with the old implementation + my code. That's bad. I
would have to copy the new function and add my code to it every time this kind of
thing happens otherwise things might start to fall apart. There is a better
approach with advice's,
(defun myf (n) (message "The number: %d" 5)) (myf 5) The number: 5 (advice-add 'myf :before (lambda (n) (message "I've called myf with argument %d" n))) (myf 5) I’ve called myf with argument 5 The number: 5
Concretely, I've called advice-add
with my function as a symbol, together with
a new function of one argument, since the original myf
function also expects
one argument, and the keyword :before
. The result is exactly the same as the
previous attempt where I've modified the function itself. But the advantage is
greater with advising, since if the package containing myf
gets updated, I
don't have to modify anything to get the same behavior I want,
(defun myf (n) (message "The number is: %d" 5)) (myf 5) I’ve called myf with argument 5 The number is: 5
I get my own message as before, plus the updated message from myf
. This is
perfect!
I can use this great feature to advice the function that does the buffer
switching, in my case ivy-switch-buffer
but you should adapt it for your own
needs. That is, I want to make sure I open the buffer in the correct tab, so I
don't have to manually switch tabs before actually opening the buffer. Looking
at it in detail, advising ivy-switch-buffer
is a bit wrong, since that
function only opens a list of buffers for narrowing and selection and does not
make the buffer selection itself. The correct function to advice is the function
that gets called once I select a buffer. So I call C-h f ivy-switch-buffer
and
check it's definition,
(defun ivy-switch-buffer () "Switch to another buffer." (interactive) (ivy-read "Switch to buffer: " #'internal-complete-buffer :keymap ivy-switch-buffer-map :preselect (buffer-name (other-buffer (current-buffer))) :action #'ivy--switch-buffer-action :matcher #'ivy--switch-buffer-matcher :caller 'ivy-switch-buffer))
Ok, the function I'm interested in is ivy--switch-buffer-action
. That's an
internal function and ideally I should not depend or use it in any way, but I
went for it in this case, anyway. So now I have this,
;; Make sure `ivy-switch-buffer' opens each buffer in it's correct tab, ;; depending on its mode. (advice-add 'ivy--switch-buffer-action :before #'switch-to-tab-based-on-mode) (defun switch-to-tab-based-on-mode (buff) "Switch to the desired tab-bar, depending on BUFF mode." (or (and (buffer-mode-p buff 'eshell-mode) ;; open eshell buffers in the eshells tab (tab-bar-eshells)) (and (buffer-mode-p buff 'exwm-mode) ;; ..browsers and the like in the exwm tab (tab-bar-exwm)) ;; ...and the rest of the buffers in the files tab (tab-bar-files))) (defun buffer-mode-p (buffer-or-string mode) "Returns the major mode associated with a buffer." (with-current-buffer buffer-or-string (equal major-mode mode)))
I hope that is clear. Before calling ivy--switch-buffer-action
, which is the
function invoked when I press RET on the buffers list, I want to switch to the
correct tab based on the buffer's major mode. So if I'm writing some piece of
code in the files
tab and I want to switch to an eshell buffer to build my
project, let's say, that would automatically change the tab bar to
eshells
. Pretty neat. This is, of course, assuming I got the correct eshell
buffer already open in my eshells
tab. But I'll get to that in a moment.
Its a similar story when opening new files. I've shown how we can handle
switching buffers. That is one command. But it would be useful if say, I'm in an
eshell buffer and want to open a file, either interactively
(counsel-find-file
, for example), or directly from the command line with
find-file
. These are different commands than the one I've advised
before. Surely, I'd want that new buffer to get thrown in the files tab and not
poison my healthy eshells tab,
(advice-add 'find-file :around #'find-file-in-the-files-tab-advice) (defun find-file-in-the-files-tab-advice (orig-func filename &rest wildcards) "Open FILENAME in the files tab." (let ((dir default-directory)) (tab-bar-select-tab-by-name "files") (let ((default-directory dir)) (apply orig-func filename wildcards))))
Honestly, I can't tell why I haven't used a :before
advice in this case, as
well. Maybe there really is a reason and I'm not planing on fiddling with it now
since I'm writing this way after the fact. The :around
advice is a little more
complicated, but not conceptually. While in the previous advice, my new function
gets the argument passed when calling the original function, in this case I also
get the original function itself. It now depends on me to actually call the
original function after I did my job (or before, or between, it's my choice,
really), whereas before the original function was automatically called for me
after my new function, as the name implies. You can play around with some
examples. It really is neat and easy to grasp once you've seen an example or
two.
One thing I like to have is the ability to quickly open any of the most
used/visited buffers, like init.el
, the *scratch*
buffer, the browser or my
notes.org
. That is, I don't want to have to search through a list of all opened
buffers everytime I want to switch to any one of those. I want a quick access
key combination for that,
(defhydra hydra-file (:exit t) ("w" (lambda () (interactive) (switch-to-buffer "chromium" )) "browser") ("s" (lambda () (interactive) (switch-to-buffer "*scratch*")) "*scratch*") ("i" (lambda () (interactive) (switch-to-buffer "init.el" )) "init.el") ("c" (lambda () (interactive) (switch-to-buffer "notes.org")) "notes") ("q" nil "quit")) (bind-key ("s-f" . hydra-file/body))
So s-f w
goes to the chromium buffer, s-f i
to the init.el buffer, etc. But
I get the same problem as before, meaning, I want to quickly go to these buffers
but at the same time let Emacs automatically switch tabs for me. So instead of
switch-to-buffer
we can have a function that switches tabs first, and only
then calls switch-to-buffer
. I have tried to advice the switch-to-buffer
function itself, but I get some unwanted side-effects. I assume it is because
that function is used in a whole lot of places and in different packages to do
maybe some non-orthodox stuff, unlike the ivy one you've seen before which is
used only by the ivy package. Since there are, presumably, only a few very
important buffers, we can do something like this,
(defhydra hydra-file (:exit t) ("w" (lambda () (interactive) (select-buffer-in-tab "chromium" "exwm")) "browser") ("s" (lambda () (interactive) (select-buffer-in-tab "*scratch*" "files")) "*scratch*") ("i" (lambda () (interactive) (select-buffer-in-tab "init.el" "files")) "init.el") ("m" (lambda () (interactive) (select-buffer-in-tab "*Messages*" "files")) "*Messages*") ("c" (lambda () (interactive) (select-buffer-in-tab "cooking.org" "files")) "cooking") ("q" nil "quit")) (defun select-buffer-in-tab (buffer tab) "Select `buffer' in `tab'." (tab-bar-select-tab-by-name tab) (switch-to-buffer buffer))
One extra nice thing we can do, is that in case of chromium, and maybe other
such buffers in the exwm tab, if the chromium buffer is not available, meaning
we didn't start the browser yet, we can have s-f w
switch to the exwm tab and
start chromium at the same time,
(defun select-buffer-in-tab (buffer tab) "Select or create `buffer' in `tab'." (tab-bar-select-tab-by-name tab) (if (and (equal tab "exwm") (not (get-buffer buffer))) (exwm-start-app buffer) (switch-to-buffer buffer)))
With tens of maybe hundreds of buffers open at the same time, it becomes
difficult to switch between them. I've assigned the next-buffer
and the
previous-buffer
to C-,
and C-.
for quick access for a long time now. So if
I have two or three buffers that I work on at the same time, I can quickly
switch between them without having to select the buffer from the buffer list
with ivy each time. But there is also an improvement to this. Say you have two
or three browsers opened at the same time. When you're in the exwm buffer, and
you press next-buffer
, you want to go from chromium -> firefox ->
some-other-gui-app -> chromium -> firefox, etc. Similar to a buffer toggle but
only between buffers that belong to the same tab.
;; Next and previous buffer commands look for the same major mode in case of ;; eshell and exwm buffers. (bind-keys ("C-," . previous-buffer-maybe-same-major-mode) ("C-." . next-buffer-maybe-same-major-mode)) (defun next-buffer-maybe-same-major-mode () "Like `next-buffer' for buffers in the current `major-mode'." (interactive) (change-buffer-maybe-same-major-mode 'next-buffer)) (defun previous-buffer-maybe-same-major-mode () "Like `previous-buffer' for buffers in the current `major-mode'." (interactive) (change-buffer-maybe-same-major-mode 'previous-buffer)) (defun change-buffer-maybe-same-major-mode (change-buffer) "Call CHANGE-BUFFER until the current buffer has the initial `major-mode'. Only apply this behavior on selected tabs." (let ((tab-name (alist-get 'name (tab-bar--current-tab)))) (if (member tab-name '("eshells" "exwm")) (let ((mode major-mode) (original-buffer (current-buffer))) (funcall change-buffer) (while (and (not (eq major-mode mode)) (not (eq original-buffer (current-buffer)))) (funcall change-buffer))) (funcall change-buffer))))
I'm using a method that checsk the major-mode of the current buffer and gets the
next buffer with the same major-mode as the current one. Surely, this is only
useful for UI buffers in the exwm
tab, since they all have exwm-mode
and the
eshells
. If you have dired
buffers or similar, it might apply there to. For
the files
tab, which contains code from different languages, for example, it
doesn't make much sense, so I'm not appling this change in this last case.
But this brings up another problem. Sometimes you want to have a pdf or tutorial
from the web side-by-side with a buffer where to try out the stuff you're
reading. Or a shell right below your code buffer where you try your builds. In
this case, you really do want to have buffers that naturally belong to different
tabs in the same tab. Fortunately, there is an easy solution for that with the
universal argument, and it only implies adding one line to the already defined
switch-to-tab-based-on-mode
function,
(advice-add 'ivy--switch-buffer-action :before #'switch-to-tab-based-on-mode) (defun switch-to-tab-based-on-mode (buff) "Switch to the desired tab-bar, depending on BUFF mode." ;; With universal argument, do nothing, open the the buffer normally. (unless current-prefix-arg (or (and (buffer-mode-p buff 'eshell-mode) ;; Otherwise, open eshell buffers in the eshells tab (tab-bar-eshells)) (and (buffer-mode-p buff 'exwm-mode) ;; ..browsers and the like in the exwm tab (tab-bar-exwm)) ;; ...and the rest of the buffers in the files tab (tab-bar-files))))
So calling C-u
before the switch buffer command would be as if we haven't
advised our switch buffer command in the first place.
Sometimes when writing code, I want to build that said code from the command line, or want to create some folder structure directly from the command line. In short, I want to get as quickly as possible to my eshell and then back to my code,
("s-e" . open-eshell-in-eshells-tab-here) (defun open-eshell-in-eshells-tab-here () (interactive) ;;remember the pwd from where we've started, since we're changing tabs and ;;buffers and the pwd as a result (let ((dir default-directory)) (tab-bar-eshells) (let ((default-directory dir)) (eshell 'new-shell))))
Straightforward. I'm remembering the current working directory of the buffer
I'm opening the eshell buffer from, then switch to the eshells
tab and then
open a new eshell buffer. One small gotcha, though. If there is already an
eshell buffer opened for the exact same location, I'm not switching to it, but
opening a new one. It would be nicer to reuse the same buffer, if it exists or
create a new one if it doesn't,
(defun open-eshell-in-eshells-tab-here () "Reuse or open a new eshell in the eshells tab and switch to it. If there already is an eshell with the same pwd as the file we're opening it from, switch to it; otherwise, create a new eshell and set it's name accordingly." (interactive) ;;remember the pwd from where we've started, since we're chainging tabs and ;;buffers and the pwd as a result (let ((dir default-directory) (existing)) (tab-bar-eshells) (mapcar (lambda (buff) (and (equal (buffer-local-value 'default-directory buff) dir) (progn (setf existing t) t) (switch-to-buffer buff))) (all-buffers-for-major-mode 'eshell-mode)) (unless existing (let ((default-directory dir) (eshell-buffer-name (format "*eshell%s" dir))) (eshell 'new-shell)))))
I'm also renaming the buffer to reflect its location. That way, when switching
buffers with C-x b
, I can easily filter the candidates. To make it consistent
though, i want to change the buffer name when I change its directory with cd
,
(add-hook 'eshell-directory-change-hook (lambda () (rename-buffer (format "*eshell%s" default-directory) t)))
An org link can be opened directly in the browser, for example, or I sometimes
google-search a selected region from code to search for some documentation. In
the same vein, it would be nice if these browse functionalities would switch to
the exwm
bar before calling the browser. There is nothing new here,
(advice-add 'browse-url :before (lambda (url) (tab-bar-exwm)))
It's as simple as that. I'm advising the browse-url
function and telling it
to do its business as before, only change to the exwm tab before that.
I already have a nice split-up of tabs and buffers. It keeps everything in its
own place, nice and clean. But usually I'll only have a few eshell buffers and a
few UI buffers but most of the buffers and work in Emacs will be done in files
tab. That is, writing code, sometimes for more than one project at the same
time, write blog posts, taking notes, etc. I can either make new tabs for each
of these setups, since on different projects I have different needs regarding
the window configuration (how many splits I have, maybe I have a pdf open, or an
eshell buffer together with my code). You get the point. I can certainly do
that and bind some easy reached keys for each of those workspaces. Or, I can
use some window registers,
(bind-keys ("C-M-u" . (lambda (interactive) (window-configuration-to-register 'u))) ("C-M-i" . (lambda (interactive) (window-configuration-to-register 'i))) ("C-M-o" . (lambda (interactive) (window-configuration-to-register 'o))) ("M-s-u" . (lambda (interactive) (jump-to-register 'u))) ("M-s-i" . (lambda (interactive) (jump-to-register 'i))) ("M-s-o" . (lambda (interactive) (jump-to-register 'o))))
These are just wrappers around the already existing functionality. They only
allow for quick saving and jumping between different window configurations, by
using the exact same key but different modifiers. Easier to remember than the
default C-x r w u
, for setting the u
register, for example.
And that's it! I hope you can use this as an inspiration for your own setup and slowly but surely use Emacs to handle all your computing needs.