-
-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add .desktop prompt launcher #651
Draft
tulth
wants to merge
5
commits into
xmonad:master
Choose a base branch
from
tulth:prompt-.desktop-launcher
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
504167f
add .desktop prompt launcher
tulth 65e1a4e
use directory built in get xdg folders
tulth bf6a128
purge ExceptT as it is not really needed
tulth e36e061
use suggested cleaner exception to string handler
tulth fe53097
use ReadP parser. move parse-related code to DotDesktopParser
tulth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
{-# LANGUAGE TypeApplications #-} | ||
{-# LANGUAGE LambdaCase #-} | ||
module XMonad.Prompt.DotDesktop | ||
( appLaunchPrompt | ||
) where | ||
|
||
import XMonad ( spawn, io, X ) | ||
import XMonad.Prompt ( mkXPrompt, XPConfig(searchPredicate) ) | ||
import XMonad.Prompt.Shell ( Shell(Shell) ) | ||
import XMonad.Prompt.DotDesktopParser ( runDotDesktopParser ) | ||
|
||
import qualified Data.Map as M | ||
import Control.Monad (filterM) | ||
import Control.Monad.Except | ||
( runExceptT, ExceptT (ExceptT), liftEither ) | ||
import Control.Exception ( try, Exception ) | ||
import Data.Functor ( (<&>) ) | ||
import Data.List ( isSuffixOf, dropWhileEnd ) | ||
import Data.Maybe ( listToMaybe ) | ||
import System.Directory (listDirectory, doesDirectoryExist, getXdgDirectory, XdgDirectory (XdgData), XdgDirectoryList (XdgDataDirs), getXdgDirectoryList) | ||
import System.FilePath ((</>)) | ||
|
||
import Data.Char (isSpace) | ||
import Data.Either (rights, lefts) | ||
import XMonad.Prelude (join) | ||
|
||
isDotDesktop :: FilePath -> Bool | ||
isDotDesktop = isSuffixOf ".desktop" | ||
|
||
trimWhitespace :: String -> String | ||
trimWhitespace = dropWhileEnd isSpace . dropWhile isSpace | ||
|
||
cmdFilter :: String -> String -- fixme future do something other than dropping these | ||
cmdFilter ('%':'f':xs) = cmdFilter xs | ||
cmdFilter ('%':'F':xs) = cmdFilter xs | ||
cmdFilter ('%':'u':xs) = cmdFilter xs | ||
cmdFilter ('%':'U':xs) = cmdFilter xs | ||
cmdFilter ('%':'c':xs) = cmdFilter xs | ||
cmdFilter ('%':'k':xs) = cmdFilter xs | ||
cmdFilter ('%':'i':xs) = cmdFilter xs | ||
cmdFilter ('%':'%':xs) = '%' : cmdFilter xs | ||
cmdFilter (x:xs) = x : cmdFilter xs | ||
cmdFilter "" = "" | ||
|
||
convertExceptionToString :: Exception e => IO (Either e a) -> IO (Either String a) | ||
convertExceptionToString = fmap convertExceptionToStringHelper | ||
|
||
convertExceptionToStringHelper :: Exception e => Either e a -> Either String a | ||
convertExceptionToStringHelper = either (Left . convertExceptionToStringHelperHelper) Right | ||
|
||
convertExceptionToStringHelperHelper :: Exception e => e -> String | ||
convertExceptionToStringHelperHelper = show :: Exception e => e -> String | ||
tulth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
doReadFileLBS :: String -> ExceptT String IO String | ||
doReadFileLBS = ExceptT . convertExceptionToString . try @IOError . readFile | ||
|
||
getVal :: String -> String -> M.Map String String -> Either String String | ||
getVal msg k kvmap = maybeToEither msg $ M.lookup k kvmap | ||
|
||
maybeToEither :: b -> Maybe a -> Either b a | ||
maybeToEither _ (Just a) = Right a | ||
maybeToEither b Nothing = Left b | ||
|
||
doParseFile :: String -> ExceptT String IO DotDesktopApp | ||
doParseFile filePath = do | ||
content <- doReadFileLBS filePath | ||
parsed <- liftEither $ runDotDesktopParser content | ||
let kvMaybe = snd <$> listToMaybe (rights parsed) | ||
keyVals <- liftEither $ | ||
maybe | ||
(Left $ "Parse Resulted in no KeyVals in file " ++ filePath) | ||
Right | ||
kvMaybe | ||
let errMsg = "Unable to find Name in file " ++ filePath | ||
nom <- liftEither $ getVal errMsg "Name" keyVals | ||
exc <- liftEither $ getVal errMsg "Exec" keyVals | ||
typ <- liftEither $ getVal errMsg "Type" keyVals | ||
return DotDesktopApp { fileName = filePath | ||
, name = nom | ||
, type_ = typ | ||
, exec = exc | ||
, cmd = (trimWhitespace . cmdFilter) exc | ||
} | ||
|
||
data DotDesktopApp = DotDesktopApp { fileName :: String | ||
, name :: String | ||
, type_ :: String | ||
, exec :: String | ||
, cmd :: String | ||
} deriving Show | ||
|
||
|
||
getAppFolders :: IO [FilePath] | ||
getAppFolders = do | ||
xdgDataHome <- getXdgDirectory XdgData "" | ||
xdgDataDirs <- getXdgDirectoryList XdgDataDirs | ||
let possibleAppDirs = (xdgDataHome : xdgDataDirs) <&> (</> "applications") | ||
filterM doesDirectoryExist possibleAppDirs | ||
|
||
getDirContents :: FilePath -> ExceptT String IO [FilePath] | ||
tulth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
getDirContents dir = do | ||
fn <- ExceptT . convertExceptionToString . try @IOError . listDirectory $ dir | ||
return $ (dir </>) <$> fn | ||
|
||
getDotDesktopApps :: IO [DotDesktopApp] | ||
getDotDesktopApps = do | ||
appFolders <- getAppFolders | ||
contentsPerFolder <- mapM (runExceptT . getDirContents) appFolders | ||
let folderFiles = join $ rights contentsPerFolder | ||
dotDesktopFiles = filter isDotDesktop folderFiles | ||
folderWarnings = join $ lefts contentsPerFolder | ||
mapM_ print folderWarnings | ||
parseResults <- mapM (runExceptT . doParseFile) dotDesktopFiles | ||
let parseErrs = lefts parseResults | ||
dotDesktopApps = rights parseResults | ||
mapM_ print parseErrs | ||
return dotDesktopApps | ||
|
||
appLaunchPrompt :: XPConfig -> X () | ||
appLaunchPrompt cfg = do | ||
cmdNameMap <- io $ getDotDesktopApps <&> map (\el -> (name el, cmd el)) <&> M.fromList | ||
let cmdNameMapKeys = M.keys cmdNameMap | ||
complFunc :: String -> [String] | ||
complFunc s = filter (searchPredicate cfg s) cmdNameMapKeys | ||
-- | ||
complAction :: String -> X () | ||
complAction s = do | ||
spawn $ cmdNameMap M.! s | ||
mkXPrompt Shell cfg (pure . complFunc) complAction |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
{-# LANGUAGE TupleSections #-} | ||
{-# LANGUAGE LambdaCase #-} | ||
module XMonad.Prompt.DotDesktopParser | ||
( runDotDesktopParser | ||
) where | ||
|
||
import Data.Maybe ( catMaybes ) | ||
import Control.Monad ( MonadPlus(..) ) | ||
import Control.Applicative ( Alternative(..) ) | ||
import qualified Data.Map as MAP | ||
|
||
newtype Parser a = Parser { parse :: String -> [(a,String)] } | ||
tulth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
runParser :: Parser a -> String -> Either String a | ||
runParser m s = | ||
case parse m s of | ||
[(res, [])] -> Right res | ||
[(_, b)] -> Left $ "Parser did not consume entire stream. Remaining: " ++ show b -- ++ " " ++ show b | ||
_ -> Left "Parser error." | ||
|
||
item :: Parser Char | ||
item = Parser $ \case | ||
[] -> [] | ||
(c:cs) -> [(c,cs)] | ||
|
||
instance Functor Parser where | ||
fmap f (Parser cs) = Parser (\s -> [(f a, b) | (a, b) <- cs s]) | ||
|
||
instance Applicative Parser where | ||
pure a = Parser (\s -> [(a,s)]) | ||
(Parser cs1) <*> (Parser cs2) = Parser (\s -> [(f a, s2) | (f, s1) <- cs1 s, (a, s2) <- cs2 s1]) | ||
|
||
instance Monad Parser where | ||
p >>= f = Parser $ \s -> concatMap (\(a, s') -> parse (f a) s') $ parse p s | ||
|
||
instance MonadPlus Parser where | ||
mzero = failure | ||
mplus = combine | ||
|
||
instance Alternative Parser where | ||
empty = mzero | ||
(<|>) = option | ||
|
||
combine :: Parser a -> Parser a -> Parser a | ||
combine p q = Parser (\s -> parse p s ++ parse q s) | ||
|
||
failure :: Parser a | ||
failure = Parser (const []) | ||
|
||
option :: Parser a -> Parser a -> Parser a | ||
option p q = Parser $ \s -> | ||
case parse p s of | ||
[] -> parse q s | ||
res -> res | ||
|
||
satisfy :: (Char -> Bool) -> Parser Char | ||
satisfy p = item >>= \c -> | ||
if p c | ||
then return c | ||
else failure | ||
|
||
type Predicate a = a -> Bool | ||
|
||
notP :: Predicate a -> Predicate a | ||
notP = (not .) | ||
|
||
------------------------------------------------------------------------------- | ||
-- Combinators | ||
------------------------------------------------------------------------------- | ||
|
||
oneOf :: String -> Parser Char | ||
oneOf s = satisfy (`elem` s) | ||
|
||
notOneOf :: String -> Parser Char | ||
notOneOf s = satisfy (notP (`elem` s)) | ||
|
||
char :: Char -> Parser Char | ||
char c = satisfy (c ==) | ||
|
||
string :: String -> Parser String | ||
string [] = return [] | ||
string (c:cs) = do { char c; string cs; return (c:cs)} | ||
|
||
token :: Parser a -> Parser a | ||
token p = do { a <- p; spaces ; return a} | ||
|
||
reserved :: String -> Parser String | ||
reserved s = token (string s) | ||
|
||
spaces :: Parser String | ||
spaces = many $ oneOf " \t" | ||
|
||
newline :: Parser Char | ||
newline = char '\n' | ||
|
||
squareBrackets :: Parser a -> Parser a | ||
squareBrackets m = do | ||
reserved "[" | ||
n <- m | ||
reserved "]" | ||
return n | ||
|
||
data IniFile | ||
= DesktopEntrySection String | ||
| KeyValues [(String, String)] | ||
deriving Show | ||
|
||
keyName :: Parser String | ||
keyName = some (notOneOf "=\n \t") | ||
|
||
keyValue :: Parser (String, String) | ||
keyValue = do | ||
key <- keyName | ||
spaces | ||
char '=' | ||
spaces | ||
val <- many (notOneOf "\n") | ||
newline | ||
return (key, val) | ||
|
||
nonSectionLine :: Parser String | ||
nonSectionLine = do | ||
startChar <- notOneOf "[" | ||
otherChar <- many $ notOneOf "\n" | ||
newline | ||
return $ startChar : otherChar | ||
|
||
desktopEntrySectionLine :: Parser (Either String String) | ||
desktopEntrySectionLine = do | ||
sectionName <- squareBrackets (string "Desktop Entry") | ||
newline | ||
return $ Right sectionName | ||
|
||
badSectionLine :: Parser (Either String String) | ||
badSectionLine = do | ||
startChar <- char '[' | ||
otherChar <- many $ notOneOf "\n" | ||
newline | ||
return $ Left $ startChar : otherChar | ||
|
||
emptyLine :: Parser () | ||
emptyLine = do | ||
whitespaceLine <|> commentLine | ||
return () | ||
where whitespaceLine = spaces >> newline | ||
commentLine = spaces | ||
>> char '#' | ||
>> many (notOneOf "\n") | ||
>> newline | ||
|
||
sectionBodyLine :: Parser (Maybe (String, String)) | ||
sectionBodyLine = (Just <$> keyValue) | ||
<|> (Nothing <$ emptyLine) | ||
|
||
|
||
section :: Parser (Either String (String, MAP.Map String String)) | ||
section = do | ||
many nonSectionLine | ||
sectionLabel <- desktopEntrySectionLine <|> badSectionLine | ||
keyValsList <- catMaybes <$> many sectionBodyLine | ||
let keyVals = MAP.fromList keyValsList | ||
return $ (,keyVals) <$> sectionLabel | ||
|
||
dotDesktopParser :: Parser [Either String (String, MAP.Map String String)] | ||
dotDesktopParser = do | ||
sections <- many section | ||
many nonSectionLine | ||
return sections | ||
|
||
runDotDesktopParser :: String -> Either String [Either String (String, MAP.Map String String)] | ||
runDotDesktopParser = runParser dotDesktopParser |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well this doesn't look fun :) I'm not familiar with
.desktop
files at all; what exactly are you doing here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to just execute the .desktop command specified, without arguments.
Per the spec here:
https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
there are many field codes for arguments which I cannot pass directly to the shell to execute, so I just remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to see if using
xdg-open
to run it gives you more options.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand how I would use xdg-open? Could you provide an example?
Just for context, here is an example from
gimp.desktop
:Basically the user could type
image
, fuzzyMatch finds and displaysGNU Image Manipulation Program
, and if it is selected, the Exec isgimp-2.10 %U
. This code filters out the %U and passesgimp-2.10
to the shell to launch.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gtk-launch
orkioclient5 exec
can be used to run a .desktop file: https://askubuntu.com/a/1114798/950919There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gtk-launch
is (well, at least on my distro) installed with thegtk
package itself, so chances are users will definitely have this installed.Will the fact that it goes into
/usr/share/applications
by itself be a problem? Out of the current paths that the code searches (~/.local/share/applications
,/usr/share/applications
, and/usr/local/share/applications
) on the first one seems not to get searched bygtk-launch
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gtk-launch
does look into~/.local/share/applications
here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mh, doesn't work on my end; a
.desktop
file that's only in~/.local/share/applications
but not in any of the other directories will fail to be foundThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's really weird, I looked into the source of gtk/glib and I don't see why it wouldn't look there. Maybe try stracing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh indeed,
strace
tells me that the directory is searched. Upon closer inspection, the.desktop
file contains a non-existant executable; instead of telling me that,gtk-launch
told me that the.desktop
file couldn't be found, however ._.