Skip to content

Commit

Permalink
Add support for external CLI commands
Browse files Browse the repository at this point in the history
  • Loading branch information
ad-si committed Mar 14, 2024
1 parent 19cade7 commit 9cb8fa3
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 16 deletions.
21 changes: 21 additions & 0 deletions docs-source/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,24 @@ and
```sh
tl metadata set kanban-state sprint 01e0k6a1p00002zgzc0845vayw
```


## External Commands

Like Git, TaskLite also supports external commands.
This allows you to easily extend TaskLite's functionality with your own scripts.

For this to work, simply add an executable script (`chmod +x`)
with the prefix `tasklite-` to your `$PATH`

For example, to add a `grin` command which simply prints a smiley:

```sh
$ cat /usr/local/bin/tasklite-grin
#! /usr/bin/env bash

echo '😁' "$@"

$ tasklite grin Hi
😁 Hi
```
72 changes: 56 additions & 16 deletions tasklite-core/app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ import Protolude (
(<&>),
(||),
)
import Protolude qualified as P

import Control.Monad.Catch (catchAll)
import Data.Aeson as Aeson (KeyValue ((.=)), encode, object)
import Data.FileEmbed (embedStringFile, makeRelativeToProject)
import Data.Hourglass (
Expand All @@ -66,9 +68,6 @@ import Data.Text.Lazy.Encoding qualified as TL
import Data.Time.ISO8601.Duration qualified as Iso
import Data.Version (showVersion)
import Data.Yaml (decodeFileEither, prettyPrintParseException)

-- Special module provided by Cabal

import Database.SQLite.Simple (Connection (..))
import Database.SQLite.Simple qualified as SQLite
import GHC.IO.Encoding (setLocaleEncoding, utf8)
Expand Down Expand Up @@ -154,6 +153,7 @@ import Options.Applicative (
fullDesc,
headerDoc,
help,
helpHeader,
helper,
idm,
info,
Expand All @@ -169,6 +169,7 @@ import Options.Applicative (
subparser,
switch,
)
import Options.Applicative.Help.Chunk (Chunk (Chunk), (<<+>>))
import Options.Applicative.Help.Core (parserHelp)
import Paths_tasklite_core (version)
import Prettyprinter (
Expand All @@ -185,10 +186,11 @@ import Prettyprinter (
)
import Prettyprinter.Render.Terminal (
AnsiStyle,
Color (Black, Blue, Cyan, Yellow),
Color (Black, Blue, Cyan, Red, Yellow),
bold,
color,
colorDull,
hPutDoc,
putDoc,
)
import System.Directory (
Expand All @@ -202,6 +204,7 @@ import System.Directory (
listDirectory,
)
import System.FilePath ((</>))
import System.Process (readProcess)
import Time.System (timeCurrentP)

import Config (
Expand Down Expand Up @@ -342,13 +345,15 @@ data Command
| Help
| PrintConfig
| UlidToUtc Text
| ExternalCommand Text (Maybe [Text])
deriving (Show, Eq)


data CliArgs = CliArgs
{ cliCommand :: Command
, runHelpCommand :: Bool
}
deriving (Show, Eq)


nameToAliasList :: [(Text, Text)]
Expand Down Expand Up @@ -944,26 +949,27 @@ commandParser conf =
-- <> command "utc-quarter" -- … last day of the quarter
-- <> command "utc-year" -- … last day of the year
)

-- Catch-all parser for any external "tasklite-???" command
-- Do not show in help
<|> ExternalCommand
<$> strArgument P.mempty
<*> optional (some (strArgument P.mempty))
)

{- FOURMOLU_ENABLE -}


runHelpSwitch :: Parser Bool
runHelpSwitch =
switch
( long "help"
<> short 'h'
<> help "Display current help page"
<> internal
)


cliArgsParser :: Config -> Parser CliArgs
cliArgsParser conf =
CliArgs
<$> commandParser conf
<*> runHelpSwitch
<*> switch
( long "help"
<> short 'h'
<> help "Display current help page"
<> internal
)


parserInfo :: Config -> ParserInfo CliArgs
Expand Down Expand Up @@ -1101,7 +1107,7 @@ executeCLiCommand conf now connection = do

if runHelpCommand cliArgs
then pure $ extendHelp $ parserHelp defaultPrefs $ cliArgsParser conf
else case cliCommand cliArgs of
else case cliArgs.cliCommand of
ListAll -> listAll conf now connection
ListHead -> headTasks conf now connection
ListNew -> newTasks conf now connection
Expand Down Expand Up @@ -1191,6 +1197,40 @@ executeCLiCommand conf now connection = do
PrintConfig -> pure $ pretty conf
Alias alias _ -> pure $ aliasWarning alias
UlidToUtc ulid -> pure $ prettyUlid ulid
ExternalCommand cmd argsMb -> do
let
args =
argsMb & P.fromMaybe []

runCmd = do
output <-
readProcess
("tasklite-" <> T.unpack cmd)
(args <&> T.unpack)
""
pure $ pretty output

handleException exception = do
hPutDoc P.stderr $
if not $ exception & show & T.isInfixOf "does not exist"
then pretty (show exception :: Text)
else do
let
theHelp = parserHelp defaultPrefs $ cliArgsParser conf
newHeader =
Chunk
( Just $
annotate (color Red) $
"ERROR: Command \""
<> pretty cmd
<> "\" does not exist"
)
<<+>> helpHeader theHelp
extendHelp theHelp{helpHeader = newHeader}

P.exitFailure

catchAll runCmd handleException


printOutput :: [Char] -> Config -> IO ()
Expand Down
2 changes: 2 additions & 0 deletions tasklite-core/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ executables:
dependencies:
- aeson
- directory
- exceptions
- file-embed
- filepath
- githash
Expand All @@ -99,6 +100,7 @@ executables:
- optparse-applicative
- prettyprinter
- prettyprinter-ansi-terminal
- process
- sqlite-simple
- tasklite-core
- text
Expand Down
2 changes: 2 additions & 0 deletions tasklite-core/tasklite-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ executable tasklite
aeson
, base >=4.7 && <5
, directory
, exceptions
, file-embed
, filepath
, githash
Expand All @@ -128,6 +129,7 @@ executable tasklite
, optparse-applicative
, prettyprinter
, prettyprinter-ansi-terminal
, process
, protolude
, sqlite-simple
, tasklite-core
Expand Down

0 comments on commit 9cb8fa3

Please sign in to comment.