25

I would like tabs like firefox but for emacs.

I found this: http://emacswiki.org/emacs/TabBarMode

But it just add to each buffer (window in Emacs terminology) a bar that shows currently open buffers.

I would like a tab to be able to hold multiple buffers (windows in Emacs teminology), which I can split as desired. I.e. each tab should correspond to a "window state" (in the sense of window-state-get).

I'd have one tab for my tasks, another for code, another for web-reading etc..

Is this possible? Can tabbar be tailored to do this?

[edit2]
This question has attracted more attention than I anticipated. It looks like there is a solution, but one that would require a bit of research and tweaking. While this/next week are a bit busy for me, I will parse through the answers and will try to build something that works and then I'll edit this question to reflect my findings. Please hold on =)

[edit]
Sort of similar to :
https://stackoverflow.com/questions/24157754/make-frames-in-emacs-gui-behaves-like-frames-in-terminal

I would settle with multiple frames in a single GUI session also.

14
  • 2
    "I would like a tab to be able to hold multiple buffers, which I can split as desired." Do you mean multiple windows?
    – Malabarba
    Mar 17, 2015 at 17:09
  • 1
    It's more like I'd like to have very dyanmic tabs. I would create them and then populate them with windows. I.e I'd like a tab to be a frame. Then a new tab a new frame. Inside each tab/frame I could open the desired windows/(buffers). Is this doable? (I.e, no hard-coded buffer names etc..) Mar 17, 2015 at 18:37
  • 1
    There is a variable associated with particular windows, but it's been a month or two since I saw a thread that talked about it and I don't know what it is called off-hand. You may be interested in using a system similar to frame-bufs where a list holds the buffers associated with a frame and that list is incorporated into the frame parameter. You could use the variable associated with a particular window and and make it a list, add / remove buffers from the list -- that list would be a buffer-group to be used by tabbar. This is all theoretical, but I believe it would work.
    – lawlist
    Mar 17, 2015 at 19:36
  • 1
    I think you may refer to : stackoverflow.com/questions/24157754/… but that post doesn't seem to have a solid answer :-/ Mar 17, 2015 at 19:41
  • 1
    I would recommend taking a look at the elscreen package. Mar 17, 2015 at 22:16

5 Answers 5

10

Split buffers in groups

It is possible with tabbar. You can add rules to group buffers in groups. Here's a basic snippet:

(defun tabbar-buffer-groups ()
  "Returns the list of group names the current buffer belongs to."
  (list
   (cond

    ;; ADD RULES TO SPLIT BUFFERS IN GROUPS HERE!

    ;; if buffer is not grouped by the rules you would add above 
    ;; put it in the "General" group:
    (t
       "General"
     ))))

Example rules:

  1. List buffer names:
    ((member (buffer-name)
             '("*scratch*" "*Messages*" "*Help*"))
     "Common" ;; this is a group name
     )
  1. Regarding common buffers I prefer to put in "Common" each buffer which name starts with a star. This gives an example of making a buffer for this rule:
    ((string-equal "*" (substring (buffer-name) 0 1))
     "Common"
     )
  1. Here's an example of grouping buffers by major-mode:
    ((memq major-mode
           '(org-mode text-mode rst-mode))
     "Text"
     )
  1. Here's an example of grouping buffers based on mode they are derived from:
    ((or (get-buffer-process (current-buffer))
         ;; Check if the major mode derives from `comint-mode' or
         ;; `compilation-mode'.
         (tabbar-buffer-mode-derived-p
          major-mode '(comint-mode compilation-mode)))
     "Process"
     )
  1. Here's an example of grouping tabs by regexp:
    ((string-match "^__" (buffer-name))
     "Templates"
     )
  1. Group buffers by major mode:
    (if (and (stringp mode-name)
                  ;; Take care of preserving the match-data because this
                  ;; function is called when updating the header line.
                  (save-match-data (string-match "[^ ]" mode-name)))
             mode-name
           (symbol-name major-mode))

Once you composed the rules, you can press on + or - on tabbar's tab line to toggle groups, and also ◀ and ▶ to switch between buffers. Or just bind the following defuns:

tabbar-forward
tabbar-backward
tabbar-forward-group
tabbar-backward-group

and move between tabs and tab groups with keyboard.

Personally I group tabs, so that I see what is open, but navigate them with ido-switch-buffer.

Switch between set of rules

Also one can define different set of buffer grouping rules and cycle between these. Here's an example of cycling between two set of buffer grouping rules:

;; tab-groups!
(setq tbbr-md "common")
(defun toggle-tabbar-mode ()
  "Toggles tabbar modes - all buffers vs. defined in the `tabbar-buffer-groups'."
  (interactive)
  (if (string= tbbr-md "groups")
      (progn ;; then
        (setq tabbar-buffer-groups-function 'tabbar-buffer-groups-common)
        (setq tbbr-md "common"))
    (progn ;; else
      (setq tabbar-buffer-groups-function 'tabbar-buffer-groups)
      (setq tbbr-md "groups"))))
;; by default - all tabs:
(setq tabbar-buffer-groups-function 'tabbar-buffer-groups-common)

This toggles between tabbar-buffer-groups-common and tabbar-buffer-groups tab grouping defuns.

Sort tabbar buffers by name

I find it beneficial to sort tabbar buffers by name. Here's how to get it:

(defun tabbar-add-tab (tabset object &optional append_ignored)
  "Add to TABSET a tab with value OBJECT if there isn't one there yet.
If the tab is added, it is added at the beginning of the tab list,
unless the optional argument APPEND is non-nil, in which case it is
added at the end."
  (let ((tabs (tabbar-tabs tabset)))
    (if (tabbar-get-tab object tabset)
        tabs
      (let ((tab (tabbar-make-tab object tabset)))
        (tabbar-set-template tabset nil)
        (set tabset (sort (cons tab tabs)
                          (lambda (a b) (string< (buffer-name (car a)) (buffer-name (car b))))))))))
2
  • Thank you for the detailed answer. I will try to get to trying out the above and will let you know ~eventually :) Mar 18, 2015 at 20:00
  • But the OP doesn't want "one tabbar per window", he wants one tabbar per frame and each tab in the tabbar should represent a "window config" (i.e. several windows) rather than a buffer, so grouping buffers is not the issue.
    – Stefan
    Jul 5, 2019 at 13:03
6

ATTRIBUTE: The grouping of buffers on a per frame basis is a direct implementation of the concepts and select portions of code developed / written by Alp Aker in the library frame-bufs: https://github.com/alpaker/Frame-Bufs

The following is an example of how to use the library tabbar.el and group tabs/buffers dynamically on a per-frame basis by either adding tabs/buffers with C-c C-a or removing tabs/buffers with C-c C-n. There are only two (2) groups -- associated with the current frame (i.e., "A"), and NOT associated with the current frame (i.e., "N"). The groups are frame-local, which means that each frame can have its own grouping. The custom grouping can be reset with C-c C-r. Switch between associated and non-associated groups with C-tab. Switch to the next tab/buffer in the current group with M-s-right. Switch to the previous tab/buffer in the current group with M-s-left.

Tabs/buffers can be added or removed programmatically with my-add-buffer and my-remove-buffer. For an example of how to open certain buffers in select frames, please see the related thread entitled How to intercept a file before it opens and decide which frame: https://stackoverflow.com/a/18371427/2112489 The function my-add-buffer would need to be incorporated at the appropriate locations of the code in the above link if a user chooses to implement that feature.

The user may wish to create an entry in a custom mode-line-format that displays the name of the current tab group in the mode-line by incorporating the following snippet: (:eval (when tabbar-mode (format "%s" (tabbar-current-tabset t)))) Customizing the mode-line in more detail, however, is beyond the scope of this example.

The function tabbar-add-tab has been modified so as to alphabetize the tabs/buffers.

The function tabbar-line-tab has been modified so as to provide four (4) different faces depending upon the situation. If the tab/buffer is associated with the frame and IS selected, then use tabbar-selected-associated face. If the tab/buffer is associated with the frame and NOT selected, then use tabbar-unselected-associated face. If the tab/buffer is NOT associated with the frame and IS selected, then use tabbar-selected-unassociated face. If the tab/buffer is NOT associated with the frame and is NOT selected, then use tabbar-unselected-unassociated face.

;; Download tabbar version 2.0.1 by David Ponce:
;;   https://marmalade-repo.org/packages/tabbar
;; or use package-install for marmalade repositories.

;; Place tabbar-2.0.1.el in the `load-path` -- it is okay to rename it to tabbar.el
;; or add the directory (where `tabbar.el` resides) to the `load-path`.
;; EXAMPLE:  (setq load-path (append '("/Users/HOME/.emacs.d/lisp/") load-path))

(require 'tabbar)

(setq tabbar-cycle-scope 'tabs)

(remove-hook 'kill-buffer-hook 'tabbar-buffer-track-killed)

(defun my-buffer-groups ()
  "Function that gives the group names the current buffer belongs to.
It must return a list of group names, or nil if the buffer has no
group.  Notice that it is better that a buffer belongs to one group."
  (list
    (cond
      ((memq (current-buffer) (my-buffer-list (selected-frame)))
        "A")
      (t
        "N"))))

(setq tabbar-buffer-groups-function 'my-buffer-groups) ;; 'tabbar-buffer-groups

;; redefine tabbar-add-tab so that it alphabetizes / sorts the tabs
(defun tabbar-add-tab (tabset object &optional append)
  "Add to TABSET a tab with value OBJECT if there isn't one there yet.
If the tab is added, it is added at the beginning of the tab list,
unless the optional argument APPEND is non-nil, in which case it is
added at the end."
  (let ((tabs (tabbar-tabs tabset)))
    (if (tabbar-get-tab object tabset)
        tabs
      (let* ((tab (tabbar-make-tab object tabset))
             (tentative-new-tabset
               (if append
                 (append tabs (list tab))
                 (cons tab tabs)))
             (new-tabset
               (sort
                  tentative-new-tabset
                  #'(lambda (e1 e2)
                     (string-lessp
                       (format "%s" (car e1)) (format "%s" (car e2)))))))
        (tabbar-set-template tabset nil)
        (set tabset new-tabset)))))

;; AUTHOR:  Alp Aker -- https://github.com/alpaker/Frame-Bufs
;; @lawlist extracted/revised the function(ality) from said library.
(defun my-buffer-list (frame)
  ;; Remove dead buffers.
  (set-frame-parameter frame 'frame-bufs-buffer-list
    (delq nil (mapcar #'(lambda (x) (if (buffer-live-p x) x))
      (frame-parameter frame 'frame-bufs-buffer-list))))
  ;; Return the associated-buffer list.
  (frame-parameter frame 'frame-bufs-buffer-list))

(defun my-kill-buffer-fn ()
"This function is attached to a buffer-local `kill-buffer-hook'."
  (let ((frame (selected-frame))
        (current-buffer (current-buffer)))
    (when (memq current-buffer (my-buffer-list frame))
      (my-remove-buffer current-buffer frame))))

;; AUTHOR:  Alp Aker -- https://github.com/alpaker/Frame-Bufs
;; @lawlist extracted/revised the function(ality) from said library.
(defun my-add-buffer (&optional buf frame)
"Add BUF to FRAME's associated-buffer list if not already present."
(interactive)
  (let* ((buf (if buf buf (current-buffer)))
         (frame (if frame frame (selected-frame)))
         (associated-bufs (frame-parameter frame 'frame-bufs-buffer-list)))
    (unless (bufferp buf)
      (signal 'wrong-type-argument (list 'bufferp buf)))
    (unless (memq buf associated-bufs)
      (set-frame-parameter frame 'frame-bufs-buffer-list (cons buf associated-bufs)))
    (with-current-buffer buf
      (add-hook 'kill-buffer-hook 'my-kill-buffer-fn 'append 'local))
    (when tabbar-mode (tabbar-display-update))))

;; AUTHOR:  Alp Aker -- https://github.com/alpaker/Frame-Bufs
;; @lawlist extracted/revised the function(ality) from said library.
(defun my-remove-buffer (&optional buf frame)
"Remove BUF from FRAME's associated-buffer list."
(interactive)
  (let ((buf (if buf buf (current-buffer)))
        (frame (if frame frame (selected-frame))))
    (set-frame-parameter frame 'frame-bufs-buffer-list
      (delq buf (frame-parameter frame 'frame-bufs-buffer-list)))
    (when tabbar-mode (tabbar-display-update))))

;; AUTHOR:  Alp Aker -- https://github.com/alpaker/Frame-Bufs
;; @lawlist extracted/revised the function(ality) from said library.
(defun my-buffer-list-reset ()
    "Wipe the entire slate clean for the selected frame."
  (interactive)
    (modify-frame-parameters (selected-frame) (list (cons 'frame-bufs-buffer-list nil)))
    (when tabbar-mode (tabbar-display-update)))

(defun my-switch-tab-group ()
"Switch between tab group `A` and `N`."
(interactive)
  (let ((current-group (format "%s" (tabbar-current-tabset t)))
        (tab-buffer-list (mapcar
            #'(lambda (b)
                (with-current-buffer b
                  (list (current-buffer)
                        (buffer-name)
                        (funcall tabbar-buffer-groups-function))))
                 (funcall tabbar-buffer-list-function))))
    (catch 'done
      (mapc
        #'(lambda (group)
            (when (not (equal current-group
                          (format "%s" (car (car (cdr (cdr group)))))))
              (throw 'done (switch-to-buffer (car (cdr group))))))
        tab-buffer-list))))

(defface tabbar-selected-associated
  '((t :background "black" :foreground "yellow" :box (:line-width 2 :color "yellow")))
  "Face used for the selected tab -- associated with the `frame-bufs-buffer-list`."
  :group 'tabbar)

(defface tabbar-unselected-associated
  '((t :background "black" :foreground "white" :box (:line-width 2 :color "white")))
  "Face used for unselected tabs  -- associated with the `frame-bufs-buffer-list`."
  :group 'tabbar)

(defface tabbar-selected-unassociated
  '((t :background "black" :foreground "white" :box (:line-width 2 :color "firebrick")))
  "Face used for the selected tab -- UNassociated with the `frame-bufs-buffer-list`."
  :group 'tabbar)

(defface tabbar-unselected-unassociated
  '((t :background "black" :foreground "white" :box (:line-width 2 :color "blue")))
  "Face used for unselected tabs -- UNassociated with the `frame-bufs-buffer-list`."
  :group 'tabbar)

(setq tabbar-background-color "black")

(defsubst tabbar-line-tab (tab)
  "Return the display representation of tab TAB.
That is, a propertized string used as an `header-line-format' template
element.
Call `tabbar-tab-label-function' to obtain a label for TAB."
  (concat
    (propertize
      (if tabbar-tab-label-function
          (funcall tabbar-tab-label-function tab)
        tab)
      'tabbar-tab tab
      'local-map (tabbar-make-tab-keymap tab)
      'help-echo 'tabbar-help-on-tab
      'mouse-face 'tabbar-highlight
      'face
        (cond
          ((and
              (tabbar-selected-p tab (tabbar-current-tabset))
              (memq (current-buffer) (my-buffer-list (selected-frame))))
            'tabbar-selected-associated)
          ((and
              (not (tabbar-selected-p tab (tabbar-current-tabset)))
              (memq (current-buffer) (my-buffer-list (selected-frame))))
            'tabbar-unselected-associated)
          ((and
              (tabbar-selected-p tab (tabbar-current-tabset))
              (not (memq (current-buffer) (my-buffer-list (selected-frame)))))
            'tabbar-selected-unassociated)
          ((and
              (not (tabbar-selected-p tab (tabbar-current-tabset)))
              (not (memq (current-buffer) (my-buffer-list (selected-frame)))))
            'tabbar-unselected-unassociated))
      'pointer 'hand)
    tabbar-separator-value))

(define-key global-map "\C-c\C-r" 'my-buffer-list-reset)

(define-key global-map "\C-c\C-a" 'my-add-buffer)

(define-key global-map "\C-c\C-n" 'my-remove-buffer)

(define-key global-map (kbd "<M-s-right>") 'tabbar-forward)

(define-key global-map (kbd "<M-s-left>") 'tabbar-backward)

(define-key global-map [C-tab] 'my-switch-tab-group)

(tabbar-mode 1)

The following screenshot depicts the two possible buffer/tab groupings: (1) on the left is a grouping of those buffers/tabs that are associated with the frame named SYSTEM [yellow and white tabs], with the capital letter "A" indicated in the mode-line; and (2) on the right is a grouping of those buffers/tabs that are NOT associated with the frame named SYSTEM [blue and red tabs], with a capital letter "N" indicated in the mode-line.

Example

1
  • But the OP doesn't want "one tabbar per window", he wants one tabbar per frame and each tab in the tabbar should represent a "window config" (i.e. several windows) rather than a buffer.
    – Stefan
    Jul 5, 2019 at 13:01
5

Consider checking out elscreen, though it doesn't actually group buffers.

What it does do is group windows and provide access to multiple layouts (tabs) that you can move between quickly. My workflow often has some Ruby code and associated tests in one screen, while my todo and Org notes are in another, and perhaps a scratch buffer for drafting SQL queries is in a third. This allows me to jump between tasks and projects easily, even though each screen draws upon the same pool of buffers.

4

How about my plugin, centaur-tabs? It has a lot of configuration options, it's really functional, it is supported by very popular themes like Kaolin Themes and overall is a really nice looking and aesthetic package (according to user's feedback). It's available in MELPA and it looks like this:

enter image description here

6
  • This seems to be yet another "each tab represents a buffer and each window has its own tabbar" non-solution.
    – Stefan
    Jul 5, 2019 at 13:03
  • I just added a customization to show tabs groups instead of tab names so in a function you establish rules (i.e. group elisp and lisp in one group, group c and c++ in another and so on) and in the tabs those groups are showed. Jul 8, 2019 at 4:21
  • 1
    It's still doesn't answer the question, where there should be one tabbar per frame (rather than per window) and each tab represents a window-configuration.
    – Stefan
    Jul 8, 2019 at 14:55
  • 1
    Ok I didn't undestand. I'll investigate and work on it! Jul 8, 2019 at 15:57
  • 1
    @Eduardo It's on MELPA! Look For centaur-tabs in Github for the installation instructions. Apr 14, 2020 at 19:23
0

Here is my config, for what is worth. It features:

  • Mouse operation on tabs (mouse-2 to close, like in browsers, mouse-3 to pop open in a new Emacs window, like in i3)
  • Keyboard controls (M-left & right switch tabs, like in TMux/Screen)
  • Colors (consistent with "Moe theme / moe-dark" config included)
  • Grouping (currently, Emacs *buffers* and "regular")
  • Auto-updated (with use-package)

TabBar

(use-package tabbar
  :ensure t
  :bind
  ("<M-left>" . tabbar-backward)
  ("<M-right>" . tabbar-forward)

  :config
  (set-face-attribute
   'tabbar-button nil
   :box '(:line-width 1 :color "gray19"))

  (set-face-attribute
   'tabbar-selected nil
   :foreground "orange"
   :background "gray19"
   :box '(:line-width 1 :color "gray19"))

  (set-face-attribute
   'tabbar-unselected nil
   :foreground "gray75"
   :background "gray25"
   :box '(:line-width 1 :color "gray19"))

  (set-face-attribute
   'tabbar-highlight nil
   :foreground "black"
   :background "orange"
   :underline nil
   :box '(:line-width 1 :color "gray19" :style nil))

  (set-face-attribute
   'tabbar-modified nil
   :foreground "orange red"
   :background "gray25"
   :box '(:line-width 1 :color "gray19"))

  (set-face-attribute
   'tabbar-selected-modified nil
   :foreground "orange red"
   :background "gray19"
   :box '(:line-width 1 :color "gray19"))

  (custom-set-variables
   '(tabbar-separator (quote (0.2))))

  (defun tabbar-buffer-tab-label (tab)
    "Return a label for TAB.
  That is, a string used to represent it on the tab bar."
    (let ((label  (if tabbar--buffer-show-groups
                      (format " [%s] " (tabbar-tab-tabset tab))
                    (format " %s " (tabbar-tab-value tab)))))
      (if tabbar-auto-scroll-flag
          label
        (tabbar-shorten
         label (max 1 (/ (window-width)
                         (length (tabbar-view
                                  (tabbar-current-tabset)))))))))

  (defun px-tabbar-buffer-select-tab (event tab)
    "On mouse EVENT, select TAB."
    (let ((mouse-button (event-basic-type event))
          (buffer (tabbar-tab-value tab)))
      (cond
       ((eq mouse-button 'mouse-2) (with-current-buffer buffer (kill-buffer)))
       ((eq mouse-button 'mouse-3) (pop-to-buffer buffer t))
       (t (switch-to-buffer buffer)))
      (tabbar-buffer-show-groups nil)))

  (defun px-tabbar-buffer-help-on-tab (tab)
    "Return the help string shown when mouse is onto TAB."
    (if tabbar--buffer-show-groups
        (let* ((tabset (tabbar-tab-tabset tab))
               (tab (tabbar-selected-tab tabset)))
          (format "mouse-1: switch to buffer %S in group [%s]"
                  (buffer-name (tabbar-tab-value tab)) tabset))
      (format "\
mouse-1: switch to %S\n\
mouse-2: kill %S\n\
mouse-3: Open %S in another window"
              (buffer-name (tabbar-tab-value tab))
              (buffer-name (tabbar-tab-value tab))
              (buffer-name (tabbar-tab-value tab)))))

  (defun px-tabbar-buffer-groups ()
    "Sort tab groups."
    (list (cond ((or
                  (eq major-mode 'dired-mode)
                  (string-equal "*" (substring (buffer-name) 0 1))) "emacs")
                (t "user"))))
  (setq tabbar-help-on-tab-function 'px-tabbar-buffer-help-on-tab
        tabbar-select-tab-function 'px-tabbar-buffer-select-tab
        tabbar-buffer-groups-function 'px-tabbar-buffer-groups)

  :init
  (tabbar-mode 1))

Annex 1 - Moe Theme

(use-package moe-theme
  :ensure t
  :config
  (progn
    (load-theme 'moe-dark :no-confirm)
    (set-face-attribute 'fringe nil :background "gray19")))

Annex 2 - Toggle the last 2 buffers (KB macro)

(define-key global-map [(control tab)] (kbd "C-x b <return>")) 

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.