voidcruiser-test.nl/content/rambles/xmonad-prompts.md

195 lines
6.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)
```