chshersh / iris Goto Github PK
View Code? Open in Web Editor NEW🌈 Haskell CLI Framework supporting Command Line Interface Guidelines
Home Page: https://hackage.haskell.org/package/iris
License: Mozilla Public License 2.0
🌈 Haskell CLI Framework supporting Command Line Interface Guidelines
Home Page: https://hackage.haskell.org/package/iris
License: Mozilla Public License 2.0
optparse-applicative
provides a switch
parser with the following type:
switch :: Mod FlagFields Bool -> Parser Bool
It's not entirely clear what does the Bool
mean: Is it True
when the argument is enabled or when it's default?
I propose to introduce a helper parser in the Iris.Cli.OnOff
module:
data OnOff
= Off
| On
onoff :: Mod FlagFields Bool -> Parser OnOff
Currently, the CliEnvSettings
data type is defined in the following way:
Lines 53 to 71 in 0b9c50d
And defaultCliEnvSettings
:
Lines 78 to 86 in 0b9c50d
The idea of using ()
in the type signature was to rely on type-changing record updates. Unfortunately, the following code doesn't compile:
cmdP :: Parser Cmd
cmdP = ...
appSettings :: Iris.CliEnvSettings Cmd ()
appSettings = Iris.defaultCliEnvSettings
{ Iris.cliEnvSettingsCmdParser = cmdP
}
It fails with the error:
... location ...
• Couldn't match type ‘()’ with ‘Cmd’
Expected: CliEnvSettings Cmd ()
Actual: CliEnvSettings () ()
• In the expression:
Iris.defaultCliEnvSettings {cliEnvSettingsCmdParser = cmdP}
In an equation for ‘appSettings’:
appSettings
= Iris.defaultCliEnvSettings {cliEnvSettingsCmdParser = cmdP}
|
26 | appSettings = Iris.defaultCliEnvSettings
| ^^^^^^^^^^^^^^^^^^^^^^^^^^...
... location ...
• Couldn't match type ‘Cmd’ with ‘()’
Expected: Options.Applicative.Types.Parser ()
Actual: Options.Applicative.Types.Parser Cmd
• In the ‘cliEnvSettingsCmdParser’ field of a record
In the expression:
Iris.defaultCliEnvSettings {cliEnvSettingsCmdParser = cmdP}
In an equation for ‘appSettings’:
appSettings
= Iris.defaultCliEnvSettings {cliEnvSettingsCmdParser = cmdP}
|
27 | { cliEnvSettingsCmdParser = cmdP
| ^^^^
The problem here because the cliEnvSettingsRequiredTools
is also parametrised by cmd
. So to change the type, you need to update both fields like this:
appSettings :: Iris.CliEnvSettings Cmd ()
appSettings = Iris.defaultCliEnvSettings
{ Iris.cliEnvSettingsCmdParser = cmdP
, Iris.cliEnvSettingsRequiredTools = []
}
Which is a shame. You need to set an extra field you don't care about all the time because you want to change the type of the CLI command.
It would be great to improve this interface. However, I don't have ideas at the moment. This requires some thinking 🤔
It's extremely useful for diagnostic purposes to understand where a particular setting came from:
Prior art:
trial
package provides a data structure that allows tracking the source of config and printing this info to terminalWe would like to have this feature in Iris. This ticket is mostly about gathering ideas and designing the implementation.
Any ideas are welcome!
Copy the one from tool-sync
:
MonadUnliftIO
is essential for writing concurrent applications that use custom monads. Since CliApp
is essentially a ReaderT
, it should be trivial to derive MonadUnliftIO
instance.
To do this:
unliftio-core
to dependencies.MonadUnliftIO
To support GHC 9.0.2, one need to:
base
dependencies in iris.cabal
tested-with
fieldRefer to the following blog post for guidance around CI:
Currently, the checkTool
function has the following type:
checkTool :: cmd -> Tool cmd -> IO ToolCheckResult
where ToolCheckResult
is defined as follows:
data ToolCheckResult
= ToolNotFound Text
| ToolWrongVersion Text
| ToolOk
A value of ToolCheckResult
is thrown as an exception here:
Lines 138 to 141 in 6be65d4
This is suboptimal because the result can never have the ToolOk
constructors but we're forcing downstream users to handle this case.
So we want to split success and error cases in the ToolCheckResult
type.
To make API more consistent. Let's use Text
everywhere for now.
Blocked by:
To support GHC 9.2.3, one need to:
base
dependencies in iris.cabal
tested-with
fieldRefer to the following blog post for guidance around CI:
Similar to functions in the same module but don't output line break.
Lots of functions are missing the documentation. It would be great to add more!
This issue is an excellent way to start contributing to Iris 🤗
See a function that doesn't have a proper explanation? Feel free to add it and it would be especially great if you can add a doctest
example! 👏🏻
You can document as many functions as you want in a single PR, or you can open multiple PRs. This issue is a call for contributors 😊
The following documentation changes are especially appreciated:
doctest
-like usage examplesThe requirements for the design:
I have a sketch of a potential design, we'll get to it once I have time.
Currently, it runs only on CI. But it would be more convenient to run it locally as a pre-commit hook as well.
To enforce that they're actually used.
Add a field in the CLI environment, telling if the terminal is interactive.
Also add the --no-input
option to disable all interactive actions, e.g. the one from this issue:
In the --version
option, it's often desired to also provide the git commit hash version. The difficulty here is that Haskell packages can be installed from Hackage, not directly from Git, so applications need to somehow embed git metadata.
Current idea is to provide utlitlies for the application to acquire git version in the release stage but not sure about the best way to do this.
Any ideas on how to provide such an utility are appreciated!
This should be as simple as adding windows-latest
and macos-latest
here:
Line 18 in 6be65d4
I don't expect any new errors so this should be pretty easy and smooth 🙏🏻
The issue is about creating a type for asking and answering simple interactive questions.
Iris.Interactive.Question
.data YesNo = No | Yes
y
, Yes
, NO
, etc. with the following type:
parseYesNo :: Text -> Maybe YesNo
yesno
:: (MonadIO m, MonadReader (CliEnv cmd appEnv))
=> Text -- ^ Question
-> YesNo -- ^ Default answer when --no-input is provided
-> m YesNo -- ^ Resulting answer
Test.Iris.Interactive.Question
Usage example:
app :: App ()
app = do
answer <- Iris.yesno "Would you like to proceed?" Iris.Yes
case answer of
Yes -> proceed
No -> Iris.outLn "Aborting"
Currently, when running pre-commit
hooks, they report multiple problems. It would be nice to fix them all!
✗ pre-commit run -a
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing src/Iris/Cli.hs
Fixing iris.cabal
Fixing src/Iris/Cli/Internal.hs
Fixing src/Iris/Cli/Interactive.hs
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing cabal.project
Fixing .github/CODEOWNERS
Fixing src/Iris/Cli/ParserInfo.hs
Fixing src/Iris/Cli/Internal.hs
Fixing test/Test/Iris/Cli.hs
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
It seems that this line is unfinished:
ℹ️ NOTE: PRs are merged to the main branch using the "Squash and merge" button. You can produce granular commit history to make the review easier or if it's your
Currently, we have instructions to build project with cabal
. But as per #42, we now support stack
as well. So it would be great to have build instructions for both builds tools in README.
Similar to #56
People shouldn't specify options that conflict with ours. But if they still do, it would be nice to know whether their parsing conflicts with ours. So I propose to add two tests and document their results in public API:
my-program --no-input
when the custom parser specifies --no-input
as well (ideally this should be checked by #13)my-program --no-input my-command --no-input
is confusing but ideally should be parsed and return different results for different --no-input
. If not, having a test is still good and documenting this behaviour is nice.Stan is a Haskell static analyser. To get the most of Haskell DX, it would be nice to configure it as well.
Implement functions to run Shell commands easier in the Iris applications. A simple implementation would be something like this (inspired by shellmet
):
-- | Run the command but don't print it
shellSilent
:: FilePath -- ^ Executable name
-> [Text] -- ^ Arguments
-> IO ()
-- | Run the command and print the command itself to stderr
shell
:: Text -- ^ Prompt
-> FilePath -- ^ Executable name
-> [Text] -- ^ Arguments
-> IO ()
-- | Run the command, don't print it and return its stdout
shellRetSilent
:: FilePath -- ^ Executable name
-> [Text] -- ^ Arguments
-> IO Text
-- | Run the command, print it with prompt to stderr and return its stdout
shellRet
:: Text -- ^ Prompt
-> FilePath -- ^ Executable name
-> [Text] -- ^ Arguments
-> IO Text
Alternatively, we can integrate with one of the existing and battle-tested libraries:
Possible usage inside Iris itself — Iris.Browse
module:
Lines 69 to 74 in 7e69433
Open questions:
MonadReader CliEnv
to use something specific for running shell commands?CLI guidelines suggestion the following convention for named arguments:
--help
, --version
, --numeric-version
, etc.Is it possible to check such things with optparse-applicative
? 🤔
Iris.need
is the main function for checking whether a specified tool executable in PATH:
need :: MonadIO m => [Tool] -> m ()
It supposed to be used like this in the main application code:
app :: App ()
app = Iris.asksCliEnv Iris.cliEnvCmd >>= \case
Download url -> do
need ["curl"]
runDownload url
Evaluate hs -> do
need ["ghc", "cabal"]
runEvaluate hs
Where the Tool
type is the following:
Lines 43 to 71 in c1597ee
It does the job but it's a bit suboptimal and maybe inconvenient at times.
I'd love to improve this API in the following way:
With such an eDSL, it should be possible to write the following code:
ghc :: Iris.Tool
ghc = "ghc" `Iris.versionAtLeast` [8, 10, 7] `Iris.usingVersionFlag` "--numeric-version"
cabal :: Iris.Tool
cabal = "cabal" `Iris.versionAtLeast` [3, 6, 2, 0] `Iris.usingVersionFlag` "--numeric-version"
app :: App ()
app = Iris.asksCliEnv Iris.cliEnvCmd >>= \case
Download url -> do
need ["curl"]
runDownload url
Evaluate hs -> do
need [ghc, cabal]
runEvaluate hs
The checkToolFunction
should patched accordingly.
To have a best-in-class dev environment, it would be great to configure HLint for Iris and add a CI job for this.
Specifically:
hlint
usage and requirementsCurrently, Iris checks stdout
and stderr
handles to decide whether they support colouring or not. And that's all. This issue is about improving the detection of colouring via CLI options.
Following CLI Guidelines, the new logic for detecting colouring should be as follows:
NO_COLOR
/ NO_COLOUR
environment variable is set<MY_APP_NAME>_NO_COLOR
/ <MY_APP_NAME>_NO_COLOUR
environment variable is setTERM
environment variable is set to dumb
--no-color
/ --no-colour
/ --disable-color
/ --disable-colour
options is provided
--no-color
with the description. Other options shouldn't be displayed. Use internal for other options.Additionally, Iris should provide the --color
option (with the --colour
option as well being hidden via internal
) with the following values:
auto
(default): detects colouring automatically by checking handles, environment variables and disabling CLI optionsnever
: disables colouring
Colour disabling options should behave exactly as
--colour=never
always
: always prints colours; has higher priority than any other optionIris.Cli.Colour
always
/ auto
/ never
optionsIris.Colour.Detect
with the implementation of the colouring detection logic described in the beginning of this issuestdout
and stderr
. In other words, env variables and CLI disable / enable options apply to both stdout
and stderr
. The only different part is whether the specific handle supports colouring or notWe would like to have tutorial-like examples in Iris, showcasing project features.
The proposed structure of the example:
app
directory to examples
executable
stanza in the iris.cabal
file.playground
playground
) should be written using markdown-unlit. The structure of an example:
README.md
with the example contentMain.lhs
as a symbolic link to the READMEPossible examples:
stderr
, lines numbers to stderr
and with lines to stdout
(to mimic the output of ripgrep
)This should be enough for the start but later we can add more examples 🙂
Currently, only for whitespace cleanups. Later, things like hlint
and fourmolu
will be added.
Examples can be found here:
It would be nice to test that the --help
option by default outputs what we want. And it's also nice to see what changes when we change default parsers.
So I propose to add a test that parses the --help
option and compares it with the expected output of this option.
The plan is:
Test.Iris.Cli
where the new test will livecmdParserInfo
from Iris.Env
to Iris.Cli
--help
option using execParserPure from optparse-applicative
Similar to #56
Add unit tests for the --version
and --numeric-version
options parsing.
It would be nice to configure an automatic code formatter, close to the existing style, and a CI check.
I'd like to choose fourmolu
because it allows configuration and is still actively developed.
To be specific, the task includes:
.stylish-haskell
config and replacing it with the fourmolu
configCurrently, it's possible to disable interactivity by specifying the --no-input
options. But some terminals are non-interactive by nature (e.g. on CI). So it would be great to check this and specify the value of InteractiveMode
automatically.
Blocked by #38
stack
is a common Haskell build tool. Some developers still prefer stack
and use only stack
for their Haskell development. Adding Stack support would enable contributors who use only stack
and allow Iris to be added to Stackage later,
To add stack
, one need to:
stack.yaml
using the latest nightly resolver (so Iris can be added to Stackage later).stack
. Refer to Dead Simple Cross Platform GitHub Actions for Haskell blog post.When using the writeFile
function, the program silently overrides the file content. In this case, you may lose your previous content if you're careful enough.
I propose to create a new module Iris.IO
and add a protected writeFile
function that:
data ActionFlag = Ask | Force
Ask
is given, it checks for file existence and asks a question, whether user wants to override file content.@since x.x.x.x
annotationsA generalization for #9 .
The choices can be modeled as their display name coupled with a parsing function to the desired data type of the answer:
data MultipleChoiceOption a = MultipleChoiceOption
{ displayName :: ByteString
, parsingFun :: ByteString -> Maybe a
}
Then, we can model multiple choices as: a message prompt, a non-empty list of choices (whose default choice will always be the head), and a value for the default option:
data MultipleChoicePrompt a = MultipleChoicePrompt
{ promptMessage :: ByteString
, promptOptions :: NonEmpty (MultipleChoiceOption a)
, defaultValue :: a
}
Thus, the following prompt data:
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
]
, defaultValue=Yes
}
Will represent:
Are you sure? [y]/n
Finally, we can define a function that:
MultipleChoicePrompt
foldMap
over the Alt
monoid)stderr
in case of error, returning Nothing
multipleChoiceQuestion
:: MonadIO m
=> MultipleChoicePrompt a
-> m (Maybe a)
An example run would be:
>>> multipleChoiceQuestion
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
, MultipleChoiceOption
{ displayName="cancel"
, parsingFun = \s -> if s == "cancel" then Just Cancel else Nothing
}
]
, defaultValue=Yes
}
>>> n
Just No
Something to notice is that the function will take the default value and build a parser to accept empty responses:
>>> multipleChoiceQuestion
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
, MultipleChoiceOption
{ displayName="cancel"
, parsingFun = \s -> if s == "cancel" then Just Cancel else Nothing
}
]
, defaultValue=Yes
}
>>>
Just Yes
Finally, some points that might need refinement:
stderr
and returning Nothing
, it may be better to return Either Bytestring a
and let another function decide what to do.Non-Empty
List, we could have either a tuple with the first two options and a normal List
for the rest, or a sized container (i.e, something from Data.Size). Having a multiple option question with just one option feels wrong.I'm willing to try and accommodate all suggestions to this implementation :)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.