Skip to content

Commit 1add89e

Browse files
committed
Add "edit" command to edit YAML version of task in $EDITOR
1 parent 0f09c3d commit 1add89e

5 files changed

Lines changed: 124 additions & 37 deletions

File tree

tasklite-core/app/Main.hs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ data Command
7878
| Start [IdText]
7979
| Stop [IdText]
8080
| Duplicate [IdText]
81-
-- | Edit -- Launch editor with YAML version of task
81+
| EditTask IdText -- Launch editor with YAML version of task
8282
-- | Append -- Append words to a task description
8383
-- | Prepend -- Prepend words to a task description
8484
-- | Undo -- Revert last change
@@ -294,6 +294,9 @@ commandParser conf =
294294
<> command "end" (toParserInfo (EndTasks <$> some (strArgument idsVar))
295295
"Mark a task as obsolete")
296296

297+
<> command "edit" (toParserInfo (EditTask <$> strArgument idVar)
298+
"Edit YAML version of task in your $EDITOR")
299+
297300
<> command "trash" (toParserInfo (TrashTasks <$> some (strArgument idsVar))
298301
"Mark a task as deletable")
299302

@@ -829,6 +832,7 @@ executeCLiCommand conf now connection cmd =
829832
DoTasks ids -> doTasks conf connection Nothing ids
830833
DoOneTask id noteWords -> doTasks conf connection noteWords [id]
831834
EndTasks ids -> endTasks conf connection ids
835+
EditTask id -> editTask conf connection id
832836
TrashTasks ids -> trashTasks conf connection ids
833837
DeleteTasks ids -> deleteTasks conf connection ids
834838
RepeatTasks duration ids -> repeatTasks conf connection duration ids

tasklite-core/package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies:
4343
- protolude
4444
- QuickCheck
4545
- quickcheck-instances
46+
- read-editor
4647
- simple-sql-parser
4748
- sqlite-simple
4849
- text

tasklite-core/source/ImportExport.hs

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ import Protolude as P hiding (state)
88

99
import Data.Aeson as Aeson
1010
import Data.Aeson.Types
11-
import qualified Data.ByteString.Lazy as BL
11+
import qualified Data.ByteString.Lazy as BSL
1212
import qualified Data.Csv as Csv
13+
import qualified Data.HashMap.Strict as HM
14+
import Data.Hourglass
1315
import qualified Data.Text as T
1416
import qualified Data.Text.Lazy.Encoding as TL
1517
import Data.Text.Prettyprint.Doc hiding ((<>))
1618
import Data.Text.Prettyprint.Doc.Render.Terminal
17-
import Data.Hourglass
1819
import Data.ULID
20+
import Data.Yaml as Yaml
1921
import Database.Beam
2022
import Database.SQLite.Simple as Sql
2123
import Lib
2224
import System.Directory
2325
import System.FilePath ((</>))
2426
import System.Process
2527
import System.Posix.User (getEffectiveUserName)
28+
import System.ReadEditor (readEditorWith)
2629
import Time.System
2730
import Utils
2831
import Task
@@ -59,6 +62,10 @@ annotationToNote annot@Annotation {entry = entry, description = description} =
5962
}
6063

6164

65+
importUtcFormat :: TimeFormatString
66+
importUtcFormat = (toFormat ("YYYY-MM-DD H:MI:S" :: [Char]))
67+
68+
6269
data ImportTask = ImportTask
6370
{ task :: Task
6471
, notes :: [Note]
@@ -96,7 +103,7 @@ instance FromJSON ImportTask where
96103
let
97104
maybeModified = modified <|> modified_at <|> o_modified_utc
98105
<|> modification_date <|> updated_at
99-
modified_utc = T.pack $ timePrint ISO8601_DateAndTime $
106+
modified_utc = T.pack $ timePrint importUtcFormat $
100107
fromMaybe createdUtc (parseUtc =<< maybeModified)
101108

102109
o_tags <- o .:? "tags"
@@ -111,7 +118,7 @@ instance FromJSON ImportTask where
111118
let
112119
maybeDue = due <|> o_due_utc <|> due_on
113120
due_utc = fmap
114-
(T.pack . (timePrint ISO8601_DateAndTime))
121+
(T.pack . (timePrint importUtcFormat))
115122
(parseUtc =<< maybeDue)
116123

117124
awake' <- o .:? "awake"
@@ -126,7 +133,7 @@ instance FromJSON ImportTask where
126133
<|> sleep' <|> sleep_utc' <|> sleep_until'
127134
<|> wait' <|> wait_until'
128135
awake_utc = fmap
129-
(T.pack . (timePrint ISO8601_DateAndTime))
136+
(T.pack . (timePrint importUtcFormat))
130137
(parseUtc =<< maybeAwake)
131138

132139
ready' <- o .:? "ready"
@@ -135,7 +142,7 @@ instance FromJSON ImportTask where
135142
let
136143
maybeReady = ready' <|> ready_since' <|> ready_utc'
137144
ready_utc = fmap
138-
(T.pack . (timePrint ISO8601_DateAndTime))
145+
(T.pack . (timePrint importUtcFormat))
139146
(parseUtc =<< maybeReady)
140147

141148
review' <- o .:? "review"
@@ -145,7 +152,7 @@ instance FromJSON ImportTask where
145152
let
146153
maybeReview = review' <|> review_at' <|> review_since' <|> review_utc'
147154
review_utc = fmap
148-
(T.pack . (timePrint ISO8601_DateAndTime))
155+
(T.pack . (timePrint importUtcFormat))
149156
(parseUtc =<< maybeReview)
150157

151158
waiting' <- o .:? "waiting"
@@ -154,7 +161,7 @@ instance FromJSON ImportTask where
154161
let
155162
maybewaiting = waiting' <|> waiting_since' <|> waiting_utc'
156163
waiting_utc = fmap
157-
(T.pack . (timePrint ISO8601_DateAndTime))
164+
(T.pack . (timePrint importUtcFormat))
158165
(parseUtc =<< maybewaiting)
159166

160167
closed <- o .:? "closed"
@@ -167,34 +174,34 @@ instance FromJSON ImportTask where
167174
maybeClosed = closed <|> o_closed_utc <|> closed_on
168175
<|> end <|> o_end_utc <|> end_on
169176
closed_utc = fmap
170-
(T.pack . (timePrint ISO8601_DateAndTime))
177+
(T.pack . (timePrint importUtcFormat))
171178
(parseUtc =<< maybeClosed)
172179

173180
group_ulid' <- o .:? "group_ulid"
174181
group_id' <- o .:? "group_id"
175182
let
176183
maybeGroupUlid = group_ulid' <|> group_id'
177184
group_ulid = fmap
178-
(T.pack . (timePrint ISO8601_DateAndTime))
185+
(T.pack . (timePrint importUtcFormat))
179186
(parseUtc =<< maybeGroupUlid)
180187

181188
repetition_duration' <- o .:? "repetition_duration"
182189
repeat_duration' <- o .:? "repeat_duration"
183190
let
184191
maybeRepetition = repetition_duration' <|> repeat_duration'
185192
repetition_duration = fmap
186-
(T.pack . (timePrint ISO8601_DateAndTime))
193+
(T.pack . (timePrint importUtcFormat))
187194
(parseUtc =<< maybeRepetition)
188195

189196
recurrence_duration' <- o .:? "recurrence_duration"
190197
recur_duration' <- o .:? "recur_duration"
191198
let
192199
maybeRecurrence = recurrence_duration' <|> recur_duration'
193200
recurrence_duration = fmap
194-
(T.pack . (timePrint ISO8601_DateAndTime))
201+
(T.pack . (timePrint importUtcFormat))
195202
(parseUtc =<< maybeRecurrence)
196203

197-
o_notes <- optional (o .: "notes") :: Parser (Maybe [Note])
204+
o_notes <- o .:? "notes" :: Parser (Maybe [Note])
198205
annotations <- o .:? "annotations" :: Parser (Maybe [Annotation])
199206
let
200207
notes = case (o_notes, annotations) of
@@ -205,8 +212,9 @@ instance FromJSON ImportTask where
205212
o_user <- o .:? "user"
206213
let user = fromMaybe "" o_user
207214

215+
o_metadata <- o .:? "metadata"
208216
let
209-
metadata = Just $ Object o
217+
metadata = o_metadata <|> (Just $ Object o)
210218
tempTask = Task {ulid = "", ..}
211219

212220
o_ulid <- o .:? "ulid"
@@ -227,30 +235,34 @@ instance FromJSON ImportTask where
227235
pure $ ImportTask finalTask notes tags
228236

229237

238+
insertImportTask :: Connection -> ImportTask -> IO (Doc ann)
239+
insertImportTask connection importTaskRecord = do
240+
effectiveUserName <- getEffectiveUserName
241+
let
242+
taskParsed = task importTaskRecord
243+
theTask = if Task.user taskParsed == ""
244+
then taskParsed { Task.user = T.pack effectiveUserName }
245+
else taskParsed
246+
insertTask connection theTask
247+
insertTags connection (primaryKey theTask) (tags importTaskRecord)
248+
insertNotes connection (primaryKey theTask) (notes importTaskRecord)
249+
pure $
250+
"📥 Imported task" <+> dquotes (pretty $ Task.body theTask)
251+
<+> "with ulid" <+> dquotes (pretty $ Task.ulid theTask)
252+
<+> hardline
253+
254+
230255
importTask :: Config -> IO (Doc AnsiStyle)
231256
importTask conf = do
232257
connection <- setupConnection conf
233-
content <- BL.getContents
258+
content <- BSL.getContents
234259

235260
let
236-
importResult = Aeson.eitherDecode content :: Either [Char] ImportTask
261+
decodeResult = Aeson.eitherDecode content :: Either [Char] ImportTask
237262

238-
case importResult of
263+
case decodeResult of
239264
Left error -> die $ (T.pack error) <> " in task \n" <> show content
240-
Right importTaskRecord -> do
241-
effectiveUserName <- getEffectiveUserName
242-
let
243-
taskParsed = task importTaskRecord
244-
theTask = if Task.user taskParsed == ""
245-
then taskParsed { Task.user = T.pack effectiveUserName }
246-
else taskParsed
247-
insertTags connection (primaryKey theTask) (tags importTaskRecord)
248-
insertNotes connection (primaryKey theTask) (notes importTaskRecord)
249-
insertTask connection theTask
250-
pure $
251-
"📥 Imported task" <+> dquotes (pretty $ Task.body theTask)
252-
<+> "with ulid" <+> dquotes (pretty $ Task.ulid theTask)
253-
<+> hardline
265+
Right importTaskRecord -> insertImportTask connection importTaskRecord
254266

255267

256268
-- TODO: Use Task instead of FullTask to fix broken notes export
@@ -304,3 +316,57 @@ backupDatabase conf = do
304316
<> pretty (
305317
"✅ Backed up database \"" <> (dbName conf)
306318
<> "\" to \"" <> backupFilePath <> "\"")
319+
320+
321+
editTask :: Config -> Connection -> IdText -> IO (Doc AnsiStyle)
322+
editTask conf connection idSubstr = do
323+
execWithTask conf connection idSubstr $ \taskToEdit -> do
324+
let taskYaml = (T.unpack . decodeUtf8 . Yaml.encode) taskToEdit
325+
326+
newContent <- readEditorWith taskYaml
327+
328+
let
329+
newContentBS = encodeUtf8 $ T.pack newContent
330+
331+
parseMetadata :: Value -> Parser Bool
332+
parseMetadata val = case val of
333+
Object obj -> do
334+
let mdataMaybe = HM.lookup "metadata" obj
335+
336+
hasMdata <- pure $ case mdataMaybe of
337+
Just (Object _) -> True
338+
_ -> False
339+
340+
pure hasMdata
341+
_ -> pure False
342+
343+
hasMetadata = parseMaybe parseMetadata
344+
=<< (rightToMaybe $ Yaml.decodeEither' newContentBS :: Maybe Value)
345+
346+
decodeResult :: Either ParseException ImportTask
347+
decodeResult = Yaml.decodeEither' newContentBS
348+
349+
case decodeResult of
350+
Left error -> die $ (show error) <> " in task \n" <> show newContent
351+
Right importTaskRecord -> do
352+
effectiveUserName <- getEffectiveUserName
353+
let
354+
taskParsed = task importTaskRecord
355+
taskFixed = taskParsed
356+
{ Task.user =
357+
if Task.user taskParsed == ""
358+
then T.pack effectiveUserName
359+
else Task.user taskParsed
360+
, Task.metadata =
361+
if hasMetadata == Just True
362+
then Task.metadata taskParsed
363+
else Nothing
364+
}
365+
366+
replaceTask connection taskFixed
367+
insertTags connection (primaryKey taskFixed) (tags importTaskRecord)
368+
insertNotes connection (primaryKey taskFixed) (notes importTaskRecord)
369+
pure $
370+
"✏️ Edited task" <+> dquotes (pretty $ Task.body taskFixed)
371+
<+> "with ulid" <+> dquotes (pretty $ Task.ulid taskFixed)
372+
<+> hardline

tasklite-core/source/Lib.hs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,16 @@ insertTask connection task = do
108108
insertValues [task]
109109

110110

111+
replaceTask :: Connection -> Task -> IO ()
112+
replaceTask connection task = do
113+
runBeamSqlite connection $ runUpdate $
114+
save (_tldbTasks taskLiteDb) task
115+
116+
111117
insertTags :: Connection -> TaskUlid -> [Text] -> IO ()
112118
insertTags connection taskUlid tags = do
113119
taskToTags <- forM tags $ \tag -> do
114-
tagUlid <- fmap (toLower . show) getULID
120+
tagUlid <- formatUlid getULID
115121
pure $ TaskToTag tagUlid taskUlid tag
116122

117123
runBeamSqlite connection $ runInsert $
@@ -272,7 +278,8 @@ setStateAndClosed :: Connection -> TaskUlid -> Maybe TaskState -> IO ()
272278
setStateAndClosed connection taskUlid theTaskState = do
273279
runBeamSqlite connection $ runUpdate $
274280
update (_tldbTasks taskLiteDb)
275-
(\task -> mconcat [ (Task.state task) <-. val_ theTaskState
281+
(\task -> mconcat
282+
[ (Task.state task) <-. val_ theTaskState
276283
, (Task.review_utc task) <-. val_ Nothing
277284
-- closed_utc is set via an SQL trigger
278285
])
@@ -749,7 +756,7 @@ addTag conf connection tag ids = do
749756
prettyId = dquotes (pretty idText)
750757

751758
now <- fmap (pack . timePrint (utcFormat conf)) timeCurrentP
752-
ulid <- fmap (toLower . show) getULID
759+
ulid <- formatUlid getULID
753760

754761
let taskToTag = TaskToTag ulid taskUlid tag
755762

@@ -778,7 +785,7 @@ addNote conf connection noteBody ids = do
778785
prettyId = dquotes (pretty idText)
779786

780787
now <- fmap (pack . timePrint (utcFormat conf)) timeCurrentP
781-
ulid <- fmap (toLower . show) getULID
788+
ulid <- formatUlid getULID
782789

783790
let taskToNote = TaskToNote ulid taskUlid noteBody
784791

tasklite-core/source/Note.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Protolude
44
import Data.Aeson
55
import qualified Data.Csv as Csv
66
import Data.Text as T
7+
import Data.ULID
8+
79

810
data Note = Note
911
{ ulid :: Text
@@ -14,10 +16,17 @@ instance ToJSON Note
1416

1517
instance FromJSON Note where
1618
parseJSON = withObject "note" $ \o -> do
17-
ulid <- o .: "ulid"
19+
o_ulid <- o .:? "ulid"
20+
21+
let
22+
ulidGenerated = (ulidFromInteger . abs . toInteger . hash) o
23+
ulid = T.toLower $ fromMaybe "" (o_ulid <|> Just (show ulidGenerated))
24+
1825
body <- o .: "body"
26+
1927
pure Note{..}
2028

29+
2130
instance Hashable Note
2231

2332
instance Csv.ToField [Note] where

0 commit comments

Comments
 (0)