aboutsummaryrefslogblamecommitdiff
path: root/hmm.el
blob: 25ca4a08c87907695453b2542e823e925956e4c7 (plain) (tree)
1
2

                              







































                                                                                              


                                               








                                                            





                                           




































                                                                            
 




                                                               
                                               
                               
                                                                    

                                    



























                                                                                   
 
                                                    




                                                            
                                                                
                                                                 


                                                        


                                                   
                             
                                                                        





                                                                             


                                  
                                        



                                                                       



                                                                
                                           
 

                                                     
                                 








                                                                           
 
                                    


                                                         
                       
 












                                                                       







                                                      

                        
                          
                                                               


                                                                          
                                                         

                      


                                                                      
 
                                              












                                                  
                                                                               

                                       





                                              

                                             
                                                  
                             
                                                       
                                       
 



                                                       
                                                         

                                
                                                      


                               
                                                 


                                                






                                              
                             
               
                                         
                                                   
                                              


                                                                                  


                                                
                                          
                                          









                                                                     
 



                                 
                            
 
                              
             

                                                  




                                                              


                                                                  
                                                    













                                                                                     








                                                                
                                                  

                          

              
;; -*- lexical-binding: t; -*-

;; Copyright (C) 2022 Yuchen Pei.
;; 
;; This file is part of hmm.el.
;; 
;; hmm.el is free software: you can redistribute it and/or modify it under
;; the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; 
;; hmm.el 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 Affero General
;; Public License for more details.
;; 
;; You should have received a copy of the GNU Affero General Public
;; License along with hmm.el.  If not, see <https://www.gnu.org/licenses/>.

(defvar hmm-web-search-engines
  '((:name ddg
           :format "https://html.duckduckgo.com/html?q=%s")
    (:name fsd
           :format "https://directory.fsf.org/w/index.php?search=%s")
    (:name ml-emacs-devel
           :format
           "https://lists.gnu.org/archive/cgi-bin/namazu.cgi?query=%s&idxname=emacs-devel")
    (:name ml-help-gnu-emacs
           :format
           "https://lists.gnu.org/archive/cgi-bin/namazu.cgi?query=%s&idxname=help-gnu-emacs")
    (:name wikipedia
           :format "https://en.wikipedia.org/w/index.php?search=%s"))
  "hmm web search engines.")

(defvar hmm-web-browsers
  '((:name eww :command eww)
    (:name firefox :command browse-url-firefox))
  "hmm web browers.")

(defvar hmm-handlers
  '(:query
    ((:command locate)
     (:command project-or-external-find-regexp)
     (:command woman)
     (:command man))
    :url
    ((:schemes ("mailto") :command browse-url-mail))
    :file
    ((:command find-file)
     (:command dired :mimetypes ("inode/directory"))
     (:command byte-compile-file :mimetypes ("text/x-lisp"))
     (:command hmm-file-mime-type)))
  "hmm handlers.")

(defvar hmm-transformers
  nil
  "hmm transformers.

Transforms from one type to another type.")

(defvar hmm-external-handlers
  '((:name mpv
           :external-command "mpv %U"
           :display-name "mpv player"
           :description "Play url with mpv"
           :schemes
           ("ftp" "http" "https" "mms" "rtmp" "rtsp" "sftp" "smb" "srt")
           :handling :url)
    (:name wget
           :external-command "wget %U"
           :display-name "GNU Wget"
           :description "The non-interactive network downloader"
           :schemes
           ("ftp" "http" "https")
           :handling :url)
    (:name torsocks-mpv
           :external-command "torsocks mpv %U"
           :display-name "mpv player torsocks"
           :description "Play url with mpv over torsocks"
           :schemes
           ("ftp" "http" "https" "mms" "rtmp" "rtsp" "sftp" "smb" "srt")
           :handling :url))
  "hmm external handlers.")

(defvar hmm-matchers
  '(((thing-at-point-url-at-point) . hmm-url)
    ((thing-at-point-file-at-point) . hmm-file)
    ((and (derived-mode-p 'dired-mode) (dired-get-filename nil t))
     . hmm-file)
    ((and (derived-mode-p 'dired-mode) (expand-file-name default-directory))
     . hmm-file)
    ((and (derived-mode-p 'org-mode) (my-org-link-at-point)) . hmm-url)
    ((get-text-property (point) 'shr-url) . hmm-url)
    ((thing-at-point 'symbol) . hmm-query)
    ((buffer-file-name) . hmm-file)
    ((expand-file-name default-directory) . hmm-file))
  "Matchers for 'hmm-thing-at-point'.")

(defmacro hmm-define-web-search-engine (engine)
  "Defines a simple transformer from a search engine.

A search engine is basically a format string with one single %s
to be substitutes by the search query."
  (let* ((engine-name (plist-get engine :name))
         (function-name (intern
                         (format "hmm-%s-format-url" engine-name))))
    `(progn
       (defun ,function-name (query)
         ,(format "Format %s url from QUERY." engine-name)
         (format ,(plist-get engine :format) query))
       (hmm-add-transformer
        '( :function ,function-name :name ,engine-name
           :from :query :to :url)))))

;; TODO: Validate the output type of the transformer agrees with the
;; input type of the handler.
(defmacro hmm-define-compound-handler (transformer handler)
  "Define a compound handler that feeds the output of TRANSFORMER into HANDLER.

Say transformer is ddg (query->url) and handler is firefox (url),
then the resulting function is hmm-ddg-firefox, which takes a
input and open the ddg search url in firefox."
  (when-let* ((trans-name (or (plist-get transformer :name)
                              (plist-get transformer :function)))
              (handler-name (plist-get handler :name))
              (function-name (intern
                              (format "hmm-%s-%s" trans-name handler-name)))
              (from (plist-get transformer :from)))
    `(progn
       (defun ,function-name (input)
         ,(format "Handle the output of %s with %s."
                  trans-name handler-name)
         (interactive ,(format "sHandle %s with %s for: " trans-name handler-name))
         (,(plist-get handler :command)
          (,(plist-get transformer :function) input)))
       (hmm-add-handler '(:command ,function-name) ,from))))

(defmacro hmm-define-external-handler (handler type)
  (let* ((name (plist-get handler :name))
         (display-name (or (plist-get handler :display-name)
                           (format "%s" name)))
         (description (format "%s - %s"
                              display-name
                              (plist-get handler :description)))
         (function-name (intern (format "hmm-external-%s" name)))
         (external-command
          (mapcar (lambda (token)
                    (if (string-match "^%[fFuU]$" token)
                        (if (eq type :file)
                            `(expand-file-name arg)
                          `arg)
                      token))
                  (split-string (plist-get handler :external-command))))
         (handler (plist-put handler :command function-name))
         (interactive-form
          (if (eq type :file)
              `(list
                (read-file-name ,(format "Run %s with file: " display-name)))
            (format "sRun %s with: " display-name))))
    `(progn
       (defun ,function-name (arg)
         ,description
         (interactive ,interactive-form)
         (apply 'start-process
                (append
                 (list (format ,(format "%s-%%s" function-name) arg)
                       (format ,(format "*%s-%%s*" function-name) arg))
                 ,(cons 'list external-command)))
         ,(when (plist-get handler :display-buffer)
            `(display-buffer
              (format ,(format "*%s-%%s*" function-name) arg))))
       (hmm-add-handler ',handler ,type))))

(defun hmm-add-handler (handler handling)
  (let ((handlers (plist-get hmm-handlers handling)))
    (cl-pushnew handler handlers)
    (setq hmm-handlers (plist-put hmm-handlers handling handlers))))

(defun hmm-add-transformer (transformer)
  (let* ((sig (intern (format "%s->%s"
                              (plist-get transformer :from)
                              (plist-get transformer :to))))
         (transformers (plist-get hmm-transformers sig)))
    (cl-pushnew transformer transformers)
    (setq hmm-transformers (plist-put hmm-transformers sig transformers))))

(defun hmm-thing-in-region (from to)
  (interactive "r")
  (unless (region-active-p) (error "Region not active."))
  (let* ((query (buffer-substring from to)))
    (hmm-query query)))

(defmacro hmm-define-hmm-thing-at-point ()
  `(defun hmm-thing-at-point ()
     "Prompt for what to do with thing at point."
     (interactive)
     (let ((thing))
       ,(cons 'cond
              (append
               (mapcar
                (lambda (pair)
                  `((setq thing ,(car pair))
                    (,(cdr pair) thing)))
                hmm-matchers)
               `((t (error "Cannot guess what to do with what."))))))))

(defun hmm ()
  (interactive)
  (cond ((region-active-p)
         (call-interactively 'hmm-thing-in-region))
        (t (call-interactively 'hmm-thing-at-point))))

(define-key global-map (kbd "C-M-<return>") 'hmm)

(defun hmm-query (query)
  (interactive "sQuery: ")
  (hmm-handle-internal query :query "Handle query %s using: "))

(defun hmm-url (url)
  (interactive (list (read-string "URL: " (thing-at-point-url-at-point))))
  (hmm-handle-internal url :url "Handle url %s using: "))

(defun hmm-file (file)
  (interactive (list (read-file-name "File: " default-directory
                                     (thing-at-point-file-at-point))))
  (hmm-handle-internal file :file "Handle file %s using: "))

(defun hmm-handle-internal (thing type prompt)
  (let ((selected
         (completing-read
          (format prompt thing)
          (mapcar
           (lambda (command)
             (format "%s (%s)" command
                     (if (documentation command)
                         (car
                          (split-string
                           (documentation command)
                           "\n"))
                       "Undocumented")))
           (hmm-filter-commands thing type)))))
    (funcall-interactively (intern (car (split-string selected " ("))) thing)))

(defun hmm-filter-commands (thing type)
  (let ((mimetype (hmm-file-mime-type thing)))
    (mapcar
     (lambda (handler)
       (plist-get handler :command))
     (seq-filter
      (lambda (handler)
        (and
         (hmm-thing-match thing type handler)
         (or (null (plist-get handler :mimetypes))
             (member mimetype
                     (plist-get handler :mimetypes)))))
      (plist-get hmm-handlers type)))))

(defun hmm-thing-match (thing type handler)
  (let ((regex (plist-get handler :regex)))
    (cond ((eq type :url)
           (let ((schemes (plist-get handler :schemes))
                 (url-obj (url-generic-parse-url thing)))
             (and
              (or (null schemes)
                  (member (url-type url-obj) schemes))
              (or (not regex)
                  (string-match
                   regex
                   (hmm-url-no-scheme thing))))))
          (t (or (not regex)
                 (string-match regex thing))))))

(defun hmm-url-no-scheme (url)
  "Remove the scheme from a url."
  (let ((url-obj (url-generic-parse-url url)))
    (setf (url-type url-obj) nil
          (url-fullness url-obj) nil)
    (url-recreate-url url-obj)))

(defun hmm-update-handlers ()
  (interactive)
  (dolist (engine hmm-web-search-engines)
    (eval `(hmm-define-web-search-engine ,engine)))
  (dolist (xdg-handler (hmm-get-xdg-handlers))
    (eval `(hmm-define-external-handler ,xdg-handler :file)))
  (dolist (handler hmm-external-handlers)
    (eval `(hmm-define-external-handler ,handler ,(plist-get handler :handling))))
  (dolist (browser hmm-web-browsers)
    (hmm-add-handler
     (list :command (plist-get browser :command)
           :name (plist-get browser :name)
           :schemes (list "http" "https"))
     :url))
  (print (length  (plist-get hmm-transformers :query->:url)))
  (print (length  (plist-get hmm-handlers :url)))
  (let ((pairs))
    (dolist (transformer (plist-get hmm-transformers :query->:url))
      (dolist (handler (plist-get hmm-handlers :url))
        (pushnew (cons transformer handler) pairs)))
    (dolist (pair pairs)
      (eval `(hmm-define-compound-handler ,(car pair) ,(cdr pair)))))
  )

(defun hmm-update ()
  (interactive)
  (hmm-update-handlers)
  (hmm-define-hmm-thing-at-point)
  (message "hmm: updated."))

(defun hmm-get-xdg-handlers ()
  (seq-filter
   (lambda (handler)
     (and handler (plist-get handler :mimetypes)))
   (mapcar
    (lambda (desktop-file)
      (condition-case nil
          (let ((parsed (xdg-desktop-read-file desktop-file)))
            (list
             :name (intern (concat "xdg-desktop-"
                                   (file-name-base desktop-file)))
             :display-name (gethash "Name" parsed)
             :description (gethash "Comment" parsed)
             :external-command (gethash "Exec" parsed)
             :mimetypes (seq-filter (lambda (s) (not (string-empty-p s)))
                                    (split-string (gethash "MimeType" parsed) ";"))))
        (error nil)))
    (hmm-get-all-desktop-files))))

(defun hmm-get-all-desktop-files ()
  (mapcan (lambda (dir)
            (let ((app-dir (expand-file-name "applications" dir)))
              (when (file-exists-p app-dir)
                (directory-files app-dir t "\\.desktop$"))))
          (cons (xdg-data-home) (xdg-data-dirs))))

(defun hmm-file-mime-type (file)
  "Returns (or print if called interactively) mimetype of FILE."
  (interactive)
  (let ((out))
    (when (file-exists-p file)
      (replace-regexp-in-string
       "^.*: \\(.*\\); .*$" "\\1"
       (with-temp-buffer
         (call-process "file" nil '(t t) nil "-Li" file)
         (setq out (string-trim (buffer-string)))
         (if (called-interactively-p 'interactive)
             (message out)
           out))))))

(provide 'hmm)