From 8ee89fe11771f4763147c809aefefe1d5085e07c Mon Sep 17 00:00:00 2001 From: Marty Sluijtman Date: Wed, 8 Mar 2023 20:20:21 +0100 Subject: [PATCH] XMonad prompt article --- content/rambles/xmonad-prompts.md | 157 +++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 26 deletions(-) diff --git a/content/rambles/xmonad-prompts.md b/content/rambles/xmonad-prompts.md index 37af1d4..59f9a1b 100644 --- a/content/rambles/xmonad-prompts.md +++ b/content/rambles/xmonad-prompts.md @@ -16,6 +16,10 @@ standard XMonad Contrib modules (`XMonad.Prompt.Shell`, `XMonad.Prompt.Ssh` and it came to my universal/external Qutebrowser bookmarks menu and `yt-dlp`-and-`pipe-viewer` wrapper. +This tutorial of sorts with assume _some_ Haskell knowledge or not being afraid +of diving straight into how Haskell works. I'm not going into great detail on +how everything works here. + # Bookmarks menu The first one I decided to tackle was the bookmarks menu, as it is by far the @@ -129,17 +133,15 @@ Lets see if there is anything in the 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 ()` - ---- +> ```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` @@ -170,25 +172,128 @@ 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. - ---- +> ```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 +bookmarksFile = ".config/qutebrowser/bookmarks/urls" :: String +``` +> I didn't know where to put this, but I created a string to hold the path to my +> bookmarks + +So it takes an instance of `XPConfig` -- that will again be our `c` argument, +and a list of strings. Here is where we feed it the contents of our file using +our `fileContentList` function. We will do this by binding the output to, say +`bl` for "bookmark list" with `<-`. Since `fileContentList` is a member of the +`IO` monad and we're working in, we have to call it using the `io` function, +which is an alias for the `liftIO` function. ```haskell -bookmarkPrompt c = do - bl <- io fileContentList - mkXPrompt Bookmark c (mkComplFunFromList' c bl) +bookmarkPrompt :: XPConfig -> (String -> X ()) -> X () +bookmarkPrompt c f = do + bl <- io fileContentList bookmarksFile + mkXPrompt Bookmark c (mkComplFunFromList' c bl) f +``` + +You'll see that I've also added argument `f`, this is the function we're going +to use to actually do something with our prompt output. Considering we're +working with bookmarks, opening them in a browser would make sense. + +```haskell +openBookmark :: String -> X () +openBookmark bookmark = do + browser <- io getBrowser + spawn $ browser ++ " '" ++ getUrl bookmark ++ "'" + where getUrl = head . words +``` +`openBookmark` is a function that takes a string and returns something in the +context of the `X` monad (hence the name "XMonad", it's a monad that interacts +with Xorg). Lets go through it line by line. + +```haskell + browser <- io getBrowser +``` +First we get user's browser using the `getBrowser` function from the +`XMonad.Prompt.Shell` module and bind that to `browser`. + +This function checks the `$BROWSER` environment variable and if it isn't set, it +defaults to "firefox". + +```haskell + spawn $ browser ++ " '" ++ getUrl bookmark ++ "'" +``` +Since `getBrowser` returns a string, we can append things to it and feed that to +`spawn`. In this case, we get the URL portion of the bookmark entry surrounded by +single quotes in case a given bookmark contains any symbols that mess up our +shell. After all, what `spawn` ultimately does is feed a given string to +`/bin/sh` as a command to execute. + +```haskell + where getUrl = head . words +``` +For get `getUrl`, we take the given string, split it into a list of strings +based on space characters, pipe that into head, thus retrieving the first item. + +## Keybinding + +We now have a set of functions that create a prompt populated with our +Qutebrowser bookmarks file (any other list of URLs will also work) which will +open our browser when choosing one. + +Now all we have to do is bind it to a key. Personally I use the +`XMonad.Util.EZConfig` so I have the following in my keybindings: + +```haskell +, ("M-M1-C-b", bookmarkPrompt (myXPConfig {autoComplete = Just 200000}) openBookmark Bookmark) +``` +If you use the default way of defining keybindings you can use something like +the following: + +```haskell +, ((modm .|. controlMask, xK_b), bookmarkPrompt def openBookmark) + +``` +`def` is a reference to the default implementation of `XPConfig`. + +# Everything Together + +Everything put together, your config should have something like the following +added. + +```haskell +data Bookmark = Bookmark + +instance XPrompt Bookmark where + showXPrompt Bookmark = "Bookmark: " + +bookmarksFile = ".config/qutebrowser/bookmarks/urls" :: String + +fileContentList :: FilePath -> IO [String] +fileContentList f = do + homeDir <- getEnv "HOME" + file <- readFile (homeDir ++ "/" ++ f) + return . uniqSort . lines $ file + +bookmarkPrompt :: XPConfig -> (String -> X ()) -> X () +bookmarkPrompt c f = do + bl <- io fileContentList bookmarksFile + mkXPrompt Bookmark c (mkComplFunFromList' c bl) f + +openBookmark :: String -> X () +openBookmark bookmark = do + browser <- io getBrowser + spawn $ browser ++ " '" ++ getUrl bookmark ++ "'" + where getUrl = head . words + +-- ... keybindings +, ((modm .|. controlMask, xK_b), bookmarkPrompt def openBookmark) +-- more keybindings ... ```