Skip to content

Commit 887c145

Browse files
committed
Add custom shortcuts feature for task creation
Enable users to define their own shortcuts via the config file. This implements feature request #78. - Add Shortcut data type to Config.hs with prefix and tag fields - Add shortcuts map to Config for storing custom shortcuts - Modify CLI parser to dynamically add commands from config shortcuts - Update example-config.yaml with shortcut examples - Add tests for shortcut parsing and CLI integration - Add documentation for built-in and custom shortcuts Example config: ```yaml shortcuts: cook: prefix: Cook tag: cook fix: tag: fix ``` With this, `tl cook dinner` adds task "Cook dinner +cook"
1 parent de6ffaf commit 887c145

6 files changed

Lines changed: 241 additions & 3 deletions

File tree

docs-source/cli/usage.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,48 @@ It is also possible to immediately add tags when creating a task:
4040
tl add Improve the TaskLite manual +tasklite +pc
4141
```
4242

43+
### Built-in Shortcuts
44+
45+
TaskLite provides several built-in shortcuts to quickly add tasks with common tags:
46+
47+
- `tl write "Email to John"` → Adds "Write Email to John +write"
48+
- `tl read "Article about Haskell"` → Adds "Read Article about Haskell +read"
49+
- `tl idea "New feature"` → Adds "New feature +idea"
50+
- `tl watch "Haskell tutorial"` → Adds "Watch Haskell tutorial +watch"
51+
- `tl listen "Podcast episode"` → Adds "Listen Podcast episode +listen"
52+
- `tl buy "Groceries"` → Adds "Buy Groceries +buy"
53+
- `tl sell "Old laptop"` → Adds "Sell Old laptop +sell"
54+
- `tl pay "Electric bill"` → Adds "Pay Electric bill +pay"
55+
- `tl ship "Package to Mom"` → Adds "Ship Package to Mom +ship"
56+
57+
### Custom Shortcuts
58+
59+
You can also define your own shortcuts in the config file.
60+
For example, to add shortcuts for `cook`, `call`, and `fix`:
61+
62+
```yaml
63+
shortcuts:
64+
cook:
65+
prefix: Cook
66+
tag: cook
67+
call:
68+
prefix: Call
69+
tag: call
70+
fix:
71+
tag: fix # No prefix, just adds the tag
72+
```
73+
74+
With this configuration:
75+
76+
- `tl cook dinner` → Adds "Cook dinner +cook"
77+
- `tl call Mom` → Adds "Call Mom +call"
78+
- `tl fix login bug` → Adds "login bug +fix"
79+
80+
Each shortcut has the following fields:
81+
82+
- `prefix` (optional): Text to prepend to the task body
83+
- `tag`: Tag to add to the task (without the `+` prefix)
84+
4385
And even to set certain fields:
4486

4587
```shell

tasklite-core/example-config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,24 @@ progressBarWidth: 24
4444
# pre:
4545
# - interpreter: python3
4646
# body: print('Python test')
47+
48+
# Custom shortcuts to quickly add tasks with a tag.
49+
# For example: `tl cook Dinner` will add a task "Cook Dinner +cook"
50+
# shortcuts:
51+
# cook:
52+
# prefix: Cook
53+
# tag: cook
54+
# wash:
55+
# prefix: Wash
56+
# tag: wash
57+
# print:
58+
# prefix: Print
59+
# tag: print
60+
# call:
61+
# prefix: Call
62+
# tag: call
63+
# email:
64+
# prefix: Email
65+
# tag: email
66+
# fix:
67+
# tag: fix

tasklite-core/source/Cli.hs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ import Config (
152152
Config (..),
153153
HookSet (..),
154154
HooksConfig (..),
155+
Shortcut (..),
155156
addHookFilesToConfig,
156157
)
158+
import Data.Map.Strict qualified as Map
157159
import Control.Arrow ((>>>))
158160
import Hooks (executeHooks, formatHookResult)
159161
import ImportExport (
@@ -385,6 +387,8 @@ data Command
385387
| Gui
386388
| UlidToUtc Text
387389
| ExternalCommand Text (Maybe [Text])
390+
| AddCustomShortcut Shortcut [Text]
391+
-- ^ Custom shortcut command from config
388392
deriving (Show, Eq)
389393

390394

@@ -448,6 +452,20 @@ getCommand (alias, commandName) =
448452
(progDesc $ T.unpack $ alias <> "-> " <> commandName)
449453

450454

455+
-- | Create a parser for a custom shortcut from config
456+
getShortcutCommand :: (Text, Shortcut) -> Mod CommandFields Command
457+
getShortcutCommand (cmdName, shortcut) =
458+
command (T.unpack cmdName) $
459+
info
460+
(AddCustomShortcut shortcut <$> some (strArgument
461+
(metavar "BODY" <> help "Body of the task")))
462+
(progDesc $ T.unpack $ description)
463+
where
464+
description = case shortcut.prefix of
465+
Just pfx -> pfx <> " something (custom shortcut)"
466+
Nothing -> "Add task with +" <> shortcut.tag <> " tag (custom shortcut)"
467+
468+
451469
toParserInfo :: Parser a -> Text -> ParserInfo a
452470
toParserInfo parser description =
453471
info parser (fullDesc <> progDesc (T.unpack description))
@@ -472,6 +490,7 @@ idsVar =
472490
-- | Help Sections
473491
basic_sec
474492
, shortcut_sec
493+
, custom_shortcut_sec
475494
, list_sec
476495
, vis_sec
477496
, i_o_sec
@@ -482,6 +501,7 @@ basic_sec
482501
(Text, Text)
483502
basic_sec = ("{{basic_sec}}", "Basic Commands")
484503
shortcut_sec = ("{{shortcut_sec}}", "Shortcuts to Add a Task")
504+
custom_shortcut_sec = ("{{custom_shortcut_sec}}", "Custom Shortcuts")
485505
list_sec = ("{{list_sec}}", "List Commands")
486506
vis_sec = ("{{vis_sec}}", "Visualizations")
487507
i_o_sec = ("{{i_o_sec}}", "I/O Commands")
@@ -730,6 +750,16 @@ commandParser conf =
730750
"Ship an item to someone")
731751
)
732752

753+
-- Custom shortcuts from config (only show section if there are shortcuts)
754+
<|> (if null (Map.toList conf.shortcuts)
755+
then P.empty
756+
else hsubparser
757+
( metavar (T.unpack $ snd custom_shortcut_sec)
758+
<> commandGroup (T.unpack $ fst custom_shortcut_sec)
759+
<> foldMap getShortcutCommand (Map.toList conf.shortcuts)
760+
)
761+
)
762+
733763
<|> hsubparser
734764
( metavar (T.unpack $ snd list_sec)
735765
<> commandGroup (T.unpack $ fst list_sec)
@@ -1158,6 +1188,7 @@ helpReplacements :: Config -> [(Text, Doc AnsiStyle)]
11581188
helpReplacements conf =
11591189
[ basic_sec
11601190
, shortcut_sec
1191+
, custom_shortcut_sec
11611192
, list_sec
11621193
, vis_sec
11631194
, i_o_sec
@@ -1340,6 +1371,14 @@ executeCLiCommand config now connection progName args availableLinesMb = do
13401371
AddSell bodyWords -> addTaskC $ ["Sell"] <> bodyWords <> ["+sell"]
13411372
AddPay bodyWords -> addTaskC $ ["Pay"] <> bodyWords <> ["+pay"]
13421373
AddShip bodyWords -> addTaskC $ ["Ship"] <> bodyWords <> ["+ship"]
1374+
AddCustomShortcut shortcut bodyWords ->
1375+
let
1376+
prefixWords = case shortcut.prefix of
1377+
Just pfx -> [pfx]
1378+
Nothing -> []
1379+
tagWord = "+" <> shortcut.tag
1380+
in
1381+
addTaskC $ prefixWords <> bodyWords <> [tagWord]
13431382
LogTask bodyWords -> logTask conf connection bodyWords
13441383
EnterTask -> enterTask conf connection
13451384
ReadyOn datetime ids -> setReadyUtc conf connection datetime ids

tasklite-core/source/Config.hs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import Protolude (
2929
)
3030
import Protolude qualified as P
3131

32+
import Data.Map.Strict (Map)
33+
import Data.Map.Strict qualified as Map
34+
3235
import Data.Aeson (
3336
FromJSON (parseJSON),
3437
ToJSON (toJSON),
@@ -107,6 +110,26 @@ data HooksConfig = HooksConfig
107110
deriving (Generic, Show)
108111

109112

113+
-- | Custom shortcut for adding tasks with predefined prefix and tag
114+
data Shortcut = Shortcut
115+
{ prefix :: Maybe Text
116+
-- ^ Optional prefix to prepend to task body (e.g., "Cook")
117+
, tag :: Text
118+
-- ^ Tag to add to the task (without the + prefix)
119+
}
120+
deriving (Eq, Generic, Show)
121+
122+
123+
instance ToJSON Shortcut
124+
125+
126+
instance FromJSON Shortcut where
127+
parseJSON = withObject "shortcut" $ \o -> do
128+
prefix <- o .:? "prefix"
129+
tag <- o .:? "tag" .!= ""
130+
pure $ Shortcut{..}
131+
132+
110133
instance ToJSON HooksConfig
111134

112135

@@ -285,6 +308,8 @@ data Config = Config
285308
, maxWidth :: Maybe Int -- Automatically uses terminal width if not set
286309
, progressBarWidth :: Int
287310
, hooks :: HooksConfig
311+
, shortcuts :: Map Text Shortcut
312+
-- ^ Custom shortcuts for adding tasks (e.g., "cook" -> adds "Cook" prefix and "+cook" tag)
288313
, noColor :: Bool
289314
}
290315
deriving (Generic, Show)
@@ -318,6 +343,7 @@ instance FromJSON Config where
318343
progressBarWidth <- o .:? "progressBarWidth"
319344
.!= defaultConfig.progressBarWidth
320345
hooks <- o .:? "hooks" .!= defaultConfig.hooks
346+
shortcuts <- o .:? "shortcuts" .!= defaultConfig.shortcuts
321347
noColor <- o .:? "noColor" .!= defaultConfig.noColor
322348

323349
let maxWidth = maxWidthMb >>=
@@ -423,5 +449,6 @@ defaultConfig =
423449
, modify = emptyHookSet
424450
, exit = emptyHookSet
425451
}
452+
, shortcuts = Map.empty
426453
, noColor = False
427454
}

tasklite-core/test/TypesSpec.hs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
module TypesSpec where
44

5-
import Protolude (Maybe (..), Text, ($), (&), (<&>), (<>))
5+
import Protolude (Either (..), Maybe (..), Text, ($), (&), (<&>), (<>))
66
import Protolude qualified as P
77

88
import Test.Hspec (Spec, describe, it, shouldBe)
99

10-
import Config (defaultConfig)
10+
import Config (Shortcut (..), defaultConfig)
1111
import Data.Text qualified as T
1212
import Data.Yaml qualified
1313
import FullTask (FullTask (body, notes, tags, ulid), emptyFullTask)
@@ -174,3 +174,56 @@ spec = do
174174
<> "\n"
175175

176176
taskYaml `shouldBe` expected
177+
178+
describe "Shortcut" $ do
179+
it "can be parsed from YAML with prefix and tag" $ do
180+
let
181+
shortcutYaml :: P.ByteString
182+
shortcutYaml =
183+
[trimming|
184+
prefix: Cook
185+
tag: cook
186+
|]
187+
188+
expected :: Shortcut
189+
expected =
190+
Shortcut
191+
{ prefix = Just "Cook"
192+
, tag = "cook"
193+
}
194+
195+
Data.Yaml.decodeEither' shortcutYaml `shouldBe` Right expected
196+
197+
it "can be parsed from YAML with tag only" $ do
198+
let
199+
shortcutYaml :: P.ByteString
200+
shortcutYaml =
201+
[trimming|
202+
tag: fix
203+
|]
204+
205+
expected :: Shortcut
206+
expected =
207+
Shortcut
208+
{ prefix = Nothing
209+
, tag = "fix"
210+
}
211+
212+
Data.Yaml.decodeEither' shortcutYaml `shouldBe` Right expected
213+
214+
it "defaults to empty tag when tag is not specified" $ do
215+
let
216+
shortcutYaml :: P.ByteString
217+
shortcutYaml =
218+
[trimming|
219+
prefix: Cook
220+
|]
221+
222+
expected :: Shortcut
223+
expected =
224+
Shortcut
225+
{ prefix = Just "Cook"
226+
, tag = ""
227+
}
228+
229+
Data.Yaml.decodeEither' shortcutYaml `shouldBe` Right expected

tasklite/test/CliSpec.hs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import Cli (
3333
printOutput,
3434
renderIOWithConfig,
3535
)
36-
import Config (Config, Hook (Hook), HookSet (HookSet), defaultConfig)
36+
import Config (Config, Hook (Hook), HookSet (HookSet), Shortcut (Shortcut), defaultConfig)
3737
import Config qualified
38+
import Data.Map.Strict qualified as Map
3839
import System.Directory (
3940
Permissions (executable, readable),
4041
emptyPermissions,
@@ -244,3 +245,58 @@ spec tmpDirPath = do
244245
content <- P.readFile filePath
245246
-- Should contain ANSI escape codes
246247
T.unpack content `shouldContain` "\ESC["
248+
249+
it "includes custom shortcuts in help when configured" $ do
250+
let
251+
testShortcut =
252+
Shortcut
253+
{ Config.prefix = Just "Cook"
254+
, Config.tag = "cook"
255+
}
256+
testConf =
257+
defaultConfig
258+
{ Config.shortcuts = Map.fromList [("cook", testShortcut)]
259+
}
260+
failure :: ParserFailure ParserHelp =
261+
parserFailure
262+
defaultPrefs
263+
(commandParserInfo testConf)
264+
(ShowHelpText P.Nothing)
265+
[]
266+
helpText =
267+
renderFailure failure "xxx" & P.fst
268+
269+
helpText `shouldContain` "cook"
270+
helpText `shouldContain` "custom shortcut"
271+
272+
it "adds task with custom shortcut prefix and tag" $ do
273+
let
274+
testShortcut =
275+
Shortcut
276+
{ Config.prefix = Just "Cook"
277+
, Config.tag = "cook"
278+
}
279+
testConf =
280+
defaultConfig
281+
{ Config.shortcuts = Map.fromList [("cook", testShortcut)]
282+
}
283+
284+
_ <- printOutput "test-app" (Just ["cook", "dinner"]) testConf
285+
286+
() `shouldBe` ()
287+
288+
it "adds task with custom shortcut tag only (no prefix)" $ do
289+
let
290+
testShortcut =
291+
Shortcut
292+
{ Config.prefix = Nothing
293+
, Config.tag = "fix"
294+
}
295+
testConf =
296+
defaultConfig
297+
{ Config.shortcuts = Map.fromList [("fix", testShortcut)]
298+
}
299+
300+
_ <- printOutput "test-app" (Just ["fix", "bug", "in", "login"]) testConf
301+
302+
() `shouldBe` ()

0 commit comments

Comments
 (0)