From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED.blaine.gmane.org!not-for-mail From: Robert Cochran Newsgroups: gmane.emacs.devel Subject: Re: [PATCH] tab-bar.el: add defcustoms for functions to call after opening and before closing Date: Mon, 02 Dec 2019 14:46:46 -0800 Message-ID: <87zhgawdk9.fsf@cochranmail.com> References: <87h83egmzy.fsf@cochranmail.com> <87r22fzhag.fsf@mail.linkov.net> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: blaine.gmane.org; posting-host="blaine.gmane.org:195.159.176.226"; logging-data="36776"; mail-complaints-to="usenet@blaine.gmane.org" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/27.0.50 (gnu/linux) Cc: Robert Cochran , emacs-devel@gnu.org To: Juri Linkov Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Mon Dec 02 23:47:12 2019 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([209.51.188.17]) by blaine.gmane.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) (envelope-from ) id 1ibuTG-0009PX-0c for ged-emacs-devel@m.gmane.org; Mon, 02 Dec 2019 23:47:10 +0100 Original-Received: from localhost ([::1]:45320 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1ibuTE-00031S-PF for ged-emacs-devel@m.gmane.org; Mon, 02 Dec 2019 17:47:08 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]:35427) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1ibuT5-000312-Ul for emacs-devel@gnu.org; Mon, 02 Dec 2019 17:47:02 -0500 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1ibuT3-0000PB-R9 for emacs-devel@gnu.org; Mon, 02 Dec 2019 17:46:59 -0500 Original-Received: from mail.cochranmail.com ([64.140.150.170]:43330) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1ibuT3-0000FA-9G for emacs-devel@gnu.org; Mon, 02 Dec 2019 17:46:57 -0500 Original-Received: from SoraLaptop (unknown [71.212.134.62]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.cochranmail.com (Postfix) with ESMTPSA id 1FF59178E; Mon, 2 Dec 2019 14:46:50 -0800 (PST) In-Reply-To: <87r22fzhag.fsf@mail.linkov.net> (Juri Linkov's message of "Sun, 10 Nov 2019 22:42:15 +0200") X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 64.140.150.170 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.org gmane.emacs.devel:243023 Archived-At: --=-=-= Content-Type: text/plain Hi all, I've taken a little bit of time to rethink how I wanted this to work. Namely: 1. Trying to make a single hook do two separate things (determining whether to prevent tab closing and tasks on tab close) just made things overly complicated. Tab close prevention has been split off into a new hook, `tab-bar-prevent-close-functions'. 2. I realized that it's possible when creating a new tab to just delay the actual creation of the tab in the frame's tab list. This makes it possible to directly modify the tab passed in to tab-bar-tab-post-open-functions, ie `(setf (alist-get 'name tab) "New Name")` from within a hook function. This means it's not really necessary to make new accessors. To be honest, I've been having a hard time coming up with good examples of how to use these hooks that aren't arbitrary, contrived, or don't otherwise require support in other places (more than one of the possible examples I was thinking of works best when another mode or command, such as gdb or mpc, has the ability to automatically start in a new tab). New patch, as well as a file of examples for the new hooks, follow. Again, these new hooks still need to be documented in the manual, which I will be glad to do as soon as a design is nailed down. Thanks, -- ~Robert Cochran --=-=-= Content-Type: text/x-patch Content-Disposition: inline; filename=0001-Add-hooks-for-after-tab-open-before-close-and-to-pre.patch Content-Description: Patch for post-open, pre-close, and close prevent hooks for tabs >From d433b8075ec461ea293a2f95f3dbb7768fc0ad1e Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Fri, 8 Nov 2019 11:29:43 -0800 Subject: [PATCH] Add hooks for after tab open, before close, and to prevent closing * lisp/tab-bar.el (tab-bar-tab-post-open-functions, tab-bar-tab-prevent-close-functions, tab-bar-tab-pre-close-functions): New defcustoms (tab-bar-new-tab-to, tab-bar-close-tab): Use new defcustoms --- lisp/tab-bar.el | 132 +++++++++++++++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 45 deletions(-) diff --git a/lisp/tab-bar.el b/lisp/tab-bar.el index 5eb332884c..51cbc16aba 100644 --- a/lisp/tab-bar.el +++ b/lisp/tab-bar.el @@ -659,6 +659,15 @@ tab-bar-new-tab-to :group 'tab-bar :version "27.1") +(defcustom tab-bar-tab-post-open-functions nil + "List of functions to call after creating a new tab. +The current tab is supplied as an argument. Any modifications +made to the tab argument will be applied after all functions are +called." + :type '(repeat function) + :group 'tab-bar + :version "27.1") + (defun tab-bar-new-tab-to (&optional to-index) "Add a new tab at the absolute position TO-INDEX. TO-INDEX counts from 1. If no TO-INDEX is specified, then add @@ -693,6 +702,10 @@ tab-bar-new-tab-to ('right (1+ (or from-index 0))))))) (setq to-index (max 0 (min (or to-index 0) (length tabs)))) (cl-pushnew to-tab (nthcdr to-index tabs)) + + (run-hook-with-args 'tab-bar-tab-post-open-functions + (nth to-index tabs)) + (when (eq to-index 0) ;; pushnew handles the head of tabs but not frame-parameter (set-frame-parameter nil 'tabs tabs))) @@ -747,6 +760,24 @@ tab-bar-close-last-tab-choice :group 'tab-bar :version "27.1") +(defcustom tab-bar-tab-prevent-close-functions nil + "List of functions to call to determine whether to close a tab. +The tab to be closed and a boolean indicating whether or not it +is the only tab in the frame are supplied as arguments. If any +function returns a non-nil value, the tab will not be closed." + :type '(repeat function) + :group 'tab-bar + :version "27.1") + +(defcustom tab-bar-tab-pre-close-functions nil + "List of functions to call before closing a tab. +The tab to be closed and a boolean indicating whether or not it +is the only tab in the frame are supplied as arguments, +respectively." + :type '(repeat function) + :group 'tab-bar + :version "27.1") + (defun tab-bar-close-tab (&optional arg to-index) "Close the tab specified by its absolute position ARG. If no ARG is specified, then close the current tab and switch @@ -759,52 +790,63 @@ tab-bar-close-tab (interactive "P") (let* ((tabs (funcall tab-bar-tabs-function)) (current-index (tab-bar--current-tab-index tabs)) - (close-index (if (integerp arg) (1- arg) current-index))) - (if (= 1 (length tabs)) - (pcase tab-bar-close-last-tab-choice - ('nil - (signal 'user-error '("Attempt to delete the sole tab in a frame"))) - ('delete-frame - (delete-frame)) - ('tab-bar-mode-disable - (tab-bar-mode -1)) - ((pred functionp) - ;; Give the handler function the full extent of the tab's - ;; data, not just it's name and explicit-name flag. - (funcall tab-bar-close-last-tab-choice (tab-bar--tab)))) - - ;; More than one tab still open - (when (eq current-index close-index) - ;; Select another tab before deleting the current tab - (let ((to-index (or (if to-index (1- to-index)) - (pcase tab-bar-close-tab-select - ('left (1- current-index)) - ('right (if (> (length tabs) (1+ current-index)) - (1+ current-index) - (1- current-index))) - ('recent (tab-bar--tab-index-recent 1 tabs)))))) - (setq to-index (max 0 (min (or to-index 0) (1- (length tabs))))) - (tab-bar-select-tab (1+ to-index)) - ;; Re-read tabs after selecting another tab - (setq tabs (funcall tab-bar-tabs-function)))) - - (let ((close-tab (nth close-index tabs))) - (push `((frame . ,(selected-frame)) - (index . ,close-index) - (tab . ,(if (eq (car close-tab) 'current-tab) - (tab-bar--tab) - close-tab))) - tab-bar-closed-tabs) - (set-frame-parameter nil 'tabs (delq close-tab tabs))) - - (when (and tab-bar-mode - (and (natnump tab-bar-show) - (<= (length tabs) tab-bar-show))) - (tab-bar-mode -1)) + (close-index (if (integerp arg) (1- arg) current-index)) + (last-tab-p (= 1 (length tabs))) + (prevent-close (run-hook-with-args-until-success + 'tab-bar-tab-prevent-close-functions + (nth close-index tabs) + last-tab-p))) + + (unless prevent-close + (run-hook-with-args 'tab-bar-tab-pre-close-functions + (nth close-index tabs) + last-tab-p) + + (if (= 1 (length tabs)) + (pcase tab-bar-close-last-tab-choice + ('nil + (signal 'user-error '("Attempt to delete the sole tab in a frame"))) + ('delete-frame + (delete-frame)) + ('tab-bar-mode-disable + (tab-bar-mode -1)) + ((pred functionp) + ;; Give the handler function the full extent of the tab's + ;; data, not just it's name and explicit-name flag. + (funcall tab-bar-close-last-tab-choice (tab-bar--tab)))) + + ;; More than one tab still open + (when (eq current-index close-index) + ;; Select another tab before deleting the current tab + (let ((to-index (or (if to-index (1- to-index)) + (pcase tab-bar-close-tab-select + ('left (1- current-index)) + ('right (if (> (length tabs) (1+ current-index)) + (1+ current-index) + (1- current-index))) + ('recent (tab-bar--tab-index-recent 1 tabs)))))) + (setq to-index (max 0 (min (or to-index 0) (1- (length tabs))))) + (tab-bar-select-tab (1+ to-index)) + ;; Re-read tabs after selecting another tab + (setq tabs (funcall tab-bar-tabs-function)))) + + (let ((close-tab (nth close-index tabs))) + (push `((frame . ,(selected-frame)) + (index . ,close-index) + (tab . ,(if (eq (car close-tab) 'current-tab) + (tab-bar--tab) + close-tab))) + tab-bar-closed-tabs) + (set-frame-parameter nil 'tabs (delq close-tab tabs))) + + (when (and tab-bar-mode + (and (natnump tab-bar-show) + (<= (length tabs) tab-bar-show))) + (tab-bar-mode -1)) - (force-mode-line-update) - (unless tab-bar-mode - (message "Deleted tab and switched to %s" tab-bar-close-tab-select))))) + (force-mode-line-update) + (unless tab-bar-mode + (message "Deleted tab and switched to %s" tab-bar-close-tab-select)))))) (defun tab-bar-close-tab-by-name (name) "Close the tab by NAME." -- 2.23.0 --=-=-= Content-Type: text/plain --- patch ends here --- tab-buffer.el begins here --- --=-=-= Content-Type: text/plain Content-Disposition: inline; filename=tab-buffers.el Content-Description: Example functions for pre-open/post-close/prevent-close hooks ;; -*- lexical-binding: t -*- (defmacro tab-ex/save-tab-excursion (&rest body) (declare (indent defun)) (let ((old-tab-sym (gensym))) `(let ((,old-tab-sym (tab-bar--current-tab-index))) (unwind-protect (progn ,@body) (tab-bar-select-tab (1+ ,old-tab-sym)))))) (defmacro tab-ex/with-current-tab (new-tab &rest body) (declare (indent 1)) `(tab-ex/save-tab-excursion (tab-bar-select-tab (1+ (tab-bar--tab-index ,new-tab))) ;; No need to wrap body in a progn here; we're already in a progn in the ;; parent macro. ,@body)) (defun tab-ex/tab-buffers (tab) ;; Windows aren't usable after exiting `tab-ex/with-current-tab's ;; scope, so you have to extract the buffers out of them *then*, ;; otherwise they go stale (likely as a result of them no longer ;; being 'visible') and you can't pull the buffers out of them ;; anymore (delete-dups (if (eq (car tab) 'current-tab) (mapcar #'window-buffer (window-list-1 (frame-first-window) 'nomini)) (tab-ex/with-current-tab tab (mapcar #'window-buffer (window-list-1 (frame-first-window) 'nomini)))))) (defun tab-ex/gud-tab-explicit-name (tab) "Rename tab and enable gud-many-buffers when tab is made with the gud core buffer. When using GUD in many-buffers mode, rename the tab to something static so that it's easy to tell at a glance what the tab contains despite the various buffer names." (let ((tab-buffers (tab-ex/tab-buffers tab))) (dolist (buf tab-buffers) (with-current-buffer buf (when (and (eq major-mode 'gud-mode)) ;; It's probably better to have a way to just have gud make ;; a new tab and always have many windows on, but this is ;; just an example. (gdb-many-windows) (save-match-data (string-match "\\*gud-\\(.*\\)\\*" (buffer-name)) (setf (alist-get 'name tab) (format "GUD: %s" (match-string 1 (buffer-name))) (alist-get 'explicit-name tab) t))))))) (defun tab-ex/tab-has-unsaved-files (tab _lastp) ;; Don't let the user close a tab if they have unsaved buffers, to ;; make sure they don't lose track of unsaved buffers, for ;; example. In this case, it doesn't matter if this tab is the last ;; one or not. (cl-loop for buffer in (tab-ex/tab-buffers tab) always (and (with-current-buffer buffer buffer-file-name) (not (buffer-modified-p buffer))))) (defun tab-ex/kill-non-file-buffers-in-tab (tab lastp) ;; Don't kill the buffers if the tab is going to still be visible in ;; the end of it. (unless lastp (tab-ex/with-current-tab tab ;; Just kill all the visible non-file buffers, for the sake of example. (mapc #'kill-buffer (cl-loop for buffer in (tab-ex/tab-buffers tab) unless (with-current-buffer buffer buffer-file-name) collect buffer))))) (add-hook 'tab-bar-tab-post-open-functions #'tab-ex/gud-tab-explicit-name) (add-hook 'tab-bar-tab-prevent-close-functions #'tab-ex/tab-has-unsaved-files) (add-hook 'tab-bar-tab-pre-close-functions #'tab-ex/kill-non-file-buffers-in-tab) --=-=-=--