Skip to content

Latest commit

 

History

History
783 lines (745 loc) · 34.8 KB

helm-q.org

File metadata and controls

783 lines (745 loc) · 34.8 KB

Manage remote q sessions with Helm and q-mode

Table of Contents

Preparation

how to develop with helm

https://github.com/emacs-helm/helm/wiki/Developing

load dependencies

(require 's)
(require 'cl-lib)
(require 'helm)
(require 'q-mode)
;;; Code:

custom variables

(defgroup helm-q nil
  "Helm mode for managing kdb+/q instances."
  :group 'helm)

(defcustom helm-q-config-directory "~/.helm-q/"
  "The directory containing kdb connection files."
  :group 'helm-q
  :type 'string)

(defcustom helm-q-password-storage nil ; Change to 'pass will enable password storage via the standard unix password manager.
  "The default storage method to use."
  :group 'helm-q
  :options '(nil pass)
  :type 'symbol)

(defcustom helm-q-qcon-buffer-name-pattern '("." (service env region))
  "The name matching pattern used to build the Q-Shell buffer name.
The first item is the concatenator to join the fields in second item.
These fields in second item can be found in file `instances-meta.json'."
  :group 'helm-q
  :type 'list)

Usage

Basic workflow

  1. Put some JSON files with the same format as it is in ./instances-meta.json to directory ~/.helm-q.
  2. Load file helm-q.el (for example invoking command load-~file). Now you can execute interactive command helm-q by press key Alt-x. if M-x helm-q was invoked with C-u prefix argument, and after instance was selected from helm-q buffer, We will prompt for new user and then for password in the minibuffer. and qcon will attempt to connect with the supplied user:password.
  3. (Optional) If you want to load helm-q.el after loading q-mode, please add following initialization code in your .emacs
;; Please be sure that `helm-q.el' can be found in `load-path'.
(eval-after-load "q-mode" (require 'helm-q))

Password storage

We use the standard unix password manager to store password in local encrypted file by default. So when connecting to a q process, helm-q will query password from it.

We should have a GPG key to encrypt password to file and decrypt it back, to obtain this GPG key, please read a tutorial of GPG(for example this one). After install the standard unix password manager, you have to set up it with a GPG key, for example:

pass init AA224F33CCCCCCE40E6C1C2E8A48C70000022222

Here AA224F33CCCCCCE40E6C1C2E8A48C70000022222 is the ID of your GPG key(in the output of the shell command gpg -k).

Let’s run a test to check the setup.

# insert a password
pass insert test
# If no error message for above command then it is ok, now let's retrieve the password.
pass test

To manage password in helm-q, please execute command helm-q and press key tab on a candidate, which will list available actions.

If you want to prompt for new user, please invoke M-x helm-q with C-u prefix argument.

The pattern for Q-Shell buffer name

helm-q manages the Q-Shell buffer name via variable helm-q-qcon-buffer-name-pattern, which is ("." (service env region)) by default. The first item in this list is the concatenator to join the fields in second item. The fields in second item can be found in file instances-meta.json.

For example, if file instances-meta.json contains instance with attributes:

service:"TAQ.HDB",
env:"prod",
region:"nam"

Then the Q-Shell buffer name for this instance will be *qcon-TAQ.HDB.prod.nam*, which contains

  • A prefix *qcon- and
  • a string joining fields service, env, region with a dot(.), and
  • a suffix *.

implementation

helm-q buffer class

We define a new helm source for helm-q, which describes the additional and overwritten attributes of helm-q.

(defclass helm-q-source (helm-source-sync)
  ((instance-list
    :initarg :instance-list
    :initform #'helm-q-instance-list-from-config-directory
    :custom function
    :documentation
    "  A function with no arguments to create instance list.")
   (candidate-columns
    :initform '(address service env region)
    :documentation "The columns used to display each candidate.")
   (candidate-columns-width-hash
    :initform (make-hash-table :test 'equal)
    :documentation "The width of each column in candidate-columns, key is the column symbol and value is the width of it.")
   (init :initform 'helm-q-source-list--init)
   (multimatch :initform t)
   (multiline :initform t)
   (match :initform 'helm-q-source-match-function)
   (action :initform
           '(("Connect to a pre-existing q process"            . helm-q-source-action-qcon)
             ("Display username/password for current instance" . helm-q-source-action-show-password)
             ("Add username/password for current instance"     . helm-q-source-action-add-password)
             ("Update username/password for current instance"  . helm-q-source-action-update-password)
             ))
   (filtered-candidate-transformer :initform 'helm-q-source-filtered-candidate-transformer)
   (migemo :initform 'nomultimatch)
   (volatile :initform t)
   (nohighlight :initform nil)
   ))

how to display a q instance in helm buffer

we will calculate the maximum width of each column to make sure each column will display with same width. That is, each column will have a width which is the maximum one in all the instances.

(defun helm-q-calculate-columns-width (instances)
  "Calculate columns width.
Argument INSTANCES: the instance list."
  (cl-loop with width-hash = (helm-attr 'candidate-columns-width-hash)
           for column in (helm-attr 'candidate-columns)
           do (cl-loop for instance in instances
                       for width = (length (cdr (assoc column instance)))
                       if (or (null (gethash column width-hash))
                              (> width (gethash column width-hash)))
                       do (setf (gethash column width-hash) width))))

Now we can build a display string with fixed size.

(defun helm-q-instance-display-string (instance)
  "Argument INSTANCE: one instance."
  (let ((first-row (s-join helm-buffers-column-separator
                           (cl-loop for column in (helm-attr 'candidate-columns)
                                    collect (helm-substring-by-width (format "%s" (cdr (assoc column instance)))
                                                                     (gethash column (helm-attr 'candidate-columns-width-hash))))))
        (context-matched-columns (helm-q-context-matched-columns instance)))
    (propertize
     (if (null context-matched-columns)
       (propertize first-row 'face 'bold)
       (concat (propertize first-row 'face 'bold) "\n"
               (s-join helm-buffers-column-separator
                       (cons helm-buffers-column-separator
                             context-matched-columns))))
     'instance instance)))

setup instance list

Normally the setup will be used by class helm-q-source to load instances from file instances-meta.json, but you can overwrite the slot instance-list to load instances as you want, so we write a routine to setup for incoming instances:

(defun helm-q-instance-list (instances)
  "Load source for instances.
Argument INSTANCES: the incoming list of instance."
  (helm-q-calculate-columns-width instances)
  ;; a list whose members are `(DISPLAY . REAL)' pairs.
  (cl-loop for instance in instances
           collect (cons (helm-q-instance-display-string instance) instance)))
(defun helm-q-instance-list-from-config-directory ()
  "Load source from json files in a directory."
  (require 'json)
  (helm-q-instance-list (cl-loop for file in (directory-files helm-q-config-directory t ".json$")
                                 append (cl-loop for instance across (json-read-file file)
                                                 collect instance))))

initialize helm-q-source

(defun helm-q-source-list--init ()
  "Initialize helm-q-source."
  (helm-attrset 'candidates (funcall (helm-attr 'instance-list))))

get one instance by its candidate display string

(defun helm-q-get-instance-by-display (display-str)
  "Get an instance by its display string.
Argument DISPLAY-STR: the display string."
  (cl-loop with candidates = (helm-attr 'candidates)
           for candidate in candidates
           when (string= display-str (car candidate))
           return (cdr candidate)))

match funtion

When match, we will test some columns that are not in candidate-columns, which will not display by default. For them, if it can match, we will return them so then can be added as additional lines for display.

(defun helm-q-context-matched-columns (instance)
  "Return a list of string for matched columns.
Argument INSTANCE: one instance."
  (unless (s-blank? helm-pattern)
    (let ((word-patterns (split-string helm-pattern)))
      (append
       (cl-loop for table-columns in (cdr (assoc 'tablescolumns instance))
                for tab-name = (format "%s" (car table-columns))
                append (append (if (loop for pattern in word-patterns
                                         thereis (helm-buffer--match-pattern pattern tab-name nil))
                                 (list (format "Table:'%s'" tab-name)))
                               (cl-loop for column-name across (cdr table-columns)
                                        if (loop for pattern in word-patterns
                                                 thereis (helm-buffer--match-pattern pattern column-name nil))
                                        collect (format "Column:'%s.%s'" tab-name column-name))))
       (cl-loop for (function) in (cdr (assoc 'functions instance))
                for function-name = (format "%s" function)
                if (loop for pattern in word-patterns
                         thereis (helm-buffer--match-pattern pattern function-name nil))
                collect (format "Function:'%s'" function-name))

       (cl-loop for variable-name across (cdr (assoc 'variables instance))
                if (loop for pattern in word-patterns
                         thereis (helm-buffer--match-pattern pattern variable-name nil))
                collect (format "Var:'%s'" variable-name))))))

The helm match function will combine candidate columns and these additional columns.

(defun helm-q-source-match-function (candidate)
  "Default function to match buffers.
Argument CANDIDATE: one helm candidate."
  (let ((instance (helm-q-get-instance-by-display candidate))
        (helm-buffers-fuzzy-matching t))
    (or
      (cl-loop for slot in (helm-attr 'candidate-columns)
               for slot-value = (cdr (assoc slot instance))
               thereis (helm-buffer--match-pattern helm-pattern slot-value nil))
      (helm-q-context-matched-columns instance))))

helm-q-source-filtered-candidate-transformer

Rebuild the candidate string after a search.

(defun helm-q-source-filtered-candidate-transformer (candidates source)
  "Filter candidates by context match.
Argument CANDIDATES: the candidate list.
Argument SOURCE: the source."
  (cl-loop for (nil . instance) in candidates
           collect (cons (helm-q-instance-display-string instance) instance)))

password management

data format

We use a custom format string as the entry, that is, “helm-q/{host}/{user}” to distinguish them from other entries.

(defvar helm-q-pass-prefix "helm-q")

So to get a path for an host

(defun helm-q-pass-path-of-host (host)
  "Get the path for an host.
Argument HOST: the host of an instance."
  (format "%s/%s/" helm-q-pass-prefix host))

And the path for an user under an host.

(defun helm-q-pass-path-of-host-user (host user)
  "Get the path for an host.
Argument HOST: the host of an instance.
Argument USER: the user for the host."
  (format "%s/%s/%s" helm-q-pass-prefix host user))

If we use pass as the storage, the stored password files just like the following file structure:

$ pass show helm-q
helm-q
├── host.domain.com:5000
│   ├── user1
│   └── user2
└── host.domain.com:5001
    └── user1

We supply different password storage implementation, for each implementation, it should implement the following interfaces.

(cl-defgeneric helm-q-pass-users-of-host (storage host)
  "Get a list of users by its host.
Argument STORAGE: a valid storage method.
Argument HOST: a host.")

(cl-defgeneric helm-q-get-pass (storage host user)
  "Get pass by its host and user.
Argument STORAGE: a valid storage method.
Argument HOST: a host.
Argument USER: an user name.")

(cl-defgeneric helm-q-update-pass (storage host user &optional password)
  "Update user and pass to local encrypted storage file.
Argument STORAGE: a valid storage method.
Argument HOST: the host of an instance.
Argument USER: the user for the instance.
Argument PASSWORD: the optional password for the instance.")

no password storage

This case happens when variable helm-q-password-storage is nil. That is, we will not store any password in file and will notify user when such action is invoked.

get user name list for an host

In this case, there are no users.

(cl-defmethod helm-q-pass-users-of-host ((storage (eql nil)) host)
  "Get a list of users by its host.
Argument STORAGE: should be 'pass
Argument HOST:"
  nil)

get password for an user in an host

In this case, no password.

(cl-defmethod helm-q-get-pass ((storage (eql nil)) host user)
  "Get pass by its host and user.
Argument STORAGE: should be 'pass
Argument HOST:
Argument USER:"
  nil)

update user name and password for an host

In this case, we should notify user an error message.

(cl-defmethod helm-q-update-pass ((storage (eql nil)) host user &optional password)
  "Update user and pass to local pass storage file.
Argument STORAGE: should be 'pass
Argument HOST: the host of an instance.
Argument USER: the user for the instance.
Argument PASSWORD: the optional password for the instance."
  (message "You can't save password because this feature is disabled by Emacs lisp variable 'helm-q-password-storage'."))

the routine to call pass command.

It will return a cons whose car is true if it runs successfully, and the cdr is the result string.

(defun helm-q-run-pass (infile &rest args)
  "Run pass with args.
Argument INFILE: input file for pass process.
Argument ARGS: additional arguments for pass."
  (with-temp-buffer
      (let* ((exit-code (apply 'call-process "pass" infile (current-buffer) t args))
             (result (string-trim (buffer-string))))
        (cons (= 0 exit-code) result))))

get user name list for an host

(cl-defmethod helm-q-pass-users-of-host ((storage (eql pass)) host)
  "Get a list of users by its host.
Argument STORAGE: should be 'pass
Argument HOST:"
  (cl-destructuring-bind (succ-p . result)
      (helm-q-run-pass nil "ls" (helm-q-pass-path-of-host host))
    (when succ-p
      (let ((words (split-string result)))
        ;; th words list has the format `("helm-q/host.domain.com:5000" "├──" "user1" "└──" "user2")' .
        (cl-loop for user-list on (cdr words) by 'cddr
                 collect (second user-list))))))

get password for an user in an host

(cl-defmethod helm-q-get-pass ((storage (eql pass)) host user)
  "Get pass by its host and user.
Argument STORAGE: should be 'pass
Argument HOST:
Argument USER:"
  (cl-destructuring-bind (succ-p . entry)
      (helm-q-run-pass nil "show" (helm-q-pass-path-of-host-user host user))
    (when succ-p
      entry)))

update user name and password for an host

(cl-defmethod helm-q-update-pass ((storage (eql pass)) host user &optional password)
  "Update user and pass to local pass storage file.
Argument STORAGE: should be 'pass
Argument HOST: the host of an instance.
Argument USER: the user for the instance.
Argument PASSWORD: the optional password for the instance."
  (let* ((pass (or password (read-passwd (format "Password for %s@%s: " user host) t)))
         (in-file (make-temp-file "helm-q-")))
    ;; when insert a password in pass, it will ask for password, `call-process' will let pass read it from this input file.
    (with-temp-file in-file
      (insert pass "\n" pass "\n\n"))
    (unwind-protect
        (cl-destructuring-bind (succ-p . entry)
            (helm-q-run-pass in-file "insert" "-f" (helm-q-pass-path-of-host-user host user))
          succ-p)
      (delete-file in-file); delete this file to avoid potential security leak.
      nil)))

select a user from a user list

(defun helm-q-user (users)
  "Select a user in Helm.
Argument USERS: a user list."
  (let ((prompt "Please select an user:")
        (user "")
        (helm-source
         `((name . "helm-q-user-list")
           (candidates . ,users)
           (action . (lambda (candidate) (setf user candidate)))))
        (helm :sources '(helm-source) :prompt prompt)
        user)))

buffer name for Q-Shell

The buffer id is a string based on user configuration without prefix and suffix string. And the buffer name will surround buffer id with prefix ”qcon-” and suffix ””.

(defun helm-q-shell-buffer-id (instance)
  "Build Q-Shell buffer id based on user configuration.
Argument INSTANCE: the instance."
  (string-join (cl-loop for pattern in (second helm-q-qcon-buffer-name-pattern)
                        collect (cdr (assoc pattern instance)))
               (first helm-q-qcon-buffer-name-pattern)))

(defun helm-q-shell-buffer-name (buffer-id)
  "Build Q-Shell buffer name based on user configuration.
Argument BUFFER-ID: the buffer id."
  (concat "*qcon-" buffer-id "*"))

actions

default action to connect with q-mode

if M-x helm-q was invoked with C-u prefix argument, and after instance was selected from helm-q buffer, prompt for new user and then for password in the minibuffer. Attempt to connect with the supplied user:password. In above condition, we will use a special variable to indicate the switch.

(defvar helm-q-pass-required-p nil "Switch it on when helm-q was invoked with prefix argument.")

The action routine:

(defun helm-q-source-action-qcon (candidate)
  "Argument CANDIDATE: selected candidate."
  (let* ((instance candidate)
         (host (cdr (assoc 'address instance)))
         (host-port (split-string host ":"))
         (q-qcon-server (car host-port))
         (q-qcon-port (or (second host-port) q-qcon-port))
         (users (helm-q-pass-users-of-host helm-q-password-storage host))
         (q-qcon-user (if helm-q-pass-required-p
                        (read-string "Please enter a new user name: " (car users))
                        (case (length users)
                          (0 "")
                          (1 (car users))
                          (2 (helm-q-user users)))))
         (q-qcon-password (when q-qcon-user
                            (if helm-q-pass-required-p
                              (read-passwd (format "Password for %s@%s: " q-qcon-user host))
                              (helm-q-get-pass helm-q-password-storage host q-qcon-user))))
         ;; KLUDGE: q-mode should supply a function to build buffer name.
         (q-buffer-name (format "*%s*" (format "qcon-%s" (q-qcon-default-args))))
         (helm-q-buffer-name (helm-q-shell-buffer-name (helm-q-shell-buffer-id instance)))
         (q-buffer (get-buffer q-buffer-name)))
    (if (and helm-q-buffer-name
             (process-live-p (get-buffer-process helm-q-buffer-name)))
      ;; activate this buffer if the instance has already been connected.
      (q-activate-buffer helm-q-buffer-name)
      (when (helm-q-test-active-connection host)
        (q-qcon (q-qcon-default-args))
        (rename-buffer helm-q-buffer-name)
        (q-activate-buffer helm-q-buffer-name)))))

action to show username and password

(defun helm-q-source-action-show-password (candidate)
  "Show password for current instance.
Argument CANDIDATE: selected candidate."
  (if (null helm-q-password-storage)
    (message "This feature is disabled by Emacs lisp variable 'helm-q-password-storage'.")
    (let* ((instance candidate)
           (host (cdr (assoc 'address instance)))
           (users (helm-q-pass-users-of-host helm-q-password-storage host)))
      (case (length users)
        (0 (message "No username/password for host %s" host))
        (1 (message "%s@%s's password is '%s'" (car users) host (helm-q-get-pass helm-q-password-storage host (car users))))
        (t (let ((user (helm-q-user users)))
             (when user
               (message "%s@%s's password is '%s'" user host (helm-q-get-pass helm-q-password-storage host user)))))))))

helm-q-source-action-add-password

(defun helm-q-source-action-add-password (candidate)
  "Add password for current instance.
Argument CANDIDATE: selected candidate."
  (if (null helm-q-password-storage)
    (message "This feature is disabled by Emacs lisp variable 'helm-q-password-storage'.")
    (let* ((instance candidate)
           (host (cdr (assoc 'address instance)))
           (user (read-string "Please enter the user name: ")))
      (if (s-blank? user)
        (message "Please input a valid user name!")
        (helm-q-update-pass helm-q-password-storage host user)))))

action to update username and password

(defun helm-q-source-action-update-password (candidate)
  "Update password for current instance.
Argument CANDIDATE: selected candidate."
  (if (null helm-q-password-storage)
    (message "This feature is disabled by Emacs lisp variable 'helm-q-password-storage'.")
    (let* ((instance candidate)
           (host (cdr (assoc 'address instance)))
           (users (helm-q-pass-users-of-host helm-q-password-storage host)))
      (case (length users)
        (0 (message "No username/password for host %s" host))
        (1 (helm-q-update-pass helm-q-password-storage host (car users)))
        (t (let ((user (helm-q-user users)))
             (when user
               (helm-q-update-pass helm-q-password-storage host user))))))))

The interactive command

;;;###autoload
(defun helm-q (arg)
  "Select data source in helm.
Argument ARG: prefix argument."
  (interactive "P")
  (let ((helm-candidate-separator " ")
        (helm-q-bringing-q-actite-buffer-front-p t)
        (helm-q-pass-required-p (and arg t)))
    (helm :sources (list (helm-make-source "helm-running-q" 'helm-q-running-source)
                         (helm-make-source "helm-q" 'helm-q-source))
          :buffer "*helm q*")))

test connecting of qcon

requirement

We will try to send a command after connecting via q or qcon in Emacs, and execute different actions based on the test.

The current behavior of qcon for a command likes this.

$ qcon 192.168.0.100:5000 # a normal successful connection without password.
192.168.0.100:5000>1+1
2
192.168.0.100:5000>\\
$ qcon 192.168.0.100:5010:admin:password # a normal successful connection with password.
192.168.0.100:5010>2
2
192.168.0.100:5010>\\
$ qcon 192.168.0.100:5010:admin:badpassword # an invalid connection with bad password.
192.168.0.100:5010>2
192.168.0.100:5010>\\
$ qcon 192.168.0.100:5010:baduser:badpassword  # an invalid connection with bad user.
192.168.0.100:5010>2
192.168.0.100:5010>\\
$ qcon 192.168.0.100:5020                 # no process on the port
192.168.0.100:5020>2
conn: Connection refused
$ qcon 192.168.1.111:5000                 # bad host
192.168.1.111:5000>2
conn: No route to host

So the actions we will do based on a test command are.

  • if after a command and the process is still alive,
    • if there is a response, we will treat it as a successful connection and store the username and password based on current storage method.
    • if there is no response, we will treat it as a failed connection and ask for new username and password for it and connect again.
  • if the process is dead,
    • we will do nothing with it.

implementation

We will not do this test in comint buffer directly because it’s an interactive buffer for user. Instead, we will create a new process of qcon and send test commands to it in a temp buffer.

(defun helm-q-test-active-connection (host)
  "Test connection of qcon, return true if connection is ok.
Argument HOST: the host of current instance."
  (message "Test connection...")
  (let ((in-file (make-temp-file "helm-q-"))
        (test-message "Test Connection."))
    ;; prepare test commands in input file.
    (with-temp-file in-file
      (insert
       ;; echo a test message.
       "\"" test-message "\"" "\n"
       ;; quit from this process.
       "\\\\" "\n\n"))
    (with-temp-buffer
      (let* ((exit-code (apply 'call-process q-qcon-program in-file (current-buffer) t
                               (list (q-qcon-default-args))))
             (result (string-trim (buffer-string))))
        (delete-file in-file); remove temp file after use.
        (if (/= 0 exit-code)
          ;; if failed to connect, report the result as error message.
          (progn (message "connection failed: %s" result)
                 nil)
          (if (ignore-errors
                (goto-char (point-min))
                ;; The test message should occur in the output.
                (search-forward test-message nil nil 1))
            (progn
              ;; connection is ok, save password for this connection if it is from user input.
              (when helm-q-pass-required-p
                (helm-q-update-pass helm-q-password-storage host q-qcon-user q-qcon-password))
              t)
            (progn
              ;; invalid user/pass, ask for a new username and password.
              (message "connection is not responding: %s" result)
              (if (s-blank? q-qcon-user)
                (progn
                  ;; Prompting for user and password in case of unsuccessful passwordless connection attempt.
                  (setf q-qcon-user (read-string "Please enter the user name: " q-qcon-user))
                  (setf q-qcon-password (read-passwd "Please enter the password: "))
                  ;; test connection with new username and password.
                  (let ((helm-q-pass-required-p t)); save the password if it is ok.
                    (helm-q-test-active-connection host)))
                (progn
                  ;; Prompting for new password in case of failed authentication.
                  (setf q-qcon-password (read-passwd "Please enter the password: "))
                  ;; test connection with new username and password.
                  (let ((helm-q-pass-required-p t)); save the password if it is ok.
                    (helm-q-test-active-connection host)))))))))))

select an instance when run q-evail-*

requirement

In q-mode, q-eval-* sends string (q-send-string) from Q-Script buffer to whichever Q-Shell comint buffer that is marked as q-active-buffer. This is to be extended with the ability to interactively select which buffer/instance should the string be sent to, skipping the need to manually q-activate-buffer each time a different destination is desired.

Extend behavior of q-eval-* so when it’s called with prefix argument C-u, it brings up a helm buffer and wait for the selection of an instance. This special helm buffer consists of two sections of candidates, just like M-x list-buffers consists of three sections: Buffers, Recentf and Create buffer. The two sections are:

  1. Buffers: existing Q-Shell buffer candidates - searchable by buffer name
  2. Instances: helm-q qcon instance candidates searchable by attributes like tables, columns, functions etc..

Note that the candidates can be overlapping, when an instance listed in Instances section is an already existing Q-Shell buffer listed in Buffers section. Selecting such an instance should not create a duplicate Q-Shell buffer.

On selection, the selected Q-Shell buffer is marked as q-active-buffer and the string is sent to it (q-send-string) as usual. However, the cursor stays and never leaves the initial Q-Script buffer.

When any of the q-eval-* commands are called with double prefix argument C-u C-u, invoke helm-q with single prefix argument to prompt for user and password.

helm source of running instances.

(defclass helm-q-running-source (helm-source-sync)
  ((buffer-list
    :initarg :buffer-list
    :initform #'helm-q-running-buffer-list
    :custom function
    :documentation
    "  A function with no arguments to get running buffer list.")
   (init :initform 'helm-q-running-source-list--init)
   (multimatch :initform nil)
   (multiline :initform nil)
   (action :initform
           '(("Select a pre-existing q process" . helm-q-running-source-action-select-an-instance)))
   (migemo :initform 'nomultimatch)
   (volatile :initform t)
   (nohighlight :initform nil)))

action to select an instance

There are two conditions to execute this action:

  • update active buffer before q-send-string, in this condition, we just active the selection buffer.
  • invoked by helm-q, in this condition, we not only active the selection buffer, but also bring it to front.

We will use a special variable tow distinguish these two conditions.

(defvar helm-q-bringing-q-actite-buffer-front-p nil)

The implementation of the action.

(defun helm-q-running-source-action-select-an-instance (candidate)
  "Select an running instance.
Argument CANDIDATE: the selected candidate."
  (q-activate-buffer candidate)
  (when helm-q-bringing-q-actite-buffer-front-p
    (pop-to-buffer q-active-buffer)))

Get running Q buffers

(defun helm-q-running-buffer-list ()
  "Get running Q buffers."
  (loop with q-active-buffer-name = (if (bufferp q-active-buffer)
                                      (buffer-name q-active-buffer)
                                      q-active-buffer)
        for buffer in (buffer-list)
        if (with-current-buffer buffer
             (equal 'q-shell-mode major-mode))
          collect (let ((buffer-name (buffer-name buffer)))
                    (if (string= buffer-name q-active-buffer-name)
                      (propertize buffer-name 'face 'bold)
                      buffer-name))))

initialize candidates of running instances

(defun helm-q-running-source-list--init ()
  "Initialize helm-q-running-source."
  (helm-attrset 'candidates (funcall (helm-attr 'buffer-list))))

install as advice function

We add a before advice to function q-send-string, to detect the prefix argument for current command, to

  • change the q-active-buffer if necessary;
  • prompt for user and password in helm-q.
  • display q-active-buffer in another window.
(defun helm-q-update-active-buffer (&rest args)
  "An advice function for `q-send-string'.
To update active buffer based on prefix argument.
Argument ARGS: the argument for original function."
  (let ((update-active-buffer-p nil)
        (helm-q-pass-required-p helm-q-pass-required-p))
    (case (prefix-numeric-value current-prefix-arg)
      (4 ; prefix C-u
       (setf update-active-buffer-p t))
      (16 ; prefix C-u C-u
       (setf update-active-buffer-p t
             helm-q-pass-required-p t)))
    (when update-active-buffer-p
      (let ((another-win (if (one-window-p)
                           (if (> (window-width) 100)
                             (split-window-horizontally)
                             (split-window-vertically))
                           (next-window))))
        (helm :sources (list (helm-make-source "helm-running-q" 'helm-q-running-source)
                             (helm-make-source "helm-q" 'helm-q-source))
              :buffer "*helm q*")
        (set-window-buffer another-win q-active-buffer)))))
(advice-add 'q-send-string :before #'helm-q-update-active-buffer)

Release current library

And when a new version of ./helm-q.el can release from this file, the following code should execute.

(literate-elisp-tangle
 "helm-q.org"
 :header ";;; helm-q.el --- A library to manage remote q sessions with Helm and q-mode  -*- lexical-binding: t; -*-

;; URL: https://github.com/emacs-q/helm-q.el
;; Package-Requires: ((emacs \"26.1\") (cl-lib \"0.6\") (helm \"1.9.4\") (s \"1.10.0\") (q-mode \"0.1\") (cl-lib \"1.0\"))

;;; Commentary:

;; helm-q is an Emacs Lisp library to manage remote q sessions with Helm and q-mode.
"
                 :tail "(provide 'helm-q)
;;; helm-q.el ends here
")