--- /dev/null
+;;; cobra.el --- Complete Cobra command lines -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Eshel Yaron
+
+;; Author: Eshel Yaron <me@eshelyaron.com>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Cobra is a popular Golang framework for CLI programs. This library
+;; defines function `cobra-read-command-line', which helps you read a
+;; command line for a program that uses Cobra, with completion.
+;; Prominent examples of Cobra programs are `kubectl' and `docker'.
+
+;;; Code:
+
+(defvar cobra--cache nil)
+
+(defun cobra-completion-table (executable s p a)
+ "Completion table for command lines that invoke EXECUTABLE.
+
+Perform completion action A on string S with predicate P."
+ (let ((start 0))
+ (while (string-match "[[:space:]=]" s start)
+ (setq start (match-end 0)))
+ (if (eq a 'metadata)
+ `(metadata
+ (category . cobra-command-line)
+ (affixation-function
+ . ,(lambda (cands)
+ (let ((max (seq-max
+ (cons 0 (mapcar #'string-width cands)))))
+ (mapcar
+ (lambda (cand)
+ (list
+ cand ""
+ (if-let
+ ((desc (get-text-property
+ 0 'cobra-argument-description
+ cand)))
+ (concat
+ (make-string (1+ (- max (string-width cand))) ?\s)
+ (propertize desc 'face 'completions-annotations))
+ "")))
+ cands)))))
+ (let* ((lines
+ (cdr
+ (if (string= s (car cobra--cache))
+ ;; Cache hit.
+ cobra--cache
+ (setq
+ cobra--cache
+ (cons s
+ (apply #'process-lines-ignore-status
+ executable "__complete"
+ (let ((args (cdr (split-string-and-unquote s))))
+ (if (string-suffix-p " " s)
+ ;; Restore omitted empty argument.
+ (nconc args '(""))
+ args))))))))
+ (code nil)
+ (comps (seq-take-while
+ (lambda (line)
+ (not (and (string-match "^:\\([0-9]+\\)$" line)
+ (setq code (string-to-number
+ (match-string 1 line))))))
+ lines)))
+ ;; `code' encodes "completion directives", as follows:
+ ;; #b000001: An error occurred, ignore completions.
+ ;; #b000010: Don't add space after completion.
+ ;; #b000100: Don't fall back to file completion.
+ ;; #b001000: Completions are really file extension filters.
+ ;; #b010000: Complete directory names.
+ ;; #b100000: Preserve completions order.
+ (when (and code (zerop (logand 1 code)))
+ ;; Error bit in unset, proceed.
+ (if (= #b100 (logand #b100 code))
+ ;; No file name completion.
+ (if (eq (car-safe a) 'boundaries)
+ `(boundaries
+ ,start . ,(and (string-match "[[:space:]=]" (cdr a))
+ (match-beginning 0)))
+ (let ((table
+ (mapcar
+ ;; Annotate completion candidates.
+ (lambda (comp)
+ (pcase (split-string comp "\t" t)
+ (`(,c ,d . ,_)
+ (propertize
+ c 'cobra-argument-description
+ ;; Only keep first sentence.
+ (car (split-string d "\\." t))))
+ (`(,c . ,_) c)))
+ comps)))
+ (if a (complete-with-action a table (substring s start) p)
+ ;; `try-completion'.
+ (let ((comp (complete-with-action a table (substring s start) p)))
+ (if (stringp comp) (concat (substring s 0 start) comp) comp)))))
+ ;; File name completion.
+ (setq p
+ (cond
+ ((= #b1000 (logand #b1000 code))
+ ;; `comps' are valid extensions.
+ (lambda (f)
+ (or (file-directory-p f)
+ (when (string-match "\\.[^.]*\\'" f)
+ (member (substring f (1+ (match-beginning 0)))
+ comps)))))
+ ((= #b10000 (logand #b10000 code))
+ ;; Directory name completion.
+ #'file-directory-p)))
+ (if (eq (car-safe a) 'boundaries)
+ ;; Find nested boundaries.
+ (let* ((suf (cdr a))
+ (bounds (completion-boundaries
+ (substring s start) #'completion-file-name-table p
+ (substring suf 0 (string-match "[[:space:]=]" suf)))))
+ `(boundaries ,(+ (car bounds) start) . ,(cdr bounds)))
+ (if a (complete-with-action a #'completion-file-name-table
+ (substring s start) p)
+ (let ((comp (complete-with-action a #'completion-file-name-table
+ (substring s start) p)))
+ (if (stringp comp) (concat (substring s 0 start) comp) comp))))))))))
+
+;;;###autoload
+(defun cobra-read-command-line (prompt initial &optional hist)
+ "Prompt with PROMPT for a command line starting with INITIAL.
+
+Optional argument HIST is the name of the history list variable to use,
+if it is nil or omitted, it defaults to `shell-command-history'."
+ (let ((exec (car (split-string-and-unquote initial))))
+ (completing-read prompt (apply-partially #'cobra-completion-table exec)
+ nil nil initial (or hist 'shell-command-history))))
+
+(provide 'cobra)
+;;; cobra.el ends here
(format-prompt prompt default) nil nil nil nil
'kubed-container-images-history default))
-(defvar kubed-command-line-completion-cache nil)
-
-(defun kubed-command-line-completion-table (s p a)
- "Completion table for `kubectl' command lines.
-
-Perform completion action A on string S with predicate P."
- (let ((start 0))
- (while (string-match "[[:space:]=]" s start)
- (setq start (match-end 0)))
- (if (eq a 'metadata)
- `(metadata
- (category . kubed-command-line)
- (affixation-function
- . ,(lambda (cands)
- (let ((max (seq-max
- (cons 0 (mapcar #'string-width cands)))))
- (mapcar
- (lambda (cand)
- (list
- cand ""
- (if-let
- ((desc (get-text-property
- 0 'kubed-command-line-argument-description
- cand)))
- (concat
- (make-string (1+ (- max (string-width cand))) ?\s)
- (propertize desc 'face 'completions-annotations))
- "")))
- cands)))))
- (let* ((lines
- (cdr
- (if (string= s (car kubed-command-line-completion-cache))
- kubed-command-line-completion-cache
- (setq
- kubed-command-line-completion-cache
- (cons s
- (apply #'process-lines-ignore-status
- kubed-kubectl-executable "__complete"
- (let ((args (cdr (split-string-and-unquote s))))
- (if (string-suffix-p " " s)
- (nconc args '(""))
- args))))))))
- (code nil)
- (comps (seq-take-while
- (lambda (line)
- (not (and (string-match "^:\\([0-9]+\\)$" line)
- (setq code (string-to-number
- (match-string 1 line))))))
- lines)))
- ;; `code' encodes "completion directives", as follows:
- ;; #b000001: An error occurred, ignore completions.
- ;; #b000010: Don't add space after completion.
- ;; #b000100: Don't fall back to file completion.
- ;; #b001000: Completions are really file extension filters.
- ;; #b010000: Complete directory names.
- ;; #b100000: Preserve completions order.
- (when (and code (zerop (logand 1 code)))
- ;; Error bit in unset, proceed.
- (if (= #b100 (logand #b100 code))
- ;; No file name completion.
- (if (eq (car-safe a) 'boundaries)
- `(boundaries
- ,start . ,(and (string-match "[[:space:]=]" (cdr a))
- (match-beginning 0)))
- (let ((table
- (mapcar
- ;; Annotate completion candidates.
- (lambda (comp)
- (pcase (split-string comp "\t" t)
- (`(,c ,d . ,_)
- (propertize
- c 'kubed-command-line-argument-description
- ;; Only keep first sentence.
- (car (split-string d "\\." t))))
- (`(,c . ,_) c)))
- comps)))
- (if a (complete-with-action a table (substring s start) p)
- ;; `try-completion'.
- (let ((comp (complete-with-action a table (substring s start) p)))
- (if (stringp comp) (concat (substring s 0 start) comp) comp)))))
- ;; File name completion.
- (setq p
- (cond
- ((= #b1000 (logand #b1000 code))
- ;; `comps' are valid extensions.
- (lambda (f)
- (or (file-directory-p f)
- (when (string-match "\\.[^.]*\\'" f)
- (member (substring f (1+ (match-beginning 0)))
- comps)))))
- ((= #b10000 (logand #b10000 code))
- ;; Directory name completion.
- #'file-directory-p)))
- (if (eq (car-safe a) 'boundaries)
- ;; Find nested boundaries.
- (let* ((suf (cdr a))
- (bounds (completion-boundaries
- (substring s start) #'completion-file-name-table p
- (substring suf 0 (string-match "[[:space:]=]" suf)))))
- `(boundaries ,(+ (car bounds) start) . ,(cdr bounds)))
- (if a (complete-with-action a #'completion-file-name-table
- (substring s start) p)
- (let ((comp (complete-with-action a #'completion-file-name-table
- (substring s start) p)))
- (if (stringp comp) (concat (substring s 0 start) comp) comp))))))))))
-
(defvar kubed-kubectl-command-history nil
"Minibuffer history for `kubed-kubectl-command'.")
Interactively, prompt for COMMAND with completion for `kubectl' arguments."
(interactive
- (list (completing-read "Command: " #'kubed-command-line-completion-table
- nil nil (concat kubed-kubectl-executable " ")
- 'kubed-kubectl-command-history)))
+ (list (cobra-read-command-line "Command: " "kubectl "
+ 'kubed-kubectl-command-history)))
(shell-command command))
;;;###autoload (autoload 'kubed-prefix-map "kubed" nil t 'keymap)