-- TODO Edit issues from command line -- -- I would like to edit issues from the command line. -- -- `anissue edit @id` should bring up the issue inside `$EDITOR`, updating the issue with any changes made within `$EDITOR`. -- -- Comment markers should be stripped, and the file format should be Markdown. Issue markers could be stripped. -- -- I am personally anticipating that this is used most frequently in response to `anissue show @id`. So, maybe `anissue show @id --edit` should be a synonym? -- Rationale: the latter is typing ` --edit`. The former, after show, is typing `^wcwedit`, the former obviously being more palatable. -- TODO Tag improvements (OR-filtering) -- -- Currently it is not possible to filter for an issue satisfying one filter or another. We could add the following syntax allowing it: -- -- `--filter '(@assigned aforemny) OR (@due 2023-10-4)'` -- @topic tags -- TODO Tag improvements (globbing) -- -- I would like to filter `--filter '@assigned *@posteo.de'`. -- @topic tags -- TODO Tag improvements (priorities) -- -- I would like anissue to support priorities when filtering. Let's for a first implementation say that priorities are represented by an integer-values `@priority` tag on an issue. The `--filter` can be extended for filtering integer-valued tags, and the following syntax: -- -- - `--filter '@priority >1'`, `--filter '@priority <1` -- - `--filter '@priority >=1'`, `--filter '@priority <=1` -- -- At some later point, we can configure `high`, `medium`, `low` to mean -- `1,2,3`, `4,5,6`, `7,8,9` respectively. -- -- Issues having a `@priority` tag whose value is not an integer should be regarded not matching the filter. -- @topic tags -- TODO Tag improvements (dates) -- -- I would like anissue to support due dates when filtering. Let's for a first implementation add the following filter syntax: -- -- `--filter '@due 2023-10-04'` for all issues that are marked `@due 2023-10-04` or with an earlier `@due` date. -- -- Issues having a `@due` tag whose value does not follow that date format precisely should be regarded not matching the filter. -- @topic tags -- TODO Add support for ammendments -- -- The user can ammend more information to an issue which is located at -- a different place by referencing the issue's id. Example: -- -- ```bash -- #!/usr/bin/env bash -- -- set -efu -- -- ls -al -- # TODO Original issue -- # -- # @id original-issue -- -- ls -- # @original-issue more information on the issue -- -- edited -- ``` -- TODO Add options to show command to display source code around the issue -- -- The `show` command should have the command line arguments -- `--after/-A`, `--before/-B` and `--context/-C` which take an integer -- n and change the output to contain n lines after, before or around -- the issue. -- -- @topic show -- @topic options -- -- COMMENT Do we expect the code BEFORE an issue to be ever relevant? -- -- COMMENT Hm, this would make it possible for users to use anissue in -- that way at least. I guess most people would assume a comment to go -- before (or for a short comment at the end) the relecant line(s), so -- maybe an `--after/-A` option would suffice. Not sure what's best -- here to be honest. =) -- TODO Expose all command line options as environment variables -- -- It should be possible to provide a default for any command line -- argument via an environment variable. The name of the variable -- should be all upper case and prefixed with `ANISSUE_`, e.g. the -- default value for the command line option `--internal-tags` should be -- settable via `ANISSUE_INTERNAL_TAGS`. -- -- @topic options -- @topic settings -- -- COMMENT Can/ should this be made a feature of optparse-applicative, which we -- could fork? Like, can this work transparently if we fork -- optparse-applicative? -- -- COMMENT Hm, this would only apply to options and flags, but not sub -- commands, I guess. I would say, we could first start by just -- manually adding these env variables and see if we run into -- options/flags, where a env variable does not make sense. But then, -- this would be an optional feature in the library, as well, i guess? -- :D -- TODO Add a subcommand which appends the generated ids to the issue in the sourcecode -- -- Given the following issue -- -- ``` -- # TODO Some title -- # -- # Some description -- ``` -- -- After running `anissue lint`, the issue will be changed to -- -- ``` -- # TODO Some title -- # -- # Some description -- # -- # @id some-title -- ``` -- -- @topic ids -- -- COMMENT Can you elaborate on why this would be useful? I am expecting us to -- do some renaming detection regarding provenance. After that, will it be -- useful still? -- -- COMMENT I think, I mostly find this useful for ammendments, as I then -- neither would have to think about what the generated id was, nor -- would I have to come up with an id. And if we would decide for -- randomized (but easily typeable) generated ids, this would be useful, -- I believe. -- FIXME Exclude tags in code blocks -- -- Tags which appear in markdown code blocks should not be considered -- tags. -- -- @topic tags -- TODO Only separate generated tags with a blank line when description does not end with tags -- -- An issue like -- -- ``` -- # TODO Some title -- # -- # @id some-title -- ``` -- -- Should be rendered as -- -- ``` -- # TODO Some title -- -- @id some-title -- @file the-file.sh -- @row 42 -- ``` -- -- @topic tags -- QUESTION Should automatically generated ids be random? -- -- Right now default id of an issue is the slugification of it's title. -- Once we have a command to automatically append this id to the issue -- within the source code, users might feel tempted to change the id -- after they have adjusted the title. This then would break the -- provenance of the issue. If the generated id was something like -- `tooth-cherry-switch`, it would be just a random string and noone -- would feel the need to change it. -- -- I imagine a workflow where I add issues in the source code. And -- before commiting, I run `anissue lint` to generate ids for all new -- issues. -- -- @topic ids -- -- COMMENT I am not sure we want to automatically append `@id`s to the issue -- description. We probably want to append `@pastId` to an issue if we notice a -- change in the title. I feel users are not as likely change that. -- -- That being said, I am up for experimenting with random IDs *alongside* the -- current design. Maybe we could add a `@uuid` tag automatically that is -- generated from the issues first `@id`, and append that? -- -- COMMENT I think I like the idea of adding an id, once we see a change in the -- title. I'm not sure though, when in the workflow this would happen. -- I think the id generation topic needs some syncronous discussion. :D -- TODO Dependencies between issues -- -- The user should be able to specify that an issue is blocked by -- a different issue by adding `@blocker id-of-blocking-issue`. -- -- When running `anissue list --show-dependencies`, the output should -- render a list of trees deduced from these blocker-relations. E.g. -- -- ``` -- Issue A -- ├ Issue A1 (blocked by A) -- │ ├ Issue A11 (blocked by A1) -- ├─┴ Issue A3A11 (blocked by A and A1) -- └ Issue A2B1 (blocked by A and B) -- Issue B -- └ Issue A2B1 (blocked by A and B) -- Issue C -- ``` -- -- The items should be ordered with the most blocking issues at the top. -- -- @topic dependencies -- TODO Add fulltext search -- -- Additional to `--filter` it should be possible to search for issues -- using `--search 'some query'` using a search-engine like full text -- search. -- -- To make this fast, we could use a package like -- and make sure -- to persist the index in a local cache. -- -- @topic search -- TODO Display issue type in list and show views -- -- Depending on the type of issue (TODO, FIXME, ...) there should be -- either a prefix in the list view (e.g. 🏗️, 🐞, ...), or the list -- should be grouped by the type. -- -- COMMENT I imagine emojis might be off-putting to some potential users. I -- would like a configuration option to disable emojis and display the marker -- instead. -- -- I am fine with having emojis by default. However, seeing that markers are -- extendable, maybe we should make it an opt-in configuration in the first -- place? -- -- COMMENT Yes, thinking about this again, putting the written marker -- instead, might be better. So sth like. -- -- ``` -- FIXME Issue A -- FIXME Issue B -- TODO Story A -- QUESTION Question A -- FIXME Issue C -- ``` -- -- We could still additionally color code them. -- TODO Add option to group issue list -- -- When running `anissue list` with the `--group-by` option, the output -- will be grouped. E.g. running `anissue list --group-by topic` would -- produce -- -- ``` -- Issue D -- Issue E -- -- @topic tags (2 issues) -- Issue A -- Issue B @topic ids -- -- @topic ids (2 issues) -- Issue B @topic tags -- Issue C -- ``` -- -- Other possible grouping options could be -- -- type -- : the issue type, i.e. `TODO`, `FIXME`, `QUESTION`, etc. -- -- priority -- : the priority, groups should be sorted with the highest priority -- first -- -- author -- : the person which created the issue -- -- editor -- : the person who updated the issue last -- -- @topic options -- -- COMMENT For consistency, I would like the option to use `--group-by @topic`. -- Arbitrary tag grouping seems sufficient for this, assuming we add the -- respective *internal tags*. -- -- COMMENT Sounds good! -- TODO Support issue comments -- Currently, comments do not get picked up in issue descriptions; see the -- issue below. -- -- I would like to parse comments (much like issues), and associate them with -- the preceding issue. -- -- `anissue show` should then show all comments below the current output. -- TODO Add options to specify ordering of issues -- -- When running `anissue list` with the `--order-by` option, the list -- should be ordered accordingly. Possible values could be -- -- creation -- : the time when the issue was created -- -- update -- : the time when the issue was updated last -- -- title -- : lexicographically by title -- -- priority -- : by priority -- -- Additionally using `--reversed`, the order will be reversed. -- -- @topic options -- -- COMMENT `anissue list` now supports `--sort` which allows sorting by tags. -- To facilitate the first use-case, sorting by creation, an internal tag -- `@createdAt` has been added. Reversing is possible, ie. `--sort -- '-@createdAt'` -- -- Supposing we want to go this direction, I feel like all of the use-cases can -- be implemented using internal tags and adding support for a couple of data -- types within String-valued tags. (Sorting by `@createdAt` currently only -- works because date-based ordering coincides with string-based ordering on -- the used `YYYY-MM-DD` format.) -- -- What do you think? -- -- PS. Nothing prevents us later to extend functionality by special cases, -- should we dislike an internal `@title` tag. -- -- COMMENT I like this approach! :) -- TODO Add command for (re)generating the cache -- -- When running `anissue cache generate`, we will only generated the -- issue cache, starting from the initial commit. This will not -- re-generate already existing cache entries. To delete the old cache -- first, one has to add `--clean`. -- -- By adding `--first-commit ` the cache will only -- generated starting at the specified commit. -- -- By adding `--full` the cache will be generated for **all** commits in -- the repository. -- -- When running `anissue cache clean`, the local cache will be deleted. -- -- @topic options -- @topic cache -- -- COMMENT What is the point of this command? Can't caches be handled -- transparently to the user? -- -- COMMENT Hm, I think I don't exactly know, what you mean by -- transparently. ':) So cache invalidation would be like how e.g. elm -- does it with the elm-stuff folder? -- -- The idea for the cache generation command could be e.g. used in -- environment setup scripts. And then the first-commit option would -- make it possible to use very old repostories without a too long -- initial delay. -- TODO Add global option for specifying first considered commit -- -- Every command offers the option `--first-commit `, which -- tells anissue to only consider commits stargin with the provided -- commit. The default is the first commit HEAD eventually points to; -- this will also happen when the provided commit is not in the history -- of HEAD. -- -- @topic options -- @topic cache -- TODO Add format option (Text, JSON, ...) -- -- Add an option `--format` to the list and show commands which adjust -- the output format: -- -- text -- : this is the default -- -- json -- : render all information as json so that it can be used, e.g. by -- other tools -- -- @topic rendering -- @topic options -- TODO Add command for listing all topics -- -- Running `anissue topics` should list all topics including the amount -- of issues tagged with them. By default the list should be ordered -- with the topics first which have the most issues. -- TODO Add HTTP server -- -- When running `anissue server`, an HTTP server should be started, -- which serves a JSON-API as well as a HTML-frontend for the current -- directory. -- -- The HTML frontend should display the output of `anissue list` where -- each item links to a separate page containing the infos of `anissue -- show`. -- -- The JSON-Api offers endpoints `GET //` which returns `anissue -- list` as `GET ///` which returns `anissue show`. -- All command line arguments are exposed via query parameters. module Main where import Data.Function ((&)) import Data.List (find) import Data.Maybe (catMaybes) import Data.Text qualified as T import Data.Text.Lazy qualified as LT import Data.Text.Lazy.IO qualified as LT import Data.Time.Clock (UTCTime (utctDay)) import History (listIssues) import Issue (Issue (..)) import Issue qualified as I import Issue.Filter (Filter) import Issue.Filter qualified as I import Issue.Sort (Sort) import Issue.Sort qualified as I import Options.Applicative ((<**>)) import Options.Applicative qualified as O import Prettyprinter ((<+>)) import Prettyprinter qualified as P import Prettyprinter.Render.Terminal qualified as P import Process (proc, sh_, textInput) import System.Console.Terminal.Size qualified as Terminal import System.Exit (ExitCode (ExitFailure), exitWith) import System.Process.Typed qualified as P import Text.Printf import TreeGrepper.Match qualified as G import Prelude hiding (id) data Options = Options { command :: Command, internalTags :: Bool, colorize :: Color } deriving (Show) data Color = Always | Auto | Never deriving (Show, Eq) options :: O.Parser Options options = Options <$> cmd <*> internalTagsFlag <*> colorOption internalTagsFlag :: O.Parser Bool internalTagsFlag = O.switch ( O.long "internal-tags" <> O.help "Whether to display internal tags." ) colorOption :: O.Parser Color colorOption = O.option (O.maybeReader parse) ( O.long "color" <> O.short 'c' <> O.help "Wether to colorize output. (Default: auto)" <> O.value Auto ) where parse "auto" = pure Auto parse "always" = pure Always parse "never" = pure Never parse _ = Nothing data Command = List { files :: [String], filters :: [Filter], sort :: [Sort] } | Show { id :: String, width :: Maybe Int } deriving (Show) cmd :: O.Parser Command cmd = O.hsubparser . mconcat $ [ O.command "list" . O.info listCmd $ O.progDesc "List all issues", O.command "show" . O.info showCmd $ O.progDesc "Show details of all issues" ] listCmd :: O.Parser Command listCmd = List <$> filesArg <*> I.filterArg <*> I.sortArg showCmd :: O.Parser Command showCmd = Show <$> idArg <*> widthOption filesArg :: O.Parser [String] filesArg = O.many (O.strArgument (O.metavar "FILE" <> O.action "file")) idArg :: O.Parser String idArg = O.strArgument ( O.metavar "ID" <> O.completer ( O.listIOCompleter $ catMaybes . map I.id <$> listIssues [] [] [] ) ) widthOption :: O.Parser (Maybe Int) widthOption = O.optional ( O.option O.auto ( O.long "width" <> O.short 'w' <> O.metavar "INT" <> O.help "Wheather to insert line breaks after at most that many characters." ) ) die :: String -> IO a die s = do printf "error: %s\n" s exitWith (ExitFailure 1) main :: IO () main = do O.execParser (O.info (options <**> O.helper) O.idm) >>= \case Options {colorize, command = List {sort, filters, files}} -> do issues <- listIssues sort filters files putDoc colorize . P.vsep $ map ( \issue -> let title = P.annotate P.bold $ P.pretty issue.title fileAndRow = keyword "in" <+> value (T.pack issue.file <> ":" <> T.pack (show issue.start.row)) commit = fromProvenanceDef (keyword "via" <+> value (T.pack "HEAD")) $ \I.Provenance {firstCommit} -> keyword "via" <+> value (T.take 7 firstCommit) author = fromProvenance $ \I.Provenance {authorName, authorEmail} -> ( keyword "by" <+> value (authorName <> " <" <> authorEmail <> ">") ) date' = fromProvenance $ \I.Provenance {date} -> keyword "on" <+> value (show (utctDay date)) fromProvenanceDef def = flip (maybe def) issue.provenance fromProvenance = flip fmap issue.provenance keyword :: T.Text -> P.Doc P.AnsiStyle keyword = P.annotate (P.colorDull P.Green) . P.pretty value :: P.Pretty a => a -> P.Doc P.AnsiStyle value = P.annotate (P.color P.Green) . P.pretty in P.nest 4 . P.vsep . catMaybes $ [ Just title, Just fileAndRow, Just commit, author, date' ] ) issues Options {colorize, command = Show {id, width}} -> do issues <- listIssues [] [] [] case find ((==) (Just id) . I.id) issues of Nothing -> die (printf "no issue with id `%s'\n" id) Just issue -> do putDoc colorize $ P.annotate (P.color P.Green) $ P.pretty $ issue.file ++ ":" ++ show issue.start.row ++ ( case issue.provenance of Nothing -> "HEAD" Just provenance -> "\nvia " ++ T.unpack provenance.firstCommit ++ "\nby " ++ T.unpack provenance.authorName ++ " <" ++ T.unpack provenance.authorEmail ++ ">\nat " ++ show provenance.date ) ++ "\n\n" sh_ ( ( case width of Nothing -> "mdcat --local" Just width' -> proc "mdcat --columns % --local" width' ) & P.setStdin ( textInput ( ("# " <> LT.fromStrict issue.title) <> maybe "" (("\n\n" <>) . LT.fromStrict) issue.description ) ) ) putDoc colorize $ P.pretty $ "\n@file " ++ issue.file ++ "\n@row " ++ show issue.start.row ++ "\n" putDoc :: Color -> P.Doc P.AnsiStyle -> IO () putDoc colorize doc = do isTty <- (== 1) <$> c_isatty 1 term <- Terminal.size let s = P.renderLazy $ P.layoutSmart P.defaultLayoutOptions { P.layoutPageWidth = maybe P.Unbounded (\n -> P.AvailablePerLine n 1) (Terminal.width <$> term) } $ ( if colorize == Always || (colorize == Auto && isTty) then (\x -> x) else P.unAnnotate ) $ doc if maybe False (length (LT.lines s) >) (Terminal.height <$> term) then sh_ ( "${PAGER-less}" & P.shell & P.setStdin (textInput s) ) else LT.putStr s foreign import ccall "unistd.h isatty" c_isatty :: Int -> IO Int