├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.org ├── old ├── browser-open-prompt.sh └── powerlisp-v1.0.lisp └── powerlisp.lisp /.gitignore: -------------------------------------------------------------------------------- 1 | *.FASL 2 | *.fasl 3 | *.lisp-temp 4 | *.dfsl 5 | *.pfsl 6 | *.d64fsl 7 | *.p64fsl 8 | *.lx64fsl 9 | *.lx32fsl 10 | *.dx64fsl 11 | *.dx32fsl 12 | *.fx64fsl 13 | *.fx32fsl 14 | *.sx64fsl 15 | *.sx32fsl 16 | *.wx64fsl 17 | *.wx32fsl 18 | *~ 19 | *#*# 20 | 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.5.0 - 2018-09-03 4 | ### Added 5 | - Add better support for `rofi` and other `rofi`-friendly features. 6 | - Add customization option for displaying the launcher on bottom or screen 7 | or not (on by default). 8 | - Add macro `with-powerlisp-user-input`, which only executes its body 9 | when the user entered something on the input field. 10 | - Add macro `with-powerlisp-menu`, which is similar to 11 | `with-powerlisp-options-menu`, but does not require the input to be on the 12 | given alist, allowing to have access to the user input with the `raw-input` 13 | variable on the body, unless the user has not typed anything. 14 | - Add private navigation support when calling the browser. 15 | 16 | ### Changed 17 | - Refactor code completely. 18 | - Reorganize sections of the code for clarity. 19 | - Changed names of variables for launcher-related customizations. 20 | - Fix `dmenu` customization options which would not work. 21 | 22 | ### Removed 23 | - Zeal support. This should be a user-defined feature. 24 | Examples are maintained on the Wiki. 25 | - Default favorite websites and commands. 26 | Those should be a personal choice of the user. 27 | 28 | 29 | ## 1.0.0 - 2018-06-05 30 | ### Added 31 | - Port bash version's basic, expected features. 32 | - Add Zeal support. 33 | - Add basic customization API. 34 | - Add trivial aesthetic settings. 35 | - Add FASL compilation instructions to README. 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lucas Vieira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Powerlisp 2 | #+AUTHOR: Lucas Vieira 3 | #+MAIL: lucasvieira@lisp.com.br 4 | 5 | Common Lisp tool for automating tasks for Unix power-users. 6 | 7 | * Abstract 8 | 9 | Powerlisp is a simple and flexible tool which helps you automate your work. With 10 | the aid of =dmenu= or =rofi=, this Lisp script allows you to: 11 | 12 | - Navigate directly to your favorite websites, in your favorite browser 13 | (including incognito mode); 14 | - Search the web on your favorite search engine (and switch search engines as 15 | well); 16 | - Use flexible configuration files written on Lisp, provided by hygienic user 17 | bindings; 18 | - Create your own menus, submenus and input, while allowing you to extend it 19 | from within the user bindings, in any way you want; 20 | - Hack it any way you'd want and make it yours! 21 | 22 | This tool is written with =dmenu= and/or =rofi= in mind, plus =notify-send= for 23 | desktop notifications, and only runs using the =sbcl= compiler. While it is 24 | certainly possible to use other implementations, those may require 25 | implementation-specific extensions, which is what I am using. 26 | 27 | I opted for this instead of using any external, cross-implementation package 28 | (such as Quicklisp) for performance reasons. 29 | 30 | 31 | * Motivation 32 | 33 | Back in the day, I used to have a simple tool written in Bash which would allow 34 | me to open menus and nest them just like you see. The purpose was just to store 35 | my favorite websites and perform searches on the internet, without directly 36 | opening the browser in order to do so. 37 | 38 | Turns out this approach wasn't very flexible, and extremely hackish; this made 39 | me realize that bash is also not a language you would want to mess up with, when 40 | things get heated, due to its lack of easy support to trivial tasks such as 41 | string manipulation and this sort of thing. 42 | 43 | At this point, there were two things I could do: either switch to a shell which 44 | gives me better scripting capabilities, or use an actual programming language 45 | to rewrite this tool altogether. As you can see, I chose the latter. 46 | 47 | So, why Lisp? Well, because I could, and also to prove a point. Most tools 48 | nowadays are written in Javascript or Python, two languages which can be much 49 | slower than Common Lisp at times. However, Common Lisp is a perfectly functional 50 | and powerful language, albeit a little old. Plus, I just did not want to choose 51 | those languages, since we see them running everywhere, even though sometimes it 52 | is not the best use case. Instead, I went ahead and start taking advantage of 53 | CL's scripting support, something that was already being used on tools such as 54 | =Roswell=, for example. 55 | 56 | And so the first version of this tool arised, though it was a bit lacking; but 57 | little by little, I started adding things to my taste, which I felt would 58 | improve my workflow. 59 | 60 | 61 | * Usage 62 | 63 | All you need to do is invoke the script using a command line: 64 | 65 | #+BEGIN_SRC bash 66 | sbcl --script /path/to/powerlisp.lisp --no-linedit 67 | #+END_SRC 68 | 69 | You can input this command on your favorite tool for managing keybindings, which 70 | is native on window managers such as =openbox=, for example. On my end, I use 71 | =sxhkd= for configuring keybindings, and so I have a shortcut which spawns this 72 | very line of code. 73 | 74 | Make sure you have Steel Bank Common Lisp (=sbcl=) installed. It should work on 75 | any newer version (currently runs well under version 1.4.10). 76 | 77 | Make sure you have =dmenu= installed as well. You can also use =rofi=, though 78 | using it will require a few lines of configuration; see the Wiki for examples. 79 | 80 | 81 | * Hacking it 82 | 83 | As stated before, Powerlisp is highly hackable. 84 | 85 | There are some functions, macros and variables which any user may want to 86 | change; they will be better documented on the Wiki, so make sure you check 87 | it out. Below I've listed all of them in their order of appearance and 88 | cathegory, for a quick reference. 89 | 90 | Breaking changes have been stated on the CHANGELOG file, so you should probably 91 | look at it as well. 92 | 93 | #+BEGIN_SRC lisp 94 | ;;; Global, customizable variables 95 | *browser-command* 96 | *browser-incognito-flag* 97 | *notify-command* 98 | *launcher-prompt* 99 | *launcher-command* 100 | *launcher-on-bottom* 101 | *launcher-rofi-emulate-dmenu* 102 | *launcher-font* 103 | *launcher-bg* 104 | *launcher-fg* 105 | *launcher-sel-bg* 106 | *launcher-sel-fg* 107 | *default-search-engine* 108 | *rerun-main-menu* ; only change when using submenus 109 | 110 | ;;; Runtime variables, do not change directly 111 | *search-engines* 112 | *favorite-websites* 113 | *subcommands* 114 | *incognito-mode* 115 | 116 | ;;; User customization API 117 | ;; Functions 118 | (powerlisp-add-favorite atom url) 119 | (powerlisp-add-multi-favorites favorites-list) 120 | (powerlisp-add-search-engine atom query-parts) 121 | (powerlisp-add-multi-search-engines engines-list) 122 | (powerlisp-add-command command callback) 123 | (powerlisp-add-multi-commands commands-list) 124 | (powerlisp-spawn-menu prompt alist) 125 | (powerlisp-notify text &optional title) 126 | (powerlisp-call-browser website) 127 | (powerlisp-call-external program-path &rest arguments...) 128 | 129 | ;; Macros 130 | (powerlisp-request-user-input &optional prompt) 131 | (with-powerlisp-user-input (&optional prompt) &body body...) 132 | (with-powerlisp-options-menu (prompt alist) &body body...) 133 | (with-powerlisp-menu (prompt alist) &body body...) 134 | #+END_SRC 135 | -------------------------------------------------------------------------------- /old/browser-open-prompt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROMPT="Navegar ou pesquisar >> " 4 | REGEX='^(https?|ftp|file)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-‌​A-Za-z0-9\+&@#/%=~_|‌​]$' 5 | COMMON="https://\nhttp://\nstartpage\nduckduckgo\nreddit\ntwitter\nmastodon\nnetflix\nprotonmail\nhooktube\ngithub\nlinkedin\nhackernews\nslashdot\ninstagram\nwhatsapp\nwikipedia\nwolfram\ncplusplus" 6 | 7 | FONT="xft:Fixed:size=9" 8 | BG="#FFFFFF" 9 | FG="#000000" 10 | SB="#000000" 11 | SF="#FFFFFF" 12 | 13 | PARAMS="-b -fn $FONT -nb $BG -nf $FG -sb $SB -sf $SF -l 0" 14 | 15 | # If we have a favorite websites file, then run it now 16 | if [ -f ~/.browser-favorites.sh ] ; then 17 | source ~/.browser-favorites.sh 18 | fi 19 | 20 | 21 | # Predefine functions for suboperations 22 | hooktube_search() 23 | { 24 | PROMPT="Pesquisar no HookTube >> " 25 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 26 | 27 | if [ -z "$SEARCH" ] ; then 28 | echo "" 29 | elif [ "$SEARCH" == "[abrir website]" ] ; then 30 | echo "https://hooktube.com/" 31 | else 32 | notify-send "Pesquisando no YouTube por \"${SEARCH}\"..." 33 | SEARCH=${SEARCH//[+]/%2B} 34 | SEARCH=${SEARCH//[ ]/+} 35 | echo "https://hooktube.com/results?search_query=${SEARCH}" 36 | fi 37 | } 38 | 39 | 40 | facebook_search() 41 | { 42 | PROMPT="Pesquisar no Facebook >> " 43 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 44 | 45 | if [ -z "$SEARCH" ] ; then 46 | echo "" 47 | elif [ "$SEARCH" == "[abrir website]" ] ; then 48 | echo "https://facebook.com/" 49 | else 50 | notify-send "Pesquisando no Facebook por \"${SEARCH}\"..." 51 | SEARCH=${SEARCH//[+]/%2B} 52 | SEARCH=${SEARCH//[ ]/+} 53 | echo "https://facebook.com/search/top/?q=${SEARCH}" 54 | fi 55 | } 56 | 57 | 58 | twitter_search() 59 | { 60 | PROMPT="Pesquisar no Twitter >> " 61 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 62 | 63 | if [ -z "$SEARCH" ] ; then 64 | echo "" 65 | elif [ "$SEARCH" == "[abrir website]" ] ; then 66 | echo "https://twitter.com/" 67 | else 68 | notify-send "Pesquisando no Twitter por \"${SEARCH}\"..." 69 | SEARCH=${SEARCH//[+]/%2B} 70 | SEARCH=${SEARCH//[ ]/+} 71 | echo "https://twitter.com/search?q=${SEARCH}" 72 | fi 73 | } 74 | 75 | 76 | 77 | wikipedia_search() 78 | { 79 | PROMPT="Pesquisar na Wikipedia >> " 80 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 81 | 82 | if [ -z "$SEARCH" ] ; then 83 | echo "" 84 | elif [ "$SEARCH" == "[abrir website]" ] ; then 85 | echo "https://wikipedia.org/" 86 | else 87 | notify-send "Pesquisando na Wikipedia por \"${SEARCH}\"..." 88 | SEARCH=${SEARCH//[+]/%2B} 89 | SEARCH=${SEARCH//[ ]/+} 90 | echo "https://en.wikipedia.org/w/index.php?search=${SEARCH}&title=Special%3ASearch" 91 | fi 92 | } 93 | 94 | cplusplus_search() 95 | { 96 | PROMPT="Pesquisar no cplusplus (http) >> " 97 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 98 | 99 | if [ -z "$SEARCH" ] ; then 100 | echo "" 101 | elif [ "$SEARCH" == "[abrir website]" ] ; then 102 | echo "http://cplusplus.com/" 103 | else 104 | notify-send "Pesquisando no cplusplus por \"${SEARCH}\"..." 105 | SEARCH=${SEARCH//[+]/%2B} 106 | SEARCH=${SEARCH//[ ]/+} 107 | echo "http://cplusplus.com/search.do?q=${SEARCH}" 108 | fi 109 | } 110 | 111 | 112 | wolfram_search() 113 | { 114 | PROMPT="Pesquisar no Wolfram|Alpha >> " 115 | SEARCH=`printf "[abrir website]" | dmenu $PARAMS -p "$PROMPT"` 116 | 117 | if [ -z "$SEARCH" ] ; then 118 | echo "" 119 | elif [ "$SEARCH" == "[abrir website]" ] ; then 120 | echo "https://www.wolframalpha.com/" 121 | else 122 | notify-send "Pesquisando no Wolfram|Alpha por \"${SEARCH}\"..." 123 | SEARCH=${SEARCH//[+]/%2B} 124 | SEARCH=${SEARCH//[ ]/+} 125 | echo "https://www.wolframalpha.com/input/?i=${SEARCH}" 126 | fi 127 | } 128 | 129 | favselect() 130 | { 131 | PROMPT="Favoritos >> " 132 | FAVORITES="wtf" 133 | 134 | for website in "${!PREFS[@]}" ; do 135 | FAVORITES="${FAVORITES}\n${website}" 136 | done 137 | 138 | URL=`printf "${FAVORITES}" | dmenu $PARAMS -p "$PROMPT"` 139 | } 140 | 141 | 142 | 143 | #Comment if you want to show it on main menu 144 | COMMON="${COMMON}\nfavorites" 145 | 146 | # Also, loop through our favorites list and append them to our commons list 147 | # Uncomment if you want to show it on main menu 148 | #for website in "${!PREFS[@]}" ; do 149 | # COMMON="${COMMON}\n${website}" ; 150 | #done 151 | 152 | # Get access url 153 | URL=`printf "$COMMON" | dmenu $PARAMS -p "$PROMPT"` 154 | 155 | 156 | # Comment if you're showing favorites on main menu 157 | if [[ "$URL" == "favorites" ]] ; then 158 | favselect 159 | fi 160 | 161 | # Switch for default options 162 | case $URL in 163 | "startpage") 164 | URL="https://startpage.com/" ;; 165 | #"facebook") 166 | # URL="$(facebook_search)" ;; 167 | "duckduckgo") 168 | URL="https://duckduckgo.com/" ;; 169 | "reddit") 170 | URL="https://reddit.com/" ;; 171 | "twitter") 172 | URL="$(twitter_search)" ;; 173 | "mastodon") 174 | URL="https://mastodon.gamedev.place/" ;; 175 | "netflix") 176 | URL="https://netflix.com/" ;; 177 | "protonmail") 178 | URL="https://mail.protonmail.com/login" ;; 179 | "hooktube") 180 | URL="$(hooktube_search)" ;; 181 | "github") 182 | URL="https://github.com/" ;; 183 | "linkedin") 184 | URL="https://linkedin.com/" ;; 185 | "hackernews") 186 | URL="https://news.ycombinator.com/" ;; 187 | "slashdot") 188 | URL="https://slashdot.org/" ;; 189 | "instagram") 190 | URL="https://instagram.com/" ;; 191 | "whatsapp") 192 | URL="https://web.whatsapp.com/" ;; 193 | "wikipedia") 194 | URL="$(wikipedia_search)" ;; 195 | "wolfram") 196 | URL="$(wolfram_search)" ;; 197 | "cplusplus") 198 | URL="$(cplusplus_search)" ;; 199 | esac 200 | 201 | 202 | # Switch for user-created options 203 | # If it is set... 204 | if [ ! -z "$URL" ] && [ -z ${PREFS+x} ] ; then 205 | #Check if input is in hash table 206 | if test "${PREFS[$URL]+isset}" ; then 207 | URL="${PREFS[$URL]}" 208 | fi 209 | fi 210 | 211 | if [ -z "$URL" ] ; then 212 | # Do nothing... 213 | echo "Acesso cancelado." 214 | elif [ "$URL" == "https://" ] || [ "$URL" == "http://" ] ; then 215 | echo "Você precisa especificar sua URL." 216 | notify-send "Você precisa especificar sua URL." 217 | # Check if URL is valid 218 | elif [[ $URL =~ $REGEX ]] ; then 219 | echo "Acessando $URL..." 220 | #notify-send "Acessando $URL..." 221 | xdg-open $URL 222 | else 223 | echo "Pesquisando por \"$URL\"..." 224 | notify-send "Pesquisando por \"$URL\"..." 225 | URL=${URL//[+]/%2B} 226 | URL=${URL//[ ]/+} 227 | xdg-open "https://duckduckgo.com/?q=${URL}" 228 | fi 229 | 230 | -------------------------------------------------------------------------------- /old/powerlisp-v1.0.lisp: -------------------------------------------------------------------------------- 1 | ;;;; powerlisp.lisp 2 | ;;;; A useful utility for Linux powerusers. 3 | ;;;; Copyright © 2018 Lucas Vieira 4 | ;;;; 5 | ;;;; Licensed under the MIT License. 6 | ;;;; Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | ;;;; this software and associated documentation files (the "Software"), to deal in 8 | ;;;; the Software without restriction, including without limitation the rights to 9 | ;;;; use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | ;;;; the Software, and to permit persons to whom the Software is furnished to do so, 11 | ;;;; subject to the following conditions: 12 | ;;;; 13 | ;;;; The above copyright notice and this permission notice shall be included in all 14 | ;;;; copies or substantial portions of the Software. 15 | ;;;; 16 | ;;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | ;;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | ;;;; FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | ;;;; COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | ;;;; IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | ;;;; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | ;;; Important stuff: 25 | ;;; This script assumes that you are using certain default programs and settings, 26 | ;;; which are the ones I am using: 27 | ;;; - SBCL as Common Lisp implementation 28 | ;;; - Font: Fixed Semicondensed 9 29 | ;;; - Black-and-white theme 30 | ;;; - DuckDuckGo as default search engine 31 | ;;; - dmenu as launcher 32 | ;;; - Firefox as browser 33 | ;;; - Zeal as documentation reader 34 | ;;; Do not forget to change those to your liking. Hack this file as much as you can. 35 | 36 | ;;; Run this file using: 37 | ;;; $ sbcl --script powerlisp.lisp 38 | ;;; Notice that I try not to rely on Quicklisp for anything. This is intentional 39 | ;;; so that the shortcut launches faster. 40 | 41 | 42 | ;; ============================================ 43 | ;; Look and programs customization 44 | 45 | (defparameter *input-font* "xft:LucidaTypewriter:size=8") 46 | (defparameter *input-bg* "#000000") 47 | (defparameter *input-fg* "#bbbbbb") 48 | (defparameter *input-sel-bg* "#ffffff") 49 | (defparameter *input-sel-fg* "#000000") 50 | (defparameter *default-search-engine* 'duckduckgo) 51 | (defparameter *browser-command* "/usr/bin/firefox") 52 | (defparameter *notify-command* "/usr/bin/notify-send") 53 | (defparameter *zeal-command* "/usr/bin/zeal") 54 | (defparameter *dmenu-command* "/usr/bin/dmenu") 55 | (defparameter *input-params* 56 | (list "-b" 57 | "-fn" *input-font* 58 | "-nb" *input-bg* 59 | "-nf" *input-fg* 60 | "-sb" *input-sel-bg* 61 | "-sf" *input-sel-fg* 62 | "-l" "0")) 63 | 64 | 65 | ;; ============================================ 66 | ;; Websites, search engines, services 67 | 68 | (defparameter *favorite-websites* 69 | '((reddit . "https://reddit.com") 70 | (twitter . "https://twitter.com") 71 | (netflix . "https://netflix.com") 72 | ;;(hooktube . "https://hooktube.com") 73 | (youtube . "https://youtube.com") 74 | (protonmail . "https://mail.protonmail.com/login") 75 | (gitlab . "https://gitlab.com") 76 | (linkedin . "https://linkedin.com") 77 | (hackernews . "https://news.ycombinator.com") 78 | (slashdot . "https://slashdot.org") 79 | (instagram . "https://instagram.com") 80 | (whatsapp . "https://web.whatsapp.com") 81 | (cplusplus . "http://cplusplus.com"))) 82 | 83 | (defparameter *search-engines* 84 | '((duckduckgo ("https://duckduckgo.com/?q=")) 85 | (startpage ("https://startpage.com/do/search?language=english&cat=web&query=")) 86 | ;;(hooktube ("https://hooktube.com/results?search_query=")) 87 | (youtube ("https://youtube.com/results?search_query=")) 88 | (twitter ("https://twitter.com/search?q=")) 89 | (wikipedia ("https://en.wikipedia.org/w/index.php?search=" 90 | "&title=Special%3ASearch")) 91 | (github ("https://github.com/search?utf8=%E2%9C%93&q=" 92 | "&type=")) 93 | (wolfram ("https://www.wolframalpha.com/input/?i=")) 94 | (cplusplus ("http://cplusplus.com/search.do?q=")) 95 | (urbandict ("https://urbandictionary.com/define.php?term=")) 96 | (knowyourmeme ("http://knowyourmeme.com/search?q=")))) 97 | 98 | (defparameter *zeal-docs* 99 | '((c . "c") 100 | (c++ . "cpp") 101 | (common-lisp . "lisp") 102 | (css . "css") 103 | (emacs-lisp . "elisp") 104 | (erlang . "erlang") 105 | (go . "go") 106 | (haskell . "haskell") 107 | (html . "html") 108 | (js . "javascript") 109 | (julia . "julia") 110 | (latex . "latex") 111 | (markdown . "markdown") 112 | (php . "php") 113 | (processing . "processing") 114 | (rust . "rust") 115 | (bootstrap . "bootstrap") 116 | (jquery . "jquery"))) 117 | 118 | (defparameter *subcommands* nil) 119 | 120 | ;; ============================================ 121 | ;; Command calling helpers 122 | 123 | (defun *build-command* (command-parts &optional (query nil)) 124 | "Build a command using its first part, its next part 125 | and a query, if it is a search engine. If said query exists, 126 | it is sandwiched between the first and last parts." 127 | (concatenate 'string 128 | (car command-parts) 129 | query 130 | (apply #'concatenate 'string (cdr command-parts)))) 131 | 132 | (defun send-notification (&rest text) 133 | "Sends a notification to the desktop." 134 | #+SBCL (sb-ext:run-program *notify-command* text)) 135 | 136 | (defun request-input (prompt options) 137 | "Requests input using your input method. You may provide 138 | selection options. Yields the user input as a string." 139 | #+SBCL 140 | (let* ((process (sb-ext:run-program *dmenu-command* 141 | (append (list "-p" prompt) 142 | *input-params*) 143 | :input :stream 144 | :output :stream 145 | :wait nil)) 146 | (process-input (sb-ext:process-input process)) 147 | (process-output (sb-ext:process-output process)) 148 | (input-options (mapcar (lambda (x) 149 | (string-downcase (format nil "~a~%" x))) 150 | options))) 151 | ;; Submit list of elements to process input 152 | (format process-input "~a" (apply #'concatenate 'string input-options)) 153 | (finish-output process-input) 154 | (close process-input) 155 | ;; Await process end and dump what we read 156 | (sb-ext:process-wait process) 157 | (when (listen process-output) 158 | (read-line process-output)))) 159 | 160 | (defun powerlisp-call-browser (&rest website) 161 | "Effectively calls the browser with the given website as argument." 162 | #+SBCL (sb-ext:run-program *browser-command* website :wait nil)) 163 | 164 | (defun call-docs (docset-result search-query) 165 | "Calls Zeal with the documentation we need." 166 | #+SBCL (sb-ext:run-program 167 | *zeal-command* 168 | (list (concatenate 'string 169 | docset-result 170 | ":" 171 | search-query)) 172 | :wait nil)) 173 | 174 | (defun powerlisp-call-external (program-path &rest arguments) 175 | "Calls an external command and does not wait for the process to 176 | finish. `program-path` needs to be an absolute path to the binary. 177 | `arguments` is a list of strings, where each string is an argument. 178 | The arguments need to be isolated, with no whitespace inbetween." 179 | (when (and (stringp program-path) 180 | (every #'stringp arguments)) 181 | #+SBCL (sb-ext:run-program 182 | program-path 183 | arguments 184 | :wait nil))) 185 | 186 | 187 | ;; ============================================ 188 | ;; User input processing 189 | 190 | (defun atom-convert (output-string) 191 | "Convert a user-received string to an atom. 192 | This might need security tweaks..." 193 | (intern 194 | (string-trim " " (string-upcase output-string)))) 195 | 196 | (defun match-output (output-string options) 197 | "Match a string given from the output of a process to a list 198 | of options. Yields both the atom and the value associated with the 199 | string on the referred list of options." 200 | (let* ((input-atom (atom-convert output-string)) 201 | (associated-value (cdr (assoc input-atom options)))) 202 | (values input-atom associated-value))) 203 | 204 | (defun build-search-query (query engine) 205 | "Builds a search query for the given search engine. Yields the 206 | prepared URL as a string." 207 | (let ((engine-query-format (assoc engine *search-engines*))) 208 | (if (null engine-query-format) 209 | (send-notification "POWERLISP: ERROR" 210 | (format nil "Cannot find search engine \"~a\"" 211 | engine)) 212 | (let ((query-begin (caadr engine-query-format)) 213 | (query-rest (cdadr engine-query-format))) 214 | (concatenate 'string 215 | query-begin 216 | query 217 | (apply #'concatenate 'string query-rest)))))) 218 | 219 | (defun options-to-list (options) 220 | "Converts the atoms which are associated with certain URLs in an alist 221 | to a single list of those atoms." 222 | (loop for opt in options collect (car opt))) 223 | 224 | ;; ============================================ 225 | ;; User interface management 226 | 227 | (defun request-search () 228 | "Prompts the search menu. Asks for the search engine to 229 | be used, and for the query to be searched." 230 | (let ((command-result 231 | (request-input "Search engine?" 232 | (options-to-list *search-engines*)))) 233 | (multiple-value-bind (engine-atom engine-query-info) 234 | (match-output command-result *search-engines*) 235 | (if (null engine-query-info) 236 | (when (not (null engine-atom)) 237 | (send-notification 238 | "POWERLISP: ERROR" 239 | (format nil "Unknown search engine \"~a\"" engine-atom))) 240 | (let ((search-input (request-input "Search target?" nil))) 241 | (when (not (null search-input)) 242 | (send-notification 243 | "POWERLISP SEARCH" 244 | (format nil "Searching for \"~a\" in ~a..." 245 | search-input engine-atom)) 246 | (powerlisp-call-browser (build-search-query search-input engine-atom)))))))) 247 | 248 | (defun request-docs () 249 | "Prompts the documentation search menu. Asks for a docset 250 | and then searches the entry on it." 251 | (let ((docset-result 252 | (request-input "Docset?" 253 | (options-to-list *zeal-docs*)))) 254 | (multiple-value-bind (docset-atom docset-prefix) 255 | (match-output docset-result *zeal-docs*) 256 | (if (null docset-prefix) 257 | (when (not (null docset-atom)) 258 | (send-notification 259 | "POWERLISP: ERROR" 260 | (format nil "Unknown docset \"~a\"" docset-atom))) 261 | (let ((search-input (request-input "Search target?" nil))) 262 | (when (not (null search-input)) 263 | (send-notification 264 | "POWERLISP DOC SEARCH" 265 | (format nil "Searching for \"~a\" in ~a DOCS..." 266 | search-input docset-atom)) 267 | (call-docs docset-prefix search-input))))))) 268 | 269 | ;; ============================================ 270 | ;; USER CONFIGURATION API 271 | ;; Add them to ~/.powerlisp or to ~/.config/powerlisp.lisp. 272 | ;; Use the following functions to add stuff. 273 | 274 | (defun powerlisp-add-favorite (atom url) 275 | "Add a single favorite website to favorites list." 276 | (setf *favorite-websites* 277 | (append *favorite-websites* 278 | (list (cons atom url))))) 279 | 280 | (defun powerlisp-add-search-engine (atom query-parts) 281 | "Add a single search engine to search engines list. 282 | The query-parts parameter must be a list of query components, 283 | with the first one coming before the query value, and the rest 284 | coming after the query value. These strings are concatenated 285 | in this order." 286 | (setf *search-engines* 287 | (append *search-engines* 288 | (list (list atom query-parts))))) 289 | 290 | (defun powerlisp-add-multi-favorites (favorites-list) 291 | "Adds many favorites to the favorites list. 292 | Format of the list must follow the format for the favorites list. 293 | Using this function instead of powerlisp-add-favorite is recommended 294 | when you have many websites." 295 | (setf *favorite-websites* 296 | (append *favorite-websites* 297 | favorites-list))) 298 | 299 | (defun powerlisp-add-multi-search-engines (engines-list) 300 | "Adds many search engines to the search engines list. 301 | Format of the list must follow the format for the search engines list. 302 | Using this function instead of powerlisp-add-search-engine is recommended 303 | when you have many engines." 304 | (setf *search-engines* 305 | (append *search-engines* 306 | engines-list))) 307 | 308 | 309 | (defun powerlisp-add-command (command callback) 310 | "Adds a command to Powerlisp. 311 | command is the command atom, callback must be a zero-arguments function." 312 | (when (functionp callback) 313 | (setf *subcommands* 314 | (append (list (cons command callback)) 315 | *subcommands*)))) 316 | 317 | (defun powerlisp-add-multi-commands (commands-list) 318 | "Adds many commands to Powerlisp at once. 319 | The list of commands must be a list comprised of consed 320 | atoms + procedures. It is important to maintain this structure in order 321 | for this to work." 322 | (when (every (lambda (entry) (functionp (cdr entry))) 323 | commands-list) 324 | (setf *subcommands* 325 | (append commands-list *subcommands*)))) 326 | 327 | 328 | (defun powerlisp-add-documentation (command prefix-string) 329 | "Adds a new documentation set to the end of Powerlisp's 330 | documentations. The command is an identification atom, while the prefix-string 331 | is the string to preceed the doc search query." 332 | (setf *zeal-docs* 333 | (append (list (cons command prefix-string)) 334 | *zeal-docs*))) 335 | 336 | (defun powerlisp-spawn-menu (prompt alist) 337 | "Spawns an input menu with the given prompt, and offers an alist 338 | of values. This function yields two values: an atom equivalent to the user 339 | input and, if the option selected is valid, yields the associated value 340 | as well; if not, yields nil instead." 341 | (multiple-value-bind (atom assoc-value) 342 | (match-output (request-input prompt 343 | (options-to-list alist)) 344 | alist) 345 | (when assoc-value (values atom assoc-value)))) 346 | 347 | (defun powerlisp-request-user-input (&optional (prompt "input?")) 348 | "Spawns an input menu with no options. The value returned is a 349 | plain string containing what the user typed. One can customize 350 | the prompt by feeding it to this function." 351 | (request-input prompt nil)) 352 | 353 | (defmacro with-powerlisp-options-menu ((prompt alist) &body body) 354 | "Calls an options menu using an alist. If the input matches any of 355 | the values on the alist, the input is bound as an atom to `option`, 356 | and the associated value is bound to `assoc-value`. The body is then 357 | executed." 358 | `(multiple-value-bind (option assoc-value) 359 | (powerlisp-spawn-menu ,prompt ,alist) 360 | (when assoc-value ,@body))) 361 | 362 | (defun powerlisp-notify (text &optional (title "POWERLISP")) 363 | "Sends a notification to the desktop. One can optionally setup the 364 | notification title." 365 | (send-notification "POWERLISP" text)) 366 | 367 | 368 | ;; ============================================ 369 | ;; User configuration loading 370 | 371 | ;; Magic for loading default configuration 372 | (when (probe-file "~/.powerlisp") 373 | (load "~/.powerlisp")) 374 | 375 | (when (probe-file "~/.config/powerlisp.lisp") 376 | (load "~/.config/powerlisp.lisp")) 377 | 378 | ;; ============================================ 379 | ;; Build list of common commands 380 | 381 | (powerlisp-add-command 'docs #'request-docs) 382 | (powerlisp-add-command 'search #'request-search) 383 | 384 | (defun request-command (command-atom) 385 | "Dispatches the requested command." 386 | (let ((function (cdr (assoc command-atom *subcommands*)))) 387 | (when (functionp function) 388 | (funcall function)))) 389 | 390 | 391 | (defun run-powerlisp () 392 | "Prompts the favorites menu. Asks for the user to type one of the 393 | favorite websites prompted, or a command (such as search), or even 394 | for text which will be converted to a search query." 395 | (let* ((subcommands-list (options-to-list *subcommands*)) 396 | (command-result 397 | (request-input "Website, command, plain search?" 398 | (append subcommands-list 399 | (options-to-list *favorite-websites*))))) 400 | (multiple-value-bind (command-atom command-url) 401 | (match-output command-result *favorite-websites*) 402 | (cond ((null command-url) 403 | (cond ((member command-atom subcommands-list) 404 | (request-command command-atom)) 405 | (t (when (not (null command-result)) 406 | (send-notification 407 | "POWERLISP PLAIN SEARCH" 408 | (format nil "Searching for \"~a\"..." 409 | command-result)) 410 | ;; TODO: I want to be able to just type 411 | ;; websites on the future. 412 | (powerlisp-call-browser (build-search-query 413 | command-result 414 | *default-search-engine*)))))) 415 | (t (send-notification 416 | "POWERLISP" 417 | (format nil "Opening ~a~%(~a)..." 418 | command-atom 419 | command-url)) 420 | (powerlisp-call-browser command-url)))))) 421 | 422 | 423 | (run-powerlisp) ;; Magic happens here. 424 | -------------------------------------------------------------------------------- /powerlisp.lisp: -------------------------------------------------------------------------------- 1 | ;;;; Powerlisp v1.5 2 | ;;;; powerlisp.lisp 3 | ;;;; A useful utility for Unix powerusers. 4 | ;;;; Copyright © 2018 Lucas Vieira 5 | ;;;; 6 | ;;;; Licensed under the MIT License. 7 | ;;;; Permission is hereby granted, free of charge, to any person obtaining a 8 | ;;;; copy of this software and associated documentation files (the "Software"), 9 | ;;;; to deal in the Software without restriction, including without limitation 10 | ;;;; the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | ;;;; and/or sell copies of the Software, and to permit persons to whom the 12 | ;;;; Software is furnished to do so, subject to the following conditions: 13 | ;;;; 14 | ;;;; The above copyright notice and this permission notice shall be included in 15 | ;;;; all copies or substantial portions of the Software. 16 | ;;;; 17 | ;;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | ;;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | ;;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | ;;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | ;;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | ;;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | ;;;; DEALINGS IN THE SOFTWARE. 24 | 25 | ;;; ======================================================================== ;;; 26 | ;;; TODO-LIST ;;; 27 | ;;; ======================================================================== ;;; 28 | ;; 29 | ;; Nothing here currently. Yay! 30 | 31 | 32 | 33 | ;;; ======================================================================== ;;; 34 | ;;; Default software and settings ;;; 35 | ;;; ======================================================================== ;;; 36 | 37 | ;;; Below are contained global variables which determine the default location of 38 | ;;; software on your computer, as well as theme settings for your desired launch 39 | ;;; or menu application. 40 | ;;; You may want to customize these, or redefine them by hand on your 41 | ;;; configuration file. 42 | 43 | (defparameter *browser-command* "/usr/bin/firefox") 44 | (defparameter *browser-incognito-flag* "--private-window") 45 | (defparameter *notify-command* "/usr/bin/notify-send") 46 | (defparameter *launcher-prompt* "Powerlisp") 47 | (defparameter *launcher-command* "/usr/bin/dmenu") 48 | (defparameter *launcher-on-bottom* t) 49 | (defparameter *launcher-rofi-emulate-dmenu* nil) 50 | 51 | ;; Default theme is black-and-white. 52 | (defparameter *launcher-font* "xft:Terminus:size=8") 53 | (defparameter *launcher-bg* "#000000") 54 | (defparameter *launcher-fg* "#ffffff") 55 | (defparameter *launcher-sel-bg* "#ffffff") 56 | (defparameter *launcher-sel-fg* "#000000") 57 | 58 | (defparameter *default-search-engine* 'duckduckgo) 59 | 60 | ;; Below are contained some default search engines. 61 | ;; You should not need to change this by hand; instead, you may use the provided 62 | ;; API for changing Powerlisp's commands (see in the sections below). 63 | 64 | (defparameter *search-engines* 65 | '((duckduckgo ("https://duckduckgo.com/?q=")) 66 | (startpage ("https://startpage.com/do/search?language=english&cat=web&query=")) 67 | (hooktube ("https://hooktube.com/results?search_query=")))) 68 | 69 | 70 | ;; The following are favorite websites. 71 | ;; It should be a list of dotted pairs but, for now, they'll be empty. 72 | (defparameter *favorite-websites* nil) 73 | 74 | ;; Those subcommands take precedence in order, over the favorite websites, 75 | ;; and are always displayed first 76 | (defparameter *subcommands* nil) 77 | 78 | ;; This variable is kind of a hack to allow a loopback effect on the main menu. 79 | ;; Set it to t anytime you want the main menu to be shown after a selection. 80 | (defparameter *rerun-main-menu* nil) 81 | 82 | ;; This variable controls whether you want to use incognito mode or not. 83 | (defparameter *incognito-mode* nil) 84 | 85 | ;;; ======================================================================== ;;; 86 | ;;; Customization API ;;; 87 | ;;; ======================================================================== ;;; 88 | 89 | ;;; The following procedures should be used when building your own configuration 90 | ;;; files. 91 | 92 | (defun powerlisp-add-favorite (atom url) 93 | "Add a single favorite website to favorites list." 94 | (setf *favorite-websites* 95 | (append *favorite-websites* (list (cons atom url))))) 96 | 97 | (defun powerlisp-add-multi-favorites (favorites-list) 98 | "Adds many favorites to the favorites list. Format of the list must follow the 99 | format for the favorites list. Using this function instead of 100 | powerlisp-add-favorite is recommended when you have many websites." 101 | (setf *favorite-websites* 102 | (append *favorite-websites* favorites-list))) 103 | 104 | (defun powerlisp-add-search-engine (atom query-parts) 105 | "Add a single search engine to search engines list. 106 | The query-parts parameter must be a list of query components, with the first one 107 | coming before the query value, and the rest coming after the query value. These 108 | strings are concatenated in this order." 109 | (setf *search-engines* 110 | (append *search-engines* (list (list atom query-parts))))) 111 | 112 | (defun powerlisp-add-multi-search-engines (engines-list) 113 | "Adds many search engines to the search engines list. 114 | Format of the list must follow the format for the search engines list. Using 115 | this function instead of powerlisp-add-search-engine is recommended when you 116 | have many engines." 117 | (setf *search-engines* 118 | (append *search-engines* engines-list))) 119 | 120 | (defun powerlisp-add-command (command callback) 121 | "Adds a command to Powerlisp. 122 | command is the command atom, callback must be a zero-arguments function." 123 | (and (functionp callback) 124 | (setf *subcommands* 125 | (append (list (cons command callback)) 126 | *subcommands*)))) 127 | 128 | (defun powerlisp-add-multi-commands (commands-list) 129 | "Adds many commands to Powerlisp at once. 130 | The list of commands must be a list comprised of consed atoms + procedures. It 131 | is important to maintain this structure in order for this to work." 132 | (and (every (lambda (entry) (functionp (cdr entry))) 133 | commands-list) 134 | (setf *subcommands* 135 | (append commands-list *subcommands*)))) 136 | 137 | 138 | ;;; The following procedures and macros may have unresolved atoms. 139 | ;;; This is due to the fact that some customization needs to be made 140 | ;;; before we define those missing atoms, since they depend on later 141 | ;;; procedures. 142 | 143 | (defun powerlisp-spawn-menu (prompt alist) 144 | "Spawns an input menu with the given prompt, and offers an alist of values. 145 | This function yields two values: an atom equivalent to the user input and, if 146 | the option selected is valid, yields the associated value as well; if not, 147 | yields nil instead." 148 | (multiple-value-bind (atom assoc-value raw-input) 149 | (pl-match-output (pl-request-input prompt 150 | (pl-options-to-list alist)) 151 | alist) 152 | (values atom assoc-value raw-input))) 153 | 154 | (defmacro powerlisp-request-user-input (&optional (prompt "input?")) 155 | "Spawns an input menu with no options. The value returned is a plain string 156 | containing what the user typed. One can customize the prompt by feeding it to 157 | this function." 158 | `(pl-request-input ,prompt nil)) 159 | 160 | (defmacro with-powerlisp-user-input ((&optional (prompt "input?")) &body body) 161 | "Calls the user input, optionally accepting an input prompt. If there is valid 162 | input, the BODY is evaluated, and said input string is bound to `input`." 163 | `(let ((input (powerlisp-request-user-input ,prompt))) 164 | (when input ,@body))) 165 | 166 | (defmacro with-powerlisp-options-menu ((prompt alist) &body body) 167 | "Calls an options menu using an alist. If the input matches any of the values 168 | on the alist, the input is bound as an atom to `option`, and the associated 169 | value is bound to `assoc-value`. The body is then executed." 170 | `(multiple-value-bind (option assoc-value) 171 | (powerlisp-spawn-menu ,prompt ,alist) 172 | (when assoc-value ,@body))) 173 | 174 | (defmacro with-powerlisp-menu ((prompt alist) &body body) 175 | "Calls an options menu using an alist, but evaluates the BODY regardless of a 176 | matched input. This macro provides bindings to an `option` atom, and to the 177 | associated value, if existing, as `assoc-value`. It also provides access to the 178 | input string from the user, assigned to `raw-input`." 179 | `(multiple-value-bind (option assoc-value raw-input) 180 | (powerlisp-spawn-menu ,prompt ,alist) 181 | (unless (or (null raw-input) 182 | (string= (string-trim '(#\Space #\Tab) raw-input) "")) 183 | ,@body))) 184 | 185 | (defun powerlisp-notify (text &optional (title "Powerlisp")) 186 | "Sends a notification to the desktop. One can optionally setup the 187 | notification title." 188 | (pl-send-notification title text)) 189 | 190 | (defun powerlisp-call-browser (website) 191 | "Effectively calls the browser with the given website as argument. If the 192 | incognito mode flag is active, the incognito flag is appended to the call." 193 | (if *incognito-mode* 194 | (pl-call-external *browser-command* *browser-incognito-flag* website) 195 | (pl-call-external *browser-command* website))) 196 | 197 | (defun powerlisp-call-external (program-path &rest arguments) 198 | "Calls an external command and does not wait for the process to 199 | finish. `program-path` needs to be an absolute path to the binary. 200 | `arguments` is a list of strings, where each string is an argument. 201 | The arguments need to be isolated, with no whitespace inbetween." 202 | (apply #'pl-call-external (cons program-path arguments))) 203 | 204 | 205 | ;;; ======================================================================== ;;; 206 | ;;; Loading User Configuration ;;; 207 | ;;; ======================================================================== ;;; 208 | 209 | ;;; The following are magic for evaluating the user configuration. 210 | (defmacro pl-probe-and-load (filename) 211 | `(when (probe-file ,filename) 212 | (load ,filename))) 213 | 214 | (pl-probe-and-load "~/.powerlisp") 215 | (pl-probe-and-load "~/.config/powerlisp.lisp") 216 | 217 | 218 | ;;; ======================================================================== ;;; 219 | ;;; Extra Configuration ;;; 220 | ;;; ======================================================================== ;;; 221 | 222 | ;;; With user bindings loaded, we can now define vital procedures, macros and 223 | ;;; variables. 224 | 225 | (defparameter *launcher-params* 226 | (list "-fn" *launcher-font* 227 | "-nb" *launcher-bg* 228 | "-nf" *launcher-fg* 229 | "-sb" *launcher-sel-bg* 230 | "-sf" *launcher-sel-fg* 231 | "-l" "0")) 232 | 233 | (when *launcher-on-bottom* 234 | (setf *launcher-params* 235 | (cons "-b" *launcher-params*))) 236 | 237 | ;; Use rofi flag for emulating dmenu, and also 238 | ;; remove the one-line flags 239 | (when *launcher-rofi-emulate-dmenu* 240 | (setf *launcher-params* 241 | (cons "-dmenu" 242 | (butlast (butlast *launcher-params*))))) 243 | 244 | 245 | ;;; ------------------------------------------------------------------------ ;;; 246 | ;;; External command macros and procedures ;;; 247 | ;;; ------------------------------------------------------------------------ ;;; 248 | 249 | (defun pl-build-command (command-parts &optional (query nil)) 250 | (concatenate 'string 251 | (car command-parts) 252 | query 253 | (apply #'concatenate 'string (cdr command-parts)))) 254 | 255 | (defun pl-send-notification (&rest text) 256 | #+SBCL 257 | (sb-ext:run-program *notify-command* text)) 258 | 259 | (defmacro pl-spawn-process (command input-params yields-input) 260 | #+SBCL 261 | `(sb-ext:run-program ,command ,input-params :wait nil 262 | ,@(when yields-input 263 | '(:input :stream :output :stream)))) 264 | 265 | (defmacro pl-with-spawned-process ((command input-params yields-input) &body body) 266 | #+SBCL 267 | `(let* ((process (pl-spawn-process ,command ,input-params ,yields-input)) 268 | (process-input (sb-ext:process-input process)) 269 | (process-output (sb-ext:process-output process))) 270 | ,@body)) 271 | 272 | (defun pl-request-input (prompt options) 273 | (pl-with-spawned-process 274 | (*launcher-command* (append (list "-p" prompt) *launcher-params*) t) 275 | ;; Apply input options 276 | (let ((input-options (mapcar (lambda (x) (string-downcase (format nil "~a~%" x))) 277 | options))) 278 | (format process-input "~a" (apply #'concatenate 'string input-options)) 279 | (finish-output process-input) 280 | (close process-input)) 281 | ;; Await process; dump what we read 282 | #+SBCL 283 | (sb-ext:process-wait process) 284 | (when (listen process-output) 285 | (read-line process-output)))) 286 | 287 | (defun pl-call-external (program-path &rest arguments) 288 | (when (and (stringp program-path)) 289 | (pl-spawn-process program-path arguments nil))) 290 | 291 | 292 | ;;; ------------------------------------------------------------------------ ;;; 293 | ;;; Input processing ;;; 294 | ;;; ------------------------------------------------------------------------ ;;; 295 | 296 | (defun pl-atom-convert (output-string) 297 | (intern (string-trim " " (string-upcase output-string)))) 298 | 299 | (defun pl-match-output (output-string options) 300 | (let ((input-atom (pl-atom-convert output-string))) 301 | (values input-atom 302 | (cdr (assoc input-atom options)) 303 | output-string))) 304 | 305 | (defmacro pl-with-search-query-format (query-format &body body) 306 | `(let ((query-begin (caadr ,query-format)) 307 | (query-rest (cdadr ,query-format))) 308 | ,@body)) 309 | 310 | (defmacro pl-build-query-string (query-begin query query-rest) 311 | `(concatenate 'string ,query-begin ,query 312 | (apply #'concatenate 'string ,query-rest))) 313 | 314 | (defun pl-build-search-query (query engine) 315 | (let ((engine-query-format (assoc engine *search-engines*))) 316 | (if (null engine-query-format) 317 | (pl-send-notification "Powerlisp: Error" 318 | (format nil "No search engine \"~a\"" engine)) 319 | (pl-with-search-query-format engine-query-format 320 | (pl-build-query-string query-begin 321 | query 322 | query-rest))))) 323 | 324 | (defun pl-options-to-list (options) 325 | (loop for opt in options collect (car opt))) 326 | 327 | (defun pl-request-command (command-atom) 328 | (let ((function (cdr (assoc command-atom *subcommands*)))) 329 | (when (functionp function) (funcall function)))) 330 | 331 | 332 | ;;; ======================================================================== ;;; 333 | ;;; Common Tools ;;; 334 | ;;; ======================================================================== ;;; 335 | 336 | ;;; Below are defined common tools, such as main menus and whatnot, for a 337 | ;;; default Powerlisp configuration. 338 | 339 | ;;; ------------------------------------------------------------------------ ;;; 340 | ;;; Default bindings ;;; 341 | ;;; ------------------------------------------------------------------------ ;;; 342 | 343 | 344 | (powerlisp-add-multi-commands 345 | (list (cons 'favorites ;; List of favorite websites 346 | (lambda () 347 | (with-powerlisp-options-menu ("Website?" *favorite-websites*) 348 | (powerlisp-notify (format nil "Accessing ~a..." option)) 349 | (powerlisp-call-browser assoc-value)))) 350 | (cons 'search-engine ;; Changes default search engine 351 | (lambda () 352 | (with-powerlisp-options-menu ("Search engine?" *search-engines*) 353 | (setf *default-search-engine* option 354 | *launcher-prompt* (format nil "~a+~a" 355 | *launcher-prompt* option))) 356 | (setf *rerun-main-menu* t))) 357 | (cons 'goto 358 | (lambda () 359 | (with-powerlisp-user-input ((format nil "url~a?" 360 | (if *incognito-mode* 361 | " [incognito]" 362 | ""))) 363 | (powerlisp-notify (format nil "Accessing \"~a\"..." input)) 364 | (powerlisp-call-browser input)))) 365 | (cons 'incognito ;; Toggles on/off incognito browsing, if supported 366 | (lambda () 367 | (setf *rerun-main-menu* t 368 | *incognito-mode* (not *incognito-mode*)))))) 369 | 370 | 371 | 372 | ;;; ------------------------------------------------------------------------ ;;; 373 | ;;; Entry point ;;; 374 | ;;; ------------------------------------------------------------------------ ;;; 375 | 376 | (defun run-powerlisp () 377 | (loop 378 | do (setf *rerun-main-menu* nil) 379 | (with-powerlisp-menu ((format nil "~a~a >> " 380 | *launcher-prompt* 381 | (if *incognito-mode* " [incognito]" "")) 382 | *subcommands*) 383 | (if (functionp assoc-value) 384 | ;; Check for listing under commands 385 | (pl-request-command option) 386 | ;; On the other hand, it may be that it is not a command. 387 | (progn (powerlisp-notify (format nil "Searching for ~a..." option)) 388 | (powerlisp-call-browser 389 | (pl-build-search-query raw-input 390 | *default-search-engine*))))) 391 | while *rerun-main-menu*)) 392 | 393 | (run-powerlisp) 394 | --------------------------------------------------------------------------------