Skip to content

Latest commit

 

History

History
292 lines (270 loc) · 10.2 KB

total-launcher.org

File metadata and controls

292 lines (270 loc) · 10.2 KB

Require

;;; Code:
(require 'xdg)
(require 'cl-seq)

Launcher

Variables

(defvar total-launcher-prompt "-> "
  "Prompt of total launcher.")

(defcustom total-launcher--annotation-function #'total-launcher--annotation-function-default
  "Define the function that genereate the annotation for each completion choices."
  :type 'function)

(defcustom total-launcher--action-function #'total-launcher--action-function-default
  "Define the function that is used to run the selected application."
  :type 'function)

Code

(defun total-launcher--annotation-function-default (choice choices-table)
  "Default function to annotate the completion choices."
  (let ((str (cdr (assq 'comment (gethash choice choices-table)))))
    (when str (concat " - " (propertize str 'face 'completions-annotations)))))

(defun total-launcher--action-function-default (selected choices-table)
  "Default function used to run the selected application."
  (let* ((exec (cdr (assq 'exec (gethash selected choices-table))))
	 (command (let (result)
		    (dolist (chunk (split-string exec " ") result)
		      (unless (or (equal chunk "%U")
				  (equal chunk "%F")
				  (equal chunk "%u")
				  (equal chunk "%f"))
			(setq result (concat result chunk " ")))))))
    (message command)
    (call-process-shell-command command nil 0 nil)))

;;;###autoload
(defun total-launcher-run-app (sources &optional arg)
  "Launch an application installed on your machine.
When ARG is non-nil, ignore NoDisplay property in *.desktop files."
  (interactive)
  (let* ((candidates (apply 'map-merge 'hash-table sources))
	 (result (completing-read
		  total-launcher-prompt
		  (lambda (str pred flag)
		    (if (eq flag 'metadata)
			'(metadata
			  (annotation-function . (lambda (choice)
						   (funcall
						    total-launcher--annotation-function
						    choice
                                                    candidates))))
		      (complete-with-action flag candidates str pred)))
		  (lambda (x y)
		    (if arg
			t
		      (cdr (assq 'visible y))))
		  t nil 'total-launcher nil nil)))
    (funcall total-launcher--action-function result candidates)))

Apps

Variables

(defvar total-launcher-apps-prefix nil
  "Prefix of apps.")

(defvar total-launcher--apps-cache nil
  "Cache of desktop files data.")

(defvar total-launcher-apps-add-keywords nil
  "If total launcher should add keywords to name of apps.")

(defvar total-launcher--apps-cache-timestamp nil
  "Time when we last updated the cached application list.")

(defvar total-launcher--cached-desktop-files nil
  "List of cached desktop files.")

Code

(defcustom total-launcher-apps-directories
  (mapcar (lambda (dir) (expand-file-name "applications" dir))
	  (cons (xdg-data-home)
		(xdg-data-dirs)))
  "Directories in which to search for applications (.desktop files)."
  :type '(repeat directory))

(defun total-launcher-list-desktop-files ()
  "Return an alist of all Linux applications.
Each list entry is a pair of (desktop-name . desktop-file).
This function always returns its elements in a stable order."
  (let ((hash (make-hash-table :test #'equal))
	result)
    (dolist (dir total-launcher-apps-directories)
      (when (file-exists-p dir)
	(let ((dir (file-name-as-directory dir)))
	  (dolist (file (directory-files-recursively dir ".*\\.desktop$"))
	    (let ((id (subst-char-in-string ?/ ?- (file-relative-name file dir))))
	      (when (and (not (gethash id hash)) (file-readable-p file))
		(push (cons id file) result)
		(puthash id file hash)))))))
    result))

(defun total-launcher-parse-desktop-files (files)
  "Parse the .desktop files to return usable informations."
  (let ((hash (make-hash-table :test #'equal)))
    (dolist (entry files hash)
      (let ((file (cdr entry)))
	(with-temp-buffer
	  (insert-file-contents file)
	  (goto-char (point-min))
	  (let ((start (re-search-forward "^\\[Desktop Entry\\] *$" nil t))
		(end (re-search-forward "^\\[" nil t))
		(visible t)
		name comment exec)
	    (catch 'break
	      (unless start
		(message "Warning: File %s has no [Desktop Entry] group" file)
		(throw 'break nil))

	      (goto-char start)
	      (when (re-search-forward "^\\(Hidden\\|NoDisplay\\) *= *\\(1\\|true\\) *$" end t)
		(setq visible nil))
	      (setq name (match-string 1))

	      (goto-char start)
	      (unless (re-search-forward "^Type *= *Application *$" end t)
		(throw 'break nil))
	      (setq name (match-string 1))

	      (goto-char start)
	      (unless (re-search-forward "^Name *= *\\(.+\\)$" end t)
		(push file counsel-linux-apps-faulty)
		(message "Warning: File %s has no Name" file)
		(throw 'break nil))
	      (setq name (match-string 1))


	      (goto-char start)
	      (when (re-search-forward "^Comment *= *\\(.+\\)$" end t)
		(setq comment (match-string 1)))
	      
	      (setq keywords nil)
	      (if total-launcher-apps-add-keywords
		  (when (re-search-forward "^Keywords *= *\\(.+\\)$" end t)
		    (setq keywords (match-string 1))))

	      (goto-char start)
	      (unless (re-search-forward "^Exec *= *\\(.+\\)$" end t)
		;; Don't warn because this can technically be a valid desktop file.
		(throw 'break nil))
	      (setq exec (match-string 1))

	      (goto-char start)
	      (when (re-search-forward "^TryExec *= *\\(.+\\)$" end t)
		(let ((try-exec (match-string 1)))
		  (unless (locate-file try-exec exec-path nil #'file-executable-p)
		    (throw 'break nil))))

	      (if total-launcher-apps-add-keywords
					; FIXME
		  (puthash (if keywords 
			       (concat 
				total-launcher-apps-prefix 
				name 
				" [" keywords "]") 
			     (concat 
			      total-launcher-apps-prefix 
			      name))
			   (list (cons 'exec exec)
				 (cons 'comment comment)
				 (cons 'visible visible))
			   hash)
		(puthash (concat total-launcher-apps-prefix name)
			 (list (cons 'exec exec)
			       (cons 'comment comment)
			       (cons 'visible visible))
			 hash)))))))))

(defun total-launcher-list-apps ()
  "Return list of all Linux .desktop applications."
  (let* ((new-desktop-alist (total-launcher-list-desktop-files))
	 (new-files (mapcar 'cdr new-desktop-alist)))
    (unless (and (equal new-files total-launcher--cached-desktop-files)
		 (null (cl-find-if
			(lambda (file)
			  (time-less-p
			   total-launcher--apps-cache-timestamp
			   (nth 5 (file-attributes file))))
			new-files)))
      (setq total-launcher--apps-cache (total-launcher-parse-desktop-files new-desktop-alist))
      (setq total-launcher--apps-cache-timestamp (current-time))
      (setq total-launcher--cached-desktop-files new-files)))
  total-launcher--apps-cache)

Directories

(defun total-launcher-list-directory-contents-recursively (directory hide-path all prefix command-to-open)
  "Return files in DIRECTORY, rercursively.
When HIDE-PATH is non-nil, will only show names of files.
When ALL is non-nil, will search in hidden files also.
This may cause problems if directory contains files with same names.
PREFIX is a prefix to all names.  COMMAND-TO-OPEN is a command to open
files.  Does not work properly with symlinks, directory names including
newline may cause problems."
  (let* ((output (substring
		  (shell-command-to-string
		   (concat (if all "ls -R1pa " "ls -R1p ") directory))
		  0 -1)) ;; Optimise!
	 (path-length (length (expand-file-name directory)))
	 (subdirectories-list (split-string output "\n\n"))
	 (hash (make-hash-table :test #'equal)))
    (dolist (directory-with-contents subdirectories-list hash)
      (let* ((separated (split-string directory-with-contents "\n"))
	     (current-directory (substring (car separated) 0 -1))
	     (files (cdr separated)))
	(if hide-path
	    (dolist (file files)
	      (unless (string= (substring file -1 nil) "/")
		(puthash (concat prefix file)
			 (list (cons 'exec
				     (concat
				      command-to-open " '" current-directory "'/'" file "'"))
			       (cons 'visible t)
			       (cons 'comment nil))
			 hash)))
	  (let* ((local-path (substring current-directory path-length nil)))
	    (dolist (file files)
	      (unless (string= (substring file -1 nil) "/")
		(puthash (concat prefix local-path "/" file)
			 (list (cons 'exec
				     (concat
				      command-to-open " '" current-directory "'/'" file "'"))
			       (cons 'visible t)
			       (cons 'comment nil))
			 hash)))))))))

(defun total-launcher-list-directory-contents (directory all prefix command-to-open)
  "Return files in DIRECTORY, rercursively.
When ALL is non-nil, will search in hidden files also.
This may cause problems if directory contains files with same names.
PREFIX is a prefix to all names.  COMMAND-TO-OPEN is a command to open
files.  Does not work properly with symlinks, directory names including
newline may cause problems."
  (let* ((output (substring
		  (shell-command-to-string (concat (if all "ls -1pa " "ls -1p ") directory))
		  0 -1)) ;; Optimise!
	 (files (split-string output "\n"))
	 (directory-right-name (shell-quote-argument (expand-file-name directory)))
	 (hash (make-hash-table :test #'equal)))
    (dolist (file files hash)
      (unless (string= (substring file -1 nil) "/")
	(puthash (concat prefix file)
		 (list (cons 'exec
			     (concat
			      command-to-open " " directory-right-name "'" file "'"))
		       (cons 'visible t)
		       (cons 'comment nil))
		 hash)))))

Recent files

Variables

(defvar total-launcher-command-to-open-file "xdg-open "
  "Command used to open file.")

(defvar total-launcher-recentf-prefix "Recent: "
  "Prefix of recent files.")

Code

(defun total-launcher-list-recentf ()
  "Return hash table of recent file names (from recentf-list) and commands to open them."
  (let ((hash (make-hash-table :test #'equal)))
    (dolist (filename recentf-list hash)
      (puthash (concat total-launcher-recentf-prefix filename)
	       (list (cons 'exec (concat
				  total-launcher-command-to-open-file
				  (shell-quote-argument (expand-file-name filename))))
		     (cons 'comment nil)
		     (cons 'visible t))
	       hash))))

Provide

(provide 'total-launcher)
;;; total-launcher.el ends here