diff --git a/content/rambles/xmonad-prompts.md b/content/rambles/xmonad-prompts.md new file mode 100644 index 0000000..37af1d4 --- /dev/null +++ b/content/rambles/xmonad-prompts.md @@ -0,0 +1,194 @@ +--- +title: "XMonad Promtps" +date: "2023-03-08T14:20:11+01:00" +author: "$HUMANOID" +tags: ["haskell", "xmonad", "linux"] +description: "There aren't a lot of instructions or explanatiosn on creating XMonad prompts, or at least not within a minute of checking my searx instance." +toc: true +--- +# Introduction + +XMonad has it's own prompt system. Some time ago, I wanted to see if it could +replace dmenu entirely. I managed it for the more common usages I had for it. My +application launcher, `ssh` prompt and pass interface were easy to replace using +standard XMonad Contrib modules (`XMonad.Prompt.Shell`, `XMonad.Prompt.Ssh` and +`XMonad.Prompt.Pass` respectively). However, things became more difficult when +it came to my universal/external Qutebrowser bookmarks menu and +`yt-dlp`-and-`pipe-viewer` wrapper. + +# Bookmarks menu + +The first one I decided to tackle was the bookmarks menu, as it is by far the +simplest of the two. + +Let's take a look at the original: + +```sh +#!/bin/sh +bookmarks="$HOME/.config/qutebrowser/bookmarks/urls" +choice="$(awk '{print$1}' $bookmarks | sort | dmenu -p "Bookmark:" -l 30)" +[ -z $choice ] || qutebrowser "$choice" +``` + +Things get interesting at the declaration of the `choice` variable: + +1. It takes the contents of Qutebrowser's bookmarks file +2. It sorts the results of that +3. Sends that to `dmenu`, prompting the user to make a choice + + +After this, it checks whether `choice` is empty or not and in case it isn't, +opens Qutebrowser with its contents. + +{{< start-details summary="Here is an example of how Qutebrowser saves its bookmarks" >}} +``` +https://www.alpinelinux.org/ index | Alpine Linux +https://www.openbsd.org/ftp.html OpenBSD: Mirrors +https://commonmark.org/ CommonMark +https://xxiivv.com/ Echorridoors +https://100r.co/site/home.html 100R — home +https://solar.lowtechmagazine.com/about.html About this website | LOW←TECH MAGAZINE +``` +{{< end-details >}} + +## Implementation + +Its functionality does boils down to the following: + +1. Parse a given file according to a set of rules, returning it's contents in + the form of a list +2. Allow the user to make a choice from that list +3. Launch an application with that choice as parameter + +Seems easy enough to implement. + +### Parsing the Bookmarks file + +Lets start off by creating a function that can parse our bookmarks file. Here we +need something to read a file -- in this case a bookmars file -- and return its +contents in the form of a list. So lets create a function that takes an arbitrary +file path and reads its contents, returning them as a list of strings. + +```haskell +fileContentList :: FilePath -> IO [String] +``` + +This function takes a filepath and returns `IO [String]`. This is to +accommodate that it has to read a file. + +Now for the rest of the function: + +```haskell +fileContentList :: FilePath -> IO [String] +fileContentList f = do + homeDir <- getEnv "HOME" + file <- readFile (homeDir ++ "/" ++ f) + return . uniqSort . lines $ file +``` + +Lets go over what is happening here. + +`fileContentList` is a function that takes an argument `f`. + +First, it retrieves the current home directory based on the `$HOME` environment +variable and binds it to `homeDir` using the `getEnv` function from the +`System.Environment` module. `getEnv` returns a string with the contents of the +variable given as its argument. + +Next, it retrieves the file contents from `$HOME/path/to/file` using the +`readFile`. This path is created by appending `f` to the `homeDir`. + +Now for the final line. + +First it takes the `file` and splits it up into a list of strings based on `\n` +using the `lines` function. + +```haskell + lines $ file +``` + +Then it pipes the result from that into `uniqSort` from the `XMonad.Prompt` +module in order to -- as the name implies -- sort it and get rid of any +duplicate items. + +```haskell + uniqSort . lines $ file +``` + +And the output of that is piped into `return`: +```haskell + return . uniqSort . lines $ file +``` + +This function will allows us to parse any given text file. To parse the +Qutebrowser bookmarks file, call it using `.config/qutebrowser/bookmarks/url` + +### Creating a Prompt +Lets see if there is anything in the +[`XMonad.Prompt`](https://hackage.haskell.org/package/xmonad-contrib-0.17.1/docs/XMonad-Prompt.html) +module that looks like it could help us in creating a prompt. + + +--- +```haskell +mkXPrompt :: XPrompt p => p -> XPConfig -> ComplFunction -> (String -> X ()) -> X () +``` +Creates a prompt given: +- a prompt type, instance of the `XPrompt` class. +- a prompt configuration (`def` can be used as a starting point) +- a completion function (`mkComplFunFromList` can be used to create a completions function given a list of possible completions) +- an action to be run: the action must take a string and return `X ()` + +--- + +This looks like it could serve as the basis for our prompt. The description and +type signature tell us that it is going to require an instance of the `XPrompt` +typeclass. So lets create a `Bookmark` datatype and implement the `showXPrompt` +function from `XPrompt` in order to give it a default message when executed and +thereby having it derive from `XPrompt`. + +```haskell +data Bookmark = Bookmark + +instance XPrompt Bookmark where + showXPrompt Bookmark = "Bookmark: " +``` +As its second argument, `mkXPrompt` requires an instance of `XPConfig`. The +`XPConfig` typeclass is where you -- as the name implies -- specify the +configuration of XMonad's prompts. Knowing this we can start to write function that +uses `mkXPrompt`: + +```haskell +bookmarkPrompt c = do + mkXPrompt Bookmark c +``` +`c` is our `XPConfig` argument. + +This takes care of the `XPrompt p => p -> XPConfig` portion of the function. + +Now for the completion function, that will handle the list given to our prompt. +Lets mostly follow the suggestion in the description of `mkXPrompt` and lets +take a look at: + +--- +```haskell +mkComplFunFromList' :: XPConfig -> [String] -> String -> IO [String] +``` +This function takes a list of possible completions and returns a completions +function to be used with mkXPrompt. If the string is null it will return all +completions. + +--- + +This is how Qutebrowser and `dmenu` act by default with a given list of possible +options. + +So it takes an instance of `XPConfig` -- that will be our `c` argument, and a +list of strings. Here is where we feed it the contents of our file using our +`fileContentList` function. + +```haskell +bookmarkPrompt c = do + bl <- io fileContentList + mkXPrompt Bookmark c (mkComplFunFromList' c bl) +``` diff --git a/themes/vugo b/themes/vugo index 78d0d2d..9b04750 160000 --- a/themes/vugo +++ b/themes/vugo @@ -1 +1 @@ -Subproject commit 78d0d2d803187c7c26f36310e2692c4d7491ef7a +Subproject commit 9b04750cf5d8a891ee9f1e2b94b410ec3e947ab5