├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .hlint.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── LICENSE ├── README.md ├── cabal.project ├── docs └── RELEASE_CHECKLIST.md ├── examples └── simple-grep │ ├── Main.lhs │ ├── README.md │ └── simple-grep.cabal ├── fourmolu.yaml ├── hie.yaml ├── images ├── demo-simple-grep.png ├── iris-dark-always.png ├── iris-dark.png └── iris-light.png ├── iris.cabal ├── src ├── Iris.hs └── Iris │ ├── App.hs │ ├── Browse.hs │ ├── Cli.hs │ ├── Cli │ ├── Browse.hs │ ├── Colour.hs │ ├── Interactive.hs │ ├── Internal.hs │ ├── ParserInfo.hs │ └── Version.hs │ ├── Colour.hs │ ├── Colour │ ├── Formatting.hs │ └── Mode.hs │ ├── Env.hs │ ├── Interactive.hs │ ├── Interactive │ └── Question.hs │ ├── Settings.hs │ └── Tool.hs ├── stack.yaml └── test ├── Spec.hs └── Test ├── Iris.hs └── Iris ├── Cli.hs ├── Colour.hs ├── Colour └── Mode.hs ├── Common.hs ├── Interactive.hs ├── Interactive └── Question.hs └── Tool.hs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @chshersh 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | Resolves #PUT_ISSUE_NUMBER_HERE_IF_APPLICABLE 6 | 7 | 10 | 11 | 12 | ### Additional tasks 13 | 14 | - [ ] Documentation for changes provided/changed 15 | - [ ] Tests added 16 | - [ ] Updated CHANGELOG.md 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | prefix: "GA" 9 | include: "scope" 10 | labels: 11 | - "CI" 12 | - "dependencies" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened] 6 | push: 7 | branches: [main] 8 | # schedule: 9 | # # additionally run once per week (At 00:00 on Sunday) to maintain cache 10 | # - cron: '0 0 * * 0' 11 | 12 | jobs: 13 | cabal: 14 | name: ${{ matrix.os }} / ghc ${{ matrix.ghc }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macOS-latest, windows-latest] 19 | cabal: ["3.8"] 20 | ghc: 21 | - "8.10.7" 22 | - "9.0.2" 23 | - "9.2.7" 24 | - "9.4.4" 25 | - "9.6.1" 26 | 27 | exclude: 28 | - os: macOS-latest 29 | ghc: 8.10.7 30 | - os: macOS-latest 31 | ghc: 9.0.2 32 | - os: macOS-latest 33 | ghc: 9.2.7 34 | - os: macOS-latest 35 | ghc: 9.4.4 36 | 37 | - os: windows-latest 38 | ghc: 8.10.7 39 | - os: windows-latest 40 | ghc: 9.0.2 41 | - os: windows-latest 42 | ghc: 9.2.7 43 | - os: windows-latest 44 | ghc: 9.4.4 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | - uses: haskell/actions/setup@v2.3 50 | id: setup-haskell-cabal 51 | name: Setup Haskell 52 | with: 53 | ghc-version: ${{ matrix.ghc }} 54 | cabal-version: ${{ matrix.cabal }} 55 | 56 | - name: Configure 57 | run: | 58 | cabal configure --enable-tests --enable-benchmarks --enable-documentation --test-show-details=direct --write-ghc-environment-files=always 59 | 60 | - name: Freeze 61 | run: | 62 | cabal freeze 63 | 64 | - uses: actions/cache@v3 65 | name: Cache ~/.cabal/store 66 | with: 67 | path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 68 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 69 | 70 | - name: Install dependencies 71 | run: | 72 | cabal build all --only-dependencies 73 | 74 | - name: Build 75 | run: | 76 | cabal build all 77 | 78 | - name: Test 79 | run: | 80 | cabal test all 81 | 82 | - name: Documentation 83 | run: | 84 | cabal haddock 85 | 86 | stack: 87 | name: stack / ghc ${{ matrix.ghc }} 88 | runs-on: ubuntu-latest 89 | strategy: 90 | matrix: 91 | stack: ["2.9.3"] 92 | ghc: ["9.2.7"] 93 | 94 | steps: 95 | - uses: actions/checkout@v3 96 | 97 | - uses: haskell/actions/setup@v2.3 98 | name: Setup Haskell Stack 99 | with: 100 | ghc-version: ${{ matrix.ghc }} 101 | stack-version: ${{ matrix.stack }} 102 | 103 | - uses: actions/cache@v3 104 | name: Cache ~/.stack 105 | with: 106 | path: ~/.stack 107 | key: ${{ runner.os }}-${{ matrix.ghc }}-stack 108 | 109 | - name: Install dependencies 110 | run: | 111 | stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies 112 | 113 | - name: Build 114 | run: | 115 | stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks 116 | 117 | - name: Test 118 | run: | 119 | stack test --system-ghc 120 | 121 | format: 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v2 125 | - uses: fourmolu/fourmolu-action@v6 126 | 127 | hlint: 128 | runs-on: ubuntu-latest 129 | steps: 130 | - uses: actions/checkout@v2 131 | 132 | # See: https://github.com/haskell/actions/issues/128 133 | - name: Fix Ubuntu Runner 134 | run: | 135 | sudo apt-get install -y libncurses5 136 | 137 | - name: Run HLint 138 | env: 139 | HLINT_VERSION: "3.5" 140 | run: | 141 | curl -L https://github.com/ndmitchell/hlint/releases/download/v${HLINT_VERSION}/hlint-${HLINT_VERSION}-x86_64-linux.tar.gz --output hlint.tar.gz 142 | tar -xvf hlint.tar.gz 143 | ./hlint-${HLINT_VERSION}/hlint src/ test/ 144 | 145 | stan: 146 | name: stan 147 | runs-on: ubuntu-latest 148 | env: 149 | CABAL_VERSION: "3.6" 150 | # `stan` Github Release supports this version as of now. If we want to run stan on multiple 151 | # GHC versions, we need to build `stan` from source. 152 | GHC_VERSION: "8.10.1" 153 | STAN_VERSION: "0.0.1.0" 154 | 155 | steps: 156 | - uses: actions/checkout@v3 157 | 158 | - uses: haskell/actions/setup@v2.3 159 | id: setup-haskell-cabal 160 | name: Setup Haskell 161 | with: 162 | ghc-version: ${{ env.GHC_VERSION }} 163 | cabal-version: ${{ env.CABAL_VERSION }} 164 | 165 | - name: Configure 166 | run: | 167 | cabal configure --enable-tests --enable-benchmarks --enable-documentation --test-show-details=direct --write-ghc-environment-files=always 168 | 169 | - name: Freeze 170 | run: | 171 | cabal freeze 172 | 173 | - uses: actions/cache@v3 174 | name: Cache ~/.cabal/store 175 | with: 176 | path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 177 | key: stan-${{ runner.os }}-${{ env.GHC_VERSION }}-${{ hashFiles('cabal.project.freeze') }} 178 | 179 | - name: Install dependencies 180 | run: | 181 | cabal build all --only-dependencies 182 | 183 | - name: Build 184 | run: | 185 | cabal build all 186 | 187 | - name: Download `stan` 188 | run: | 189 | curl --silent -L https://github.com/kowainik/stan/releases/latest/download/stan-$STAN_VERSION-Linux-ghc-$GHC_VERSION --output stan 190 | chmod +x stan 191 | 192 | - name: Run `stan` 193 | run: | 194 | ./stan report 195 | 196 | - name: Upload HTML stan report artifact 197 | uses: actions/upload-artifact@v3 198 | with: 199 | name: stan-report 200 | path: stan.html 201 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Haskell 2 | dist 3 | dist-* 4 | cabal-dev 5 | *.o 6 | *.hi 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dyn_hi 11 | *.prof 12 | *.aux 13 | *.hp 14 | *.eventlog 15 | .virtualenv 16 | .hsenv 17 | .hpc 18 | .cabal-sandbox/ 19 | cabal.sandbox.config 20 | cabal.config 21 | cabal.project.local 22 | .ghc.environment.* 23 | .HTF/ 24 | .hie/ 25 | # Stack 26 | .stack-work/ 27 | stack.yaml.lock 28 | 29 | ### IDE/support 30 | # Vim 31 | [._]*.s[a-v][a-z] 32 | [._]*.sw[a-p] 33 | [._]s[a-v][a-z] 34 | [._]sw[a-p] 35 | *~ 36 | tags 37 | 38 | # IntellijIDEA 39 | .idea/ 40 | .ideaHaskellLib/ 41 | *.iml 42 | 43 | # Atom 44 | .haskell-ghc-mod.json 45 | 46 | # VS 47 | .vscode/ 48 | 49 | # Emacs 50 | *# 51 | .dir-locals.el 52 | TAGS 53 | 54 | # other 55 | .DS_Store 56 | 57 | # stan 58 | stan.html 59 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # HLint configuration file 2 | # https://github.com/ndmitchell/hlint 3 | ########################## 4 | 5 | # This file contains a template configuration file, which is typically 6 | # placed as .hlint.yaml in the root of your project 7 | 8 | 9 | # Warnings currently triggered by your code 10 | - ignore: {name: "Avoid lambda"} 11 | - ignore: {name: "Use >=>"} 12 | 13 | 14 | # Specify additional command line arguments 15 | # 16 | # - arguments: [--color, --cpp-simple, -XQuasiQuotes] 17 | 18 | 19 | # Control which extensions/flags/modules/functions can be used 20 | # 21 | # - extensions: 22 | # - default: false # all extension are banned by default 23 | # - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used 24 | # - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module 25 | # 26 | # - flags: 27 | # - {name: -w, within: []} # -w is allowed nowhere 28 | # 29 | # - modules: 30 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set' 31 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely 32 | # 33 | # - functions: 34 | # - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules 35 | 36 | 37 | # Add custom hints for this project 38 | # 39 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar" 40 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x} 41 | 42 | # The hints are named by the string they display in warning messages. 43 | # For example, if you see a warning starting like 44 | # 45 | # Main.hs:116:51: Warning: Redundant == 46 | # 47 | # You can refer to that hint with `{name: Redundant ==}` (see below). 48 | 49 | # Turn on hints that are off by default 50 | # 51 | # Ban "module X(module X) where", to require a real export list 52 | # - warn: {name: Use explicit module export list} 53 | # 54 | # Replace a $ b $ c with a . b $ c 55 | # - group: {name: dollar, enabled: true} 56 | # 57 | # Generalise map to fmap, ++ to <> 58 | # - group: {name: generalise, enabled: true} 59 | 60 | 61 | # Ignore some builtin hints 62 | # - ignore: {name: Use let} 63 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules 64 | 65 | 66 | # Define some custom infix operators 67 | # - fixity: infixr 3 ~^#^~ 68 | 69 | 70 | # To generate a suitable file for HLint do: 71 | # $ hlint --default > .hlint.yaml 72 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [PVP Versioning][1]. The changelog is also 7 | available [on GitHub][2]. 8 | 9 | ## [Unreleased] 10 | 11 | 12 | 13 | ### Added 14 | [#9](https://github.com/chshersh/iris/issues/9): 15 | Implement Yes/No reading functions: 16 | 17 | * Add `yesno` function for asking a question with can be answered with either Yes or No 18 | * Add `YesNo` type (`Yes` | `No`) 19 | 20 | (by [@martinhelmer]) 21 | 22 | * [#116](https://github.com/chshersh/iris/issues/116): 23 | Supports GHC 9.6.1 24 | (by [@celsobonutti]) 25 | 26 | ## [0.1.0.0] — 2023-03-02 🎂 27 | 28 | ### Added 29 | 30 | - [#22](https://github.com/chshersh/iris/issues/22): 31 | Implement full CLI Guidelines recommendations on colouring: 32 | 33 | * Detect colours of `stdout` and `stderr` automatically 34 | * Always output colours when `--colour` (or `--color`) is specified 35 | * Disable colours when: 36 | * `--no-colour` (or `--no-color`) is specified 37 | * Output handle doesn't support colouring 38 | * `NO_COLOUR` (or `NO_COLOR`) env is set 39 | * `MYAPP_NO_COLOUR` (or `MYAPP_NO_COLOR`) env is set 40 | * `TERM=dumb` env variable is set 41 | 42 | (by [@marcellourbani], [@chshersh]) 43 | - [#58](https://github.com/chshersh/iris/issues/58): 44 | Detect non-interactive terminals automatically 45 | (by [@marcellourbani]) 46 | - [#89](https://github.com/chshersh/iris/issues/89) 47 | Add `putStderrColoured` and `putStdoutColoured` functions for putting string 48 | without outputting the line break 49 | (by [@Dponya]) 50 | - [#84](https://github.com/chshersh/iris/issues/84): 51 | Add support for GHC 9.4 52 | (by [@blackheaven]) 53 | - [#27](https://github.com/chshersh/iris/issues/27): 54 | Add Haddock with usage examples to many modules 55 | (by [@chshersh]) 56 | 57 | ### Changed 58 | 59 | - [#90](https://github.com/chshersh/iris/issues/90): 60 | Use `Text` instead of `ByteString` in `putStdoutColoured(Ln)` and 61 | `putStderrColoured(Ln)` functions 62 | (by [@lillycat332]) 63 | - __Migration guide:__ Use functions from `Data.Text.Encoding` to convert 64 | between `ByteString` and `Text` or change your types to `Text` 65 | - [#67](https://github.com/chshersh/iris/issues/67): 66 | Remove the `cliEnvSettingsRequiredTools` field from the `CliEnvSettings` type. 67 | Also remove the `CliEnvException` type. Now, a new function `need` from 68 | `Iris.Tool` should be used for each individual command instead. 69 | (by [@german1608]) 70 | - __Migration guide 1:__ Use the `need` function from the `Iris.Tool` module instead. 71 | - __Migration guide 2:__ Don't catch `CliEnvException` from `mkCliEnv` anymore. 72 | - [#33](https://github.com/chshersh/iris/issues/33): 73 | Move errors from `ToolCheckResult` into a separate type 74 | (by [@charrsky]) 75 | - __Migration guide:__ Change pattern-matching on `ToolCheckResult` to 76 | handle only one constructor with an error instead of previous two. 77 | 78 | ### Non-UX changes 79 | 80 | - [#16](https://github.com/chshersh/iris/issues/16): 81 | Write complete Iris tutorial with a usage example using Literate Haskell 82 | (by [@Dponya], [@chshersh]) 83 | - [#56](https://github.com/chshersh/iris/issues/56): 84 | Implement tests for the `--help` parser 85 | (by [@CThuleHansen]) 86 | - [#57](https://github.com/chshersh/iris/issues/57): 87 | Add tests for the `--version` and `--numeric-version` flags 88 | (by [@CThuleHansen]) 89 | - [#59](https://github.com/chshersh/iris/issues/59): 90 | Write a test to check if global parsing conflicts with local parsing 91 | (by [@zetkez]) 92 | - [#70](https://github.com/chshersh/iris/issues/70): 93 | Add HLint configuration and CI support 94 | (by [@blackheaven]) 95 | - [#64](https://github.com/chshersh/iris/issues/64): 96 | Add Stan configuration and CI support 97 | (by [@blackheaven]) 98 | - [#69](https://github.com/chshersh/iris/issues/69): 99 | Add pre-commit hooks config 100 | (by [@aleeusgr]) 101 | - [#74](https://github.com/chshersh/iris/issues/74): 102 | Add pull request template 103 | (by [@himanshumalviya15]) 104 | - [#62](https://github.com/chshersh/iris/issues/62): 105 | Use `fourmolu` for code formatting 106 | (by [@chshersh]) 107 | 108 | ## [0.0.0.0] — 2022-08-09 🌇 109 | 110 | Initial release prepared by [@chshersh]. 111 | 112 | ### Added 113 | 114 | - [#34](https://github.com/chshersh/iris/issues/34): 115 | Add the `--no-input` CLI option for disabling interactivity 116 | (by [@charrsky]) 117 | - [#36](https://github.com/chshersh/iris/issues/36): 118 | Support Windows and macOS 119 | (by [@charrsky]) 120 | - [#37](https://github.com/chshersh/iris/issues/37): 121 | Support GHC 9.0.2 122 | (by [@charrsky]) 123 | - [#38](https://github.com/chshersh/iris/issues/38): 124 | Support GHC 9.2.3 and GHC 9.2.4 125 | (by [@charrsky], [@chshersh]) 126 | - [#42](https://github.com/chshersh/iris/issues/42), 127 | [#52](https://github.com/chshersh/iris/issues/52): 128 | Add `stack` support and instructions to build with `stack` 129 | (by [@charrsky], [@chshersh]) 130 | - [#43](https://github.com/chshersh/iris/issues/43): 131 | Add `MonadUnliftIO` instance for the `CliApp` monad 132 | (by [@charrsky]) 133 | 134 | 135 | 136 | [@aleeusgr]: https://github.com/aleeusgr 137 | [@blackheaven]: https://github.com/blackheaven 138 | [@celsobonutti]: https://github.com/celsobonutti 139 | [@charrsky]: https://github.com/charrsky 140 | [@chshersh]: https://github.com/chshersh 141 | [@CThuleHansen]: https://github.com/CThuleHansen 142 | [@Dponya]: https://github.com/Dponya 143 | [@german1608]: https://github.com/german1608 144 | [@himanshumalviya15]: https://github.com/himanshumalviya15 145 | [@lillycat332]: https://github.com/lillycat332 146 | [@marcellourbani]: https://github.com/marcellourbani 147 | [@martinhelmer]: https://github.com/martinhelmer 148 | [@zetkez]: https://github.com/zetkez 149 | 150 | 151 | 152 | [1]: https://pvp.haskell.org 153 | [2]: https://github.com/chshersh/iris/releases 154 | 155 | 156 | 157 | [Unreleased]: https://github.com/chshersh/iris/compare/v0.1.0.0...HEAD 158 | [0.1.0.0]: https://github.com/chshersh/iris/releases/tag/v0.1.0.0 159 | [0.0.0.0]: https://github.com/chshersh/iris/releases/tag/v0.0.0.0 160 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This code of conduct outlines our expectations for all those who 4 | participate in the Iris project development. 5 | 6 | We invite all those who participate in Iris development to help us to 7 | create safe, inclusive and positive experiences. 8 | 9 | ## Our Standards 10 | 11 | The main concern of Iris developers is friendliness and inclusion. 12 | As such, we are committed to providing a friendly, safe and welcoming environment for all. 13 | So, we are using the following standards in our organization. 14 | 15 | ### Be inclusive. 16 | 17 | We welcome and support people of all backgrounds and identities. This includes, 18 | but is not limited to members of any sexual orientation, gender identity and expression, 19 | race, ethnicity, culture, national origin, social and economic class, educational level, 20 | colour, immigration status, sex, age, size, family status, political belief, religion, 21 | and mental and physical ability. 22 | 23 | ### Be respectful. 24 | 25 | We won't all agree all the time but disagreement is no excuse for disrespectful behaviour. 26 | We will all experience frustration from time to time, but we cannot allow that frustration 27 | to become personal attacks. An environment where people feel uncomfortable or threatened 28 | is not a productive or creative one. 29 | 30 | ### Choose your words carefully. 31 | 32 | Always conduct yourself professionally. Be kind to others. Do not insult or put down others. 33 | Harassment and exclusionary behaviour aren't acceptable. This includes, but is not limited to: 34 | 35 | * Threats of violence. 36 | * Discriminatory language. 37 | * Personal insults, especially those using racist or sexist terms. 38 | * Advocating for, or encouraging, any of the above behaviours. 39 | 40 | ### Don't harass. 41 | 42 | In general, if someone asks you to stop something, then stop. When we disagree, try to understand why. 43 | Differences of opinion and disagreements are mostly unavoidable. What is important is that we resolve 44 | disagreements and differing views constructively. 45 | 46 | ### Make differences into strengths. 47 | 48 | Different people have different perspectives on issues, 49 | and that can be valuable for solving problems or generating new ideas. Being unable to understand why 50 | someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that we all make mistakes, 51 | and blaming each other doesn’t get us anywhere. 52 | 53 | Instead, focus on resolving issues and learning from mistakes. 54 | 55 | ## Reporting Guidelines 56 | 57 | If you are subject to or witness unacceptable behaviour, or have any other concerns, 58 | please notify Dmitrii Kovainikov (the current admin of Iris) as soon as possible. 59 | 60 | You can reach them via the following email address: 61 | 62 | * kovanikov@gmail.com 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Iris 2 | 3 | This document describes contributing guidelines for the Iris project. 4 | 5 | You're encouraged to read this document before your first contribution 6 | to Iris. Spending your time familiarising yourself with these 7 | guidelines is much appreciated because following this guide ensures 8 | the most positive outcome for contributors and maintainers! 💖 9 | 10 | ## How to contribute 11 | 12 | Everyone is welcome to contribute as long as they follow our 13 | [rules for polite and respectful communication](https://github.com/chshersh/iris/blob/main/CODE_OF_CONDUCT.md)! 14 | 15 | And you can contribute to Iris in multiple ways: 16 | 17 | 1. Share your success stories or confusion moments when using Iris under 18 | [Discussions](https://github.com/chshersh/iris/discussions). 19 | 2. Open [Issues](https://github.com/chshersh/iris/issues) with bug 20 | reports or feature suggestions. 21 | 3. Open [Pull Requests (PRs)](https://github.com/chshersh/iris/pulls) 22 | with documentation improvements, changes to the code or even 23 | implementation of desired changes! 24 | 25 | If you would like to open a PR, **create the issue first** if it 26 | doesn't exist. Discussing implementation details or various 27 | architecture decisions avoids spending time inefficiently. 28 | 29 | > You may argue that sometimes it's easier to share your vision with 30 | > exact code changes via a PR. Still, it's better to start a 31 | > Discussion or an Issue first by mentioning that you'll open a PR 32 | > later with your thoughts laid out in code. 33 | 34 | If you want to take an existing issue, please, share your intention to 35 | work on something in the comment section under the corresponding 36 | issue. This avoids the situation when multiple people are working on 37 | the same problem concurrently. 38 | 39 | ## Pull Requests requirements 40 | 41 | Generally, the process of submitting, reviewing and accepting PRs 42 | should be as lightweight as possible if you've discussed the 43 | implementation beforehand. However, there're still a few requirements: 44 | 45 | 1. Be polite and respectful in communications. Follow our 46 | [Code of Conduct](https://github.com/chshersh/iris/blob/main/CODE_OF_CONDUCT.md). 47 | 2. The code should be formatted with [fourmolu][fourmolu] 48 | using the [Iris-specific configuration][fourmolu-config]. 49 | Your changes will be rejected if they don't follow the formatting 50 | requirements. 51 | 3. Add a `@since x.x.x.x` annotation to all newly introduced and exported types, 52 | classes, functions, fields and instances. 53 | 4. Add an entry to CHANGELOG.md describing your changes in the format similar to 54 | other changes. 55 | 56 | [fourmolu]: https://hackage.haskell.org/package/fourmolu 57 | [fourmolu-config]: https://github.com/chshersh/iris/blob/main/fourmolu.yaml 58 | 59 | That's all so far! 60 | 61 | > ℹ️ **NOTE:** PRs are merged to the `main` branch using the 62 | > "Squash and merge" button. You can produce granular commit history 63 | > to make the review easier or if it's your preferred workflow. But 64 | > all commits will be squashed when merged to `main`. 65 | 66 | ## Write access to the repository 67 | 68 | If you want to gain write access to the repository, open a 69 | [discussion with the Commit Bits category](https://github.com/chshersh/iris/discussions/categories/commit-bits) 70 | and mention your willingness to have it. 71 | 72 | I ([@chshersh](https://github.com/chshersh)) 73 | grant write access to everyone who contributed to Iris. 74 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [chshersh] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iris 2 | 3 | [![GitHub CI](https://github.com/chshersh/iris/workflows/CI/badge.svg)](https://github.com/chshersh/iris/actions) 4 | [![Hackage](https://img.shields.io/hackage/v/iris.svg?logo=haskell)](https://hackage.haskell.org/package/iris) 5 | [![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE) 6 | 7 | **Iris** is a Haskell framework for building CLI applications that follow 8 | [Command Line Interface Guidelines](https://clig.dev/). 9 | 10 | > 🌈 Iris (/ˈaɪrɪs/) is a Greek goddess associated with communication, messages, 11 | > the rainbow, and new endeavors. 12 | 13 | 14 | 15 | 16 | Iris changing her workflow and hair colour depending on the time of day. 17 | 18 | 19 | > ℹ️ **DISCLAIMER #1:** Currently, Iris is in experimental phase and 20 | > mostly for early adopters. It may lack documentation or have 21 | > significant breaking changes. We appreciate anyone's help in 22 | > improving the documentation! At the same time, the maintainers will 23 | > strive to provide helpful migration guides. 24 | 25 | > ℹ️ **DISCLAIMER #2:** Iris is developed and maintained in free time 26 | > by volunteers. The development may continue for decades or may stop 27 | > tomorrow. You can use 28 | > [GitHub Sponsorship](https://github.com/sponsors/chshersh) to support 29 | > the development of this project. 30 | 31 | ## Goals 32 | 33 | Iris development is guided by the following principles: 34 | 35 | 1. **Support [Command Line Interface Guidelines](https://clig.dev/).** 36 | Features or changes that violate these guidelines are not accepted 37 | in the project. 38 | 2. **Beginner-friendliness.** Haskell beginners should be able to build 39 | CLI applications with Iris. Hence, the implementation of Iris API 40 | that uses less fancy Haskell features are preferred. When the 41 | complexity is justified, the cost of introducing this extra 42 | complexity should be mitigated by having better documentation. 43 | 3. **Reasonable batteries-included.** Iris is not trying to be 44 | minimalistic as possible, it strives to provide out-of-the-box 45 | solutions for most common problems. But at the same time, we don't 46 | want to impose unnecessary heavy dependencies. 47 | 4. **Excellent documentation.** Iris documentation should be as 48 | helpful as possible in using the framework. 49 | 50 | > **NOTE:** Currently, Iris may lack documentation but there's an 51 | > ongoing effort to improve the situation. 52 | 53 | 5. **Single-line access.** Iris is designed for qualified imports, and you 54 | should be able to get all the needed API by writing a single import line: 55 | 56 | ```haskell 57 | import qualified Iris 58 | ``` 59 | 60 | 🧱 Iris focuses solely on CLI applications. If you're interested in 61 | building TUI app with Haskell, check out 62 | [brick](https://hackage.haskell.org/package/brick). 63 | 64 | ## Features 65 | 66 | CLI apps built with Iris offer the following features for end users: 67 | 68 | * Automatic detection of colouring support in the terminal 69 | * Ability to check required external tools if you need e.g. `curl` or 70 | `git` 71 | * Support for standard CLI options out-of-the-box: 72 | * `--help` 73 | * `--version` 74 | * `--numeric-version`: helpful for detecting required tools versions 75 | * `--no-input`: for disabling all interactive features 76 | * `--colour=(auto|never|always)`: to set colour mode 77 | * `--no-colour`: to disable terminal colouring 78 | * Utilities to open files in a browser 79 | 80 | ## Quick Start Guide 81 | 82 | > 📚 Refer to [the complete Iris tutorial][simple-grep] for more details and 83 | > examples of more Iris features. 84 | 85 | [simple-grep]: https://github.com/chshersh/iris/blob/main/examples/simple-grep/README.md 86 | 87 | `iris` is compatible with the following GHC 88 | versions - [supported versions](https://matrix.hackage.haskell.org/#/package/iris) 89 | 90 | In order to start using `iris` in your project, you 91 | will need to set it up with these steps: 92 | 93 | 1. Add the dependency on `iris` in your project's 94 | `.cabal` file. For this, you should modify the `build-depends` 95 | section according to the below section: 96 | 97 | ```haskell 98 | build-depends: 99 | , base ^>= LATEST_SUPPORTED_BASE 100 | , iris ^>= LATEST_VERSION 101 | ``` 102 | 103 | 2. To use this package, refer to the below example. 104 | 105 | ```haskell 106 | {-# LANGUAGE DerivingStrategies #-} 107 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 108 | 109 | module Main (main) where 110 | 111 | import Control.Monad.IO.Class (MonadIO (..)) 112 | 113 | import qualified Iris 114 | 115 | 116 | newtype App a = App 117 | { unApp :: Iris.CliApp () () a 118 | } deriving newtype 119 | ( Functor 120 | , Applicative 121 | , Monad 122 | , MonadIO 123 | ) 124 | 125 | appSettings :: Iris.CliEnvSettings () () 126 | appSettings = Iris.defaultCliEnvSettings 127 | { Iris.cliEnvSettingsHeaderDesc = "Iris usage example" 128 | , Iris.cliEnvSettingsProgDesc = "A simple 'Hello, world!' utility" 129 | } 130 | 131 | app :: App () 132 | app = liftIO $ putStrLn "Hello, world!" 133 | 134 | main :: IO () 135 | main = Iris.runCliApp appSettings $ unApp app 136 | ``` 137 | 138 | ## For contributors 139 | 140 | Check [CONTRIBUTING.md](https://github.com/chshersh/iris/blob/main/CONTRIBUTING.md) 141 | for contributing guidelines. 142 | 143 | ## Development 144 | 145 | To build the project and run the tests locally, you can use either 146 | `cabal` or `stack`. 147 | 148 | > See the [First time](#first-time) section if you don't have Haskell 149 | > development environment locally. 150 | 151 | ### Cabal 152 | 153 | Build the project: 154 | 155 | ```shell 156 | cabal build all 157 | ``` 158 | 159 | Run all unit tests: 160 | 161 | ```shell 162 | cabal test --enable-tests --test-show-details=direct 163 | ``` 164 | 165 | ### Stack 166 | 167 | Build the project: 168 | 169 | ```shell 170 | stack build --test --no-run-tests 171 | ``` 172 | 173 | Run all unit tests: 174 | 175 | ```shell 176 | stack test 177 | ``` 178 | 179 | ### First time 180 | 181 | If this is your first time dealing with Haskell tooling, we recommend 182 | using [GHCup](https://www.haskell.org/ghcup/). 183 | 184 | During the installation, GHCup will suggest you installing all the 185 | necessary tools. If you have GHCup installed but miss some of the 186 | tooling for some reason, type the following commands in the terminal: 187 | 188 | ```shell 189 | ghcup install ghc 9.2.5 190 | ghcup set ghc 9.2.5 191 | ghcup install cabal 3.8.1.0 192 | ``` 193 | 194 | > If you are using Linux or macOS, you may find `ghcup tui` command a 195 | > more attractive option. 196 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | . 3 | examples/simple-grep 4 | -------------------------------------------------------------------------------- /docs/RELEASE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | This document contains list of actions needed to be done before releasing a new 4 | version of Iris. 5 | 6 | - [ ] Go through commit history and update `CHANGELOG.md` 7 | - [ ] Bump up version in `iris.cabal` 8 | - [ ] Update versions of all newly introduced types and functions using the 9 | following command 10 | 11 | ```shell 12 | # Linux 13 | rg "x\.x\.x\.x" --files-with-matches | xargs sed -i 's/x.x.x.x/1.3.2.0/g' 14 | 15 | # macOS 16 | rg "x\.x\.x\.x" --files-with-matches | xargs sed -i '' 's/x.x.x.x/1.3.2.0/g' 17 | ``` 18 | 19 | - [ ] Updated `expectedNumericVersion` `Test.Iris.Cli` to the new one 20 | - [ ] Create a new release on GitHub with the new version 21 | - [ ] Upload dist to Hackage 22 | - [ ] (Optional) Upload documentation to Hackage -------------------------------------------------------------------------------- /examples/simple-grep/Main.lhs: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /examples/simple-grep/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The file contains an introduction tutorial on building a simple CLI tool with 4 | Iris. 5 | 6 | We're going to build a tool for finding lines that contain a given 7 | string in a given file. Like a very simple clone of 8 | [grep](https://www.gnu.org/software/grep/) or more modern 9 | [ripgrep](https://github.com/BurntSushi/ripgrep) 10 | 11 | The tool can be executed using `cabal` as shown in the following example: 12 | 13 | ```shell 14 | cabal run simple-grep -- --file iris.cabal --search iris 15 | ``` 16 | 17 | When run, you can expect to see output similar to the following: 18 | 19 | ![demo-simple-grep](https://raw.githubusercontent.com/chshersh/iris/main/images/demo-simple-grep.png) 20 | 21 | ## Preamble: imports and language extensions 22 | 23 | Our simple example uses the following Haskell packages: 24 | 25 | * `base`: the Haskell standard library 26 | * `pretty-terminal`: a terminal output colouring library 27 | * `iris`: a Haskell CLI framework 28 | * `mtl`: a library with monad transformers 29 | * `text`: a library with the efficient `Text` type 30 | * `optparse-applicative`: a CLI options parser 31 | 32 | Since this is a literate Haskell file, we need to specify all our language 33 | pragmas and imports upfront. 34 | 35 | First, let's opt-in to some Haskell features not enabled by default: 36 | 37 | ```haskell 38 | {-# LANGUAGE ApplicativeDo #-} 39 | {-# LANGUAGE DerivingStrategies #-} 40 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 41 | {-# LANGUAGE OverloadedStrings #-} 42 | {-# LANGUAGE RecordWildCards #-} 43 | {-# LANGUAGE StrictData #-} 44 | ``` 45 | 46 | > We use several extensions for different parts: 47 | > 48 | > * `ApplicativeDo` to write nicer code for CLI with `optparse-applicative` 49 | > * `DerivingStrategies` to be explicit about how we derive typeclasses 50 | > * `GeneralizedNewtypeDeriving` to allow deriving of everything for `newtype`s 51 | > * `OverloadedStrings` to be able to work with `Text` easier 52 | > * `RecordWildCards` for non-verbose records 53 | > * `StrictData` to avoid space leaks 54 | 55 | Then, the `module` header: 56 | 57 | ```haskell 58 | module Main (main) where 59 | ``` 60 | 61 | > Our tool is going to be rather small, so it could be put in a single file 62 | > which needs to export only the single `main` function. 63 | 64 | Now, imports from external libraries: 65 | 66 | ```haskell 67 | import Control.Monad.IO.Class (MonadIO (..)) 68 | import Control.Monad.Reader (MonadReader) 69 | import Data.Foldable (traverse_) 70 | import Data.Text (Text) 71 | 72 | import qualified Data.Text as Text 73 | import qualified Data.Text.IO as Text 74 | import qualified Options.Applicative as Opt 75 | import qualified System.Console.Pretty as Pretty 76 | ``` 77 | 78 | > We're writing a simple grep utility, we need here the `pretty-terminal` library for 79 | > printing colored messages. Other libraries such as `text` are standard for any 80 | > CLI tool. `optparse-applicative` is needed here for defining the set of 81 | > commands that Iris will consume. 82 | 83 | ✨ Iris is designed for `qualified` imports. To get access to the entire API, 84 | write the following single line: 85 | 86 | ```haskell 87 | import qualified Iris 88 | ``` 89 | 90 | Finally, we import an autogenerated `Paths_*` file to get access to our tool 91 | metadata: 92 | 93 | ```haskell 94 | import qualified Paths_simple_grep as Autogen 95 | ``` 96 | 97 | > Read more about `Paths_*` modules in the [Cabal documentation][paths-docs]. 98 | 99 | [paths-docs]: https://cabal.readthedocs.io/en/stable/cabal-package.html?highlight=Paths_*#accessing-data-files-from-package-code 100 | 101 | ## CLI 102 | 103 | First, let's define CLI for `simple-grep`. 104 | 105 | Our tool takes two arguments: 106 | 107 | 1. Path to the file for our search. 108 | 2. A string to search for. 109 | 110 | These options can be represented as a simple record type: 111 | 112 | ```haskell 113 | data Options = Options 114 | { optionsFile :: FilePath 115 | , optionsSearch :: Text 116 | } 117 | ``` 118 | 119 | After we defined the options data type, we can write a CLI parser using 120 | `optparse-applicative`. 121 | 122 | ```haskell 123 | optionsP :: Opt.Parser Options 124 | optionsP = do 125 | optionsFile <- Opt.strOption $ mconcat 126 | [ Opt.long "file" 127 | , Opt.short 'f' 128 | , Opt.metavar "FILE_PATH" 129 | , Opt.help "Path to the file" 130 | ] 131 | 132 | optionsSearch <- Opt.strOption $ mconcat 133 | [ Opt.long "search" 134 | , Opt.short 's' 135 | , Opt.metavar "STRING" 136 | , Opt.help "Substring to find and highlight" 137 | ] 138 | 139 | pure Options{..} 140 | ``` 141 | 142 | > Refer to [the `optparse-applicative` documentation][options-doc] for more details. 143 | 144 | [options-doc]: https://hackage.haskell.org/package/optparse-applicative 145 | 146 | ## The Application Monad 147 | 148 | When using Iris, you're expected to implement your own `App` monad as a wrapper 149 | around `CliApp` from `iris`. 150 | 151 | To do this: 152 | 153 | * Create `newtype App a` 154 | * `CliApp cmd env a` has three type parameters, specialise them to your 155 | application. In `simple-grep`: 156 | 157 | * `cmd` is `Options`: our CLI options record type 158 | * `env` is `()`: we won't need a custom environment in our simple tool 159 | * `a` is `a`: a standard type for value inside the monadic context 160 | 161 | * Derive all the necessary typeclasses 162 | * The important one is `MonadReader` because many functions in Iris are 163 | polymorphic over monad and deriving this typeclass enables reusing them 164 | 165 | In code, it looks like this: 166 | 167 | ```haskell 168 | newtype App a = App 169 | { unApp :: Iris.CliApp Options () a 170 | } deriving newtype 171 | ( Functor 172 | , Applicative 173 | , Monad 174 | , MonadIO 175 | , MonadReader (Iris.CliEnv Options ()) 176 | ) 177 | ``` 178 | 179 | ## Settings 180 | 181 | To run the application, we need to configure it by providing _settings_. Iris 182 | has [the `CliEnvSettings` type][settings-doc] with multiple configuration 183 | options. 184 | 185 | [settings-doc]: https://hackage.haskell.org/package/iris/docs/Iris-Settings.html 186 | 187 | In `simple-grep`, we want to specify the following: 188 | 189 | * Short header description 190 | * Longer program description that appears in the `--help` output 191 | * Version of our tool 192 | * CLI parser for our `Options` type 193 | 194 | The `CliEnvSettings cmd env` type has two type parameters: 195 | 196 | * `cmd`: the CLI command (this is our `Options` type) 197 | * `env`: custom application environment (again, this is just `()` as we don't have custom env) 198 | 199 | In code, it looks like this: 200 | 201 | ```haskell 202 | appSettings :: Iris.CliEnvSettings Options () 203 | appSettings = Iris.defaultCliEnvSettings 204 | { -- short description 205 | Iris.cliEnvSettingsHeaderDesc = "Iris usage example" 206 | 207 | -- longer description 208 | , Iris.cliEnvSettingsProgDesc = "A simple grep utility - tutorial example" 209 | 210 | -- a function to display the tool version 211 | , Iris.cliEnvSettingsVersionSettings = 212 | Just (Iris.defaultVersionSettings Autogen.version) 213 | { Iris.versionSettingsMkDesc = \v -> "Simple grep utility v" <> v 214 | } 215 | 216 | -- our 'Options' CLI parser 217 | , Iris.cliEnvSettingsCmdParser = optionsP 218 | } 219 | ``` 220 | 221 | Our `appSettings` are created with the help of `defaultCliEnvSettings`. This way 222 | you can specify only relevant fields and be forward-compatible in case Iris 223 | introduces new settings options. 224 | 225 | ## Business logic 226 | 227 | Now, as we finished configuring our CLI application, we can finally start 228 | implementing the main logic of searching the content inside the files. 229 | 230 | We need three main parts: 231 | 232 | 1. Read file content. 233 | 2. Search for lines in the file. 234 | 3. Output the result. 235 | 236 | ### Reading the input file 237 | 238 | First, let's write a helper function for reading the content of the file: 239 | 240 | ```haskell 241 | getFileContent :: FilePath -> App Text 242 | getFileContent = liftIO . Text.readFile 243 | ``` 244 | 245 | A few comments: 246 | 247 | * Our `getFileContent` function works in our `App` monad 248 | * The function takes `FilePath` and returns `Text` 249 | * We use `liftIO` to run an action of type `IO Text` as `App Text` 250 | (this is possible because we derived `MonadIO` for `App` earlier) 251 | 252 | ### Searching 253 | 254 | We want to output lines of text that contain our given substring as well as the 255 | line numbers. 256 | 257 | Following the **Imperative Shell, Functional Core** programming pattern, we can 258 | write a pure function that takes an input search term, file content and returns 259 | a list of pairs that contain the line number and line text: 260 | 261 | ```haskell 262 | search :: Text -> Text -> [(Int, Text)] 263 | ``` 264 | 265 | The implementation of this function is straightforward in Functional 266 | Programming: 267 | 268 | ```haskell 269 | search str 270 | = filter (\(_i, line) -> str `Text.isInfixOf` line) 271 | . zip [1 ..] 272 | . Text.lines 273 | ``` 274 | 275 | ### Output 276 | 277 | Now, once we find our lines, we would like to output the result. To make 278 | things more interesting and highlight a few more Iris features, we would like 279 | add a few more requirements to our output: 280 | 281 | 1. Lines of text should be printed to `stdout` while liner numbers should go 282 | `stderr`. 283 | 2. Line numbers should be coloured and bold. 284 | 285 | We're going to use `pretty-terminal` for colouring. Iris provides helper functions 286 | for handling terminal colouring support and printing coloured output. 287 | 288 | Writing this in the code: 289 | 290 | ```haskell 291 | output :: [(Int, Text)] -> App () 292 | output = traverse_ outputLine 293 | where 294 | outputLine :: (Int, Text) -> App () 295 | outputLine (i, line) = do 296 | outputLineNumber i 297 | liftIO $ Text.putStrLn line 298 | 299 | outputLineNumber :: Int -> App () 300 | outputLineNumber i = Iris.putStderrColoured 301 | (Pretty.color Pretty.Yellow . Pretty.style Pretty.Bold) 302 | (Text.pack (show i) <> ": ") 303 | ``` 304 | 305 | ### Putting all together 306 | 307 | After we've implemented relevant parts, we're ready to put everything together. 308 | 309 | Our main steps are: 310 | 311 | 1. Get the parsed CLI arguments. 312 | 2. Read the file. 313 | 3. Search for content. 314 | 4. Output the result. 315 | 316 | For this, we can create the top-level function `app` and put all steps there: 317 | 318 | ```haskell 319 | app :: App () 320 | app = do 321 | -- 1. Get parsed 'Options' from the environment 322 | Options{..} <- Iris.asksCliEnv Iris.cliEnvCmd 323 | 324 | -- 2. Read the file 325 | fileContent <- getFileContent optionsFile 326 | 327 | -- 3. Find all lines with numbers 328 | let searchResult = search optionsSearch fileContent 329 | 330 | -- 4. Output the result 331 | output searchResult 332 | ``` 333 | 334 | The only thing left is to run our `app` function from `main`. 335 | 336 | This can be done by unwrapping `Iris.CliApp` from our `App` and providing 337 | settings to the `runCliApp` function: 338 | 339 | ```haskell 340 | main :: IO () 341 | main = Iris.runCliApp appSettings $ unApp app 342 | ``` 343 | 344 | A few final notes: 345 | 346 | * We've implemented only a parser for `Options` and specified our parser as a 347 | field in `appSettings`. Iris will run the parser on the start and either throw 348 | an exception on parsing errors or parse successfully and provide the result in 349 | `CliEnv` in the `CliEnvApp`. 350 | * Parsed CLI options are stored in the `cliEnvCmd` field of the Iris environment. 351 | * We get this field by calling the `asksCliEnv` function. Since our `App` type 352 | derived `MonadReader` with the proper arguments, we can extract all the 353 | environment fields. 354 | 355 | ## Result 356 | 357 | Our simple tool is finished! Now, we can see its `--help` output to make sure 358 | that all the arguments are valid: 359 | 360 | ```shell 361 | $ cabal run simple-grep -- --help 362 | ``` 363 | 364 | and the output: 365 | 366 | ``` 367 | Iris usage example 368 | 369 | Usage: simple-grep [--version] [--numeric-version] [--no-input] 370 | (-f|--file FILE_PATH) (-s|--search STRING) 371 | [--colour | --no-colour] 372 | 373 | A simple grep utility - tutorial example 374 | 375 | Available options: 376 | -h,--help Show this help text 377 | --version Show application version 378 | --numeric-version Show only numeric application version 379 | --no-input Enter the terminal in non-interactive mode 380 | -f,--file FILE_PATH Path to the file 381 | -s,--search STRING Substring to find and highlight 382 | --colour Enable colours 383 | --no-colour Disable colours 384 | 385 | ``` 386 | 387 | And we can finally run it to see the result: 388 | 389 | ```shell 390 | $ cabal exec simple-grep -- -f iris.cabal -s iris 391 | 2: name: iris 392 | 7: See [README.md](https://github.com/chshersh/iris#iris) for more details. 393 | 8: homepage: https://github.com/chshersh/iris 394 | 9: bug-reports: https://github.com/chshersh/iris/issues 395 | 27: location: https://github.com/chshersh/iris.git 396 | 107: test-suite iris-test 397 | 117: Paths_iris 398 | 121: , iris 399 | ``` 400 | 401 | ## Bonus challenges 402 | 403 | If you wish to hack on this example more, try implementing the following improvements in the tool: 404 | 405 | * Add the `[-i | --ignore-case]` option to support the case-insensitive mode 406 | * Highlight the found part of the string inside the found line 407 | * Search in multiple files inside the directory 408 | -------------------------------------------------------------------------------- /examples/simple-grep/simple-grep.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: simple-grep 3 | version: 0.0.0.0 4 | build-type: Simple 5 | 6 | common common-options 7 | build-depends: base >= 4.14 && < 5 8 | 9 | ghc-options: -Wall 10 | -Wcompat 11 | -Widentities 12 | -Wincomplete-uni-patterns 13 | -Wincomplete-record-updates 14 | -Wredundant-constraints 15 | -Wnoncanonical-monad-instances 16 | if impl(ghc >= 8.2) 17 | ghc-options: -fhide-source-paths 18 | if impl(ghc >= 8.4) 19 | ghc-options: -Wmissing-export-lists 20 | -Wpartial-fields 21 | if impl(ghc >= 8.8) 22 | ghc-options: -Wmissing-deriving-strategies 23 | -fwrite-ide-info 24 | -hiedir=.hie 25 | if impl(ghc >= 8.10) 26 | ghc-options: -Wunused-packages 27 | if impl(ghc >= 9.0) 28 | ghc-options: -Winvalid-haddock 29 | if impl(ghc >= 9.2) 30 | ghc-options: -Wredundant-bang-patterns 31 | -Woperator-whitespace 32 | if impl(ghc >= 9.4) 33 | ghc-options: -Wredundant-strictness-flags 34 | 35 | default-language: Haskell2010 36 | 37 | executable simple-grep 38 | import: common-options 39 | 40 | if os(windows) 41 | buildable: False 42 | 43 | main-is: Main.lhs 44 | autogen-modules: Paths_simple_grep 45 | other-modules: Paths_simple_grep 46 | 47 | build-depends: 48 | , base 49 | , iris 50 | , mtl 51 | , optparse-applicative 52 | , pretty-terminal 53 | , text 54 | 55 | build-tool-depends: markdown-unlit:markdown-unlit 56 | 57 | ghc-options: 58 | -pgmL markdown-unlit 59 | -threaded 60 | -------------------------------------------------------------------------------- /fourmolu.yaml: -------------------------------------------------------------------------------- 1 | indentation: 4 2 | function-arrows: leading 3 | comma-style: leading 4 | import-export-style: diff-friendly 5 | indent-wheres: false 6 | record-brace-space: false 7 | newlines-between-decls: 1 8 | haddock-style: multi-line 9 | haddock-style-module: 10 | let-style: auto 11 | in-style: left-align 12 | respectful: true 13 | fixities: [] 14 | unicode: never -------------------------------------------------------------------------------- /hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | cabal: 3 | - path: "./src" 4 | component: "lib:iris" 5 | 6 | - path: "./test" 7 | component: "iris:test:iris-test" 8 | 9 | - path: "examples/simple-grep/./Main.lhs" 10 | component: "simple-grep:exe:simple-grep" 11 | -------------------------------------------------------------------------------- /images/demo-simple-grep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chshersh/iris/f74e261a5b1c3a423f729e12030b17c9fc1dcbfe/images/demo-simple-grep.png -------------------------------------------------------------------------------- /images/iris-dark-always.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chshersh/iris/f74e261a5b1c3a423f729e12030b17c9fc1dcbfe/images/iris-dark-always.png -------------------------------------------------------------------------------- /images/iris-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chshersh/iris/f74e261a5b1c3a423f729e12030b17c9fc1dcbfe/images/iris-dark.png -------------------------------------------------------------------------------- /images/iris-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chshersh/iris/f74e261a5b1c3a423f729e12030b17c9fc1dcbfe/images/iris-light.png -------------------------------------------------------------------------------- /iris.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: iris 3 | version: 0.1.0.0 4 | synopsis: Haskell CLI framework 5 | description: 6 | Haskell CLI framework. 7 | See [README.md](https://github.com/chshersh/iris#iris) for more details. 8 | homepage: https://github.com/chshersh/iris 9 | bug-reports: https://github.com/chshersh/iris/issues 10 | license: MPL-2.0 11 | license-file: LICENSE 12 | author: Dmitrii Kovanikov 13 | maintainer: Dmitrii Kovanikov 14 | copyright: 2022 Dmitrii Kovanikov 15 | category: CLI,Framework 16 | build-type: Simple 17 | extra-doc-files: README.md 18 | CHANGELOG.md 19 | tested-with: 20 | GHC == 9.6.1 21 | GHC == 9.4.4 22 | GHC == 9.2.7 23 | GHC == 9.0.2 24 | GHC == 8.10.7 25 | 26 | source-repository head 27 | type: git 28 | location: https://github.com/chshersh/iris.git 29 | 30 | common common-options 31 | build-depends: base >= 4.14 && < 5 32 | 33 | ghc-options: -Wall 34 | -Wcompat 35 | -Widentities 36 | -Wincomplete-uni-patterns 37 | -Wincomplete-record-updates 38 | -Wredundant-constraints 39 | -Wnoncanonical-monad-instances 40 | if impl(ghc >= 8.2) 41 | ghc-options: -fhide-source-paths 42 | if impl(ghc >= 8.4) 43 | ghc-options: -Wmissing-export-lists 44 | -Wpartial-fields 45 | if impl(ghc >= 8.8) 46 | ghc-options: -Wmissing-deriving-strategies 47 | -fwrite-ide-info 48 | -hiedir=.hie 49 | if impl(ghc >= 8.10) 50 | ghc-options: -Wunused-packages 51 | if impl(ghc >= 9.0) 52 | ghc-options: -Winvalid-haddock 53 | if impl(ghc >= 9.2) 54 | ghc-options: -Wredundant-bang-patterns 55 | -Woperator-whitespace 56 | if impl(ghc >= 9.4) 57 | ghc-options: -Wredundant-strictness-flags 58 | 59 | default-language: Haskell2010 60 | default-extensions: ConstraintKinds 61 | DeriveAnyClass 62 | DeriveGeneric 63 | DerivingStrategies 64 | GeneralizedNewtypeDeriving 65 | InstanceSigs 66 | KindSignatures 67 | LambdaCase 68 | OverloadedStrings 69 | RecordWildCards 70 | ScopedTypeVariables 71 | StandaloneDeriving 72 | StrictData 73 | TupleSections 74 | TypeApplications 75 | ViewPatterns 76 | 77 | library 78 | import: common-options 79 | hs-source-dirs: src 80 | 81 | exposed-modules: 82 | Iris 83 | Iris.App 84 | Iris.Browse 85 | Iris.Cli 86 | Iris.Cli.Browse 87 | Iris.Cli.Colour 88 | Iris.Cli.Internal 89 | Iris.Cli.Interactive 90 | Iris.Cli.ParserInfo 91 | Iris.Cli.Version 92 | Iris.Colour 93 | Iris.Colour.Formatting 94 | Iris.Colour.Mode 95 | Iris.Env 96 | Iris.Settings 97 | Iris.Tool 98 | Iris.Interactive 99 | Iris.Interactive.Question 100 | 101 | 102 | build-depends: 103 | , ansi-terminal ^>= 0.11 104 | , directory ^>= 1.3 105 | , mtl >= 2.2 && < 2.4 106 | , optparse-applicative ^>= 0.17 107 | , process ^>= 1.6 108 | , text >= 1.2 && < 2.1 109 | , unliftio-core ^>= 0.2 110 | 111 | test-suite iris-test 112 | import: common-options 113 | type: exitcode-stdio-1.0 114 | hs-source-dirs: test 115 | main-is: Spec.hs 116 | 117 | autogen-modules: 118 | Paths_iris 119 | 120 | other-modules: 121 | Paths_iris 122 | Test.Iris 123 | Test.Iris.Common 124 | Test.Iris.Cli 125 | Test.Iris.Colour 126 | Test.Iris.Colour.Mode 127 | Test.Iris.Tool 128 | Test.Iris.Interactive 129 | Test.Iris.Interactive.Question 130 | 131 | build-depends: 132 | , iris 133 | , hspec >= 2.9.7 && < 2.11 134 | , text 135 | , optparse-applicative 136 | 137 | ghc-options: 138 | -threaded 139 | -rtsopts 140 | -with-rtsopts=-N 141 | -------------------------------------------------------------------------------- /src/Iris.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | __Iris__ is a Haskell CLI framework. It contains batteries for bulding 10 | CLI applications in Haskell by following best-practices. 11 | 12 | The library is designed for __qualified__ imports. To use it, import 13 | like this: 14 | 15 | @ 16 | __import qualified__ "Iris" 17 | @ 18 | 19 | To create an CLI application with __Iris__, you need to do the 20 | following steps: 21 | 22 | 1. Create settings for your applications by constructing a value of 23 | type 'CliEnvSettings'. 24 | 2. Define a monad for your application with the help of 'CliApp' by 25 | using either __type__ or __newtype__. 26 | 27 | That's all! Now, you can write your CLI app by having access to all 28 | capabilities provided by __Iris__ 🎉 29 | 30 | For a detailed introduction to __Iris__, refer to the following examples: 31 | 32 | * [Simple Grep Example](https://github.com/chshersh/iris/blob/main/examples/simple-grep/README.md) 33 | 34 | @since 0.0.0.0 35 | -} 36 | module Iris ( 37 | -- $app 38 | module Iris.App, 39 | -- $browse 40 | module Iris.Browse, 41 | -- $cli 42 | module Iris.Cli, 43 | -- $colour 44 | module Iris.Colour, 45 | -- $env 46 | module Iris.Env, 47 | -- $settings 48 | module Iris.Settings, 49 | -- $tool 50 | module Iris.Tool, 51 | module Iris.Interactive, 52 | ) where 53 | 54 | import Iris.App 55 | import Iris.Browse 56 | import Iris.Cli 57 | import Iris.Colour 58 | import Iris.Env 59 | import Iris.Interactive 60 | import Iris.Settings 61 | import Iris.Tool 62 | 63 | {- $app 64 | CLI Application monad. 65 | -} 66 | 67 | {- $browse 68 | Functions to open local files in a browser. 69 | -} 70 | 71 | {- $cli 72 | CLI parsing utilities. 73 | -} 74 | 75 | {- $colour 76 | Functions to detect terminal support for colouring and print coloured output. 77 | -} 78 | 79 | {- $env 80 | Global environment for a CLI application and CLI app settings. 81 | -} 82 | 83 | {- $settings 84 | Settings for the environment. 85 | -} 86 | 87 | {- $tool 88 | Capabilities to check required tools on the application start. 89 | -} 90 | -------------------------------------------------------------------------------- /src/Iris/App.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingVia #-} 2 | 3 | {- | 4 | Module : Iris.App 5 | Copyright : (c) 2022 Dmitrii Kovanikov 6 | SPDX-License-Identifier : MPL-2.0 7 | Maintainer : Dmitrii Kovanikov 8 | Stability : Experimental 9 | Portability : Portable 10 | 11 | The application monad — 'CliApp'. 12 | 13 | Many functions in __Iris__ are polymorphic over any monad that has the 'MonadReader' constraint. 14 | 15 | Implement your own application monad as a __newtype__ wrapper around 'CliApp' in 16 | the following way. 17 | 18 | @ 19 | __newtype__ App a = App 20 | { unApp :: Iris.'CliApp' MyOptions MyEnv a 21 | } __deriving newtype__ 22 | ( 'Functor' 23 | , 'Applicative' 24 | , 'Monad' 25 | , 'MonadIO' 26 | , 'MonadUnliftIO' 27 | , 'MonadReader' (Iris.'CliEnv' MyOptions MyEnv) 28 | ) 29 | @ 30 | 31 | @since 0.0.0.0 32 | -} 33 | module Iris.App ( 34 | CliApp (..), 35 | runCliApp, 36 | runCliAppManually, 37 | ) where 38 | 39 | import Control.Monad.IO.Class (MonadIO) 40 | import Control.Monad.IO.Unlift (MonadUnliftIO) 41 | import Control.Monad.Reader (MonadReader, ReaderT (..)) 42 | 43 | import Iris.Env (CliEnv, mkCliEnv) 44 | import Iris.Settings (CliEnvSettings) 45 | 46 | {- | Main monad for your CLI application. 47 | 48 | The type variables are: 49 | 50 | * @cmd@: the data type for your CLI arguments 51 | * @appEnv@: custom environment for your application (can be just @()@ if you 52 | don't need one) 53 | * @a@: the value inside the monadic context 54 | 55 | @since 0.0.0.0 56 | -} 57 | newtype CliApp cmd appEnv a = CliApp 58 | { unCliApp :: CliEnv cmd appEnv -> IO a 59 | } 60 | deriving 61 | ( Functor 62 | -- ^ @since 0.0.0.0 63 | , Applicative 64 | -- ^ @since 0.0.0.0 65 | , Monad 66 | -- ^ @since 0.0.0.0 67 | , MonadIO 68 | -- ^ @since 0.0.0.0 69 | , MonadReader (CliEnv cmd appEnv) 70 | -- ^ @since 0.0.0.0 71 | , MonadUnliftIO 72 | -- ^ @since 0.0.0.0 73 | ) 74 | via ReaderT (CliEnv cmd appEnv) IO 75 | 76 | {- | Run application with settings. 77 | 78 | This function is supposed to be used in your @main@ function: 79 | 80 | @ 81 | app :: App () 82 | app = ... your main application ... 83 | 84 | main :: IO () 85 | main = 'runCliApp' mySettings (unApp app) 86 | @ 87 | 88 | @since 0.0.0.0 89 | -} 90 | runCliApp :: CliEnvSettings cmd appEnv -> CliApp cmd appEnv a -> IO a 91 | runCliApp settings cliApp = do 92 | cliEnv <- mkCliEnv settings 93 | runCliAppManually cliEnv cliApp 94 | 95 | {- | Run application by constructing 'CliEnv' settings manually. 96 | 97 | @since 0.0.0.0 98 | -} 99 | runCliAppManually :: CliEnv cmd appEnv -> CliApp cmd appEnv a -> IO a 100 | runCliAppManually cliEnv (CliApp run) = run cliEnv 101 | -------------------------------------------------------------------------------- /src/Iris/Browse.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Browse 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Implements a function that opens a given file in a browser. 10 | 11 | @since 0.0.0.0 12 | -} 13 | module Iris.Browse ( 14 | openInBrowser, 15 | BrowseException (..), 16 | ) where 17 | 18 | import Control.Exception (Exception, throwIO) 19 | import System.Directory (findExecutable) 20 | import System.Environment (lookupEnv) 21 | import System.Info (os) 22 | import System.Process (callCommand, showCommandForUser) 23 | 24 | {- | Exception thrown by 'openInBrowser'. 25 | 26 | @since 0.0.0.0 27 | -} 28 | newtype BrowseException 29 | = -- | Can't find a browser application. Stores the current OS inside. 30 | -- 31 | -- @since 0.0.0.0 32 | BrowserNotFoundException String 33 | deriving stock 34 | ( Show 35 | -- ^ @since 0.0.0.0 36 | ) 37 | deriving newtype 38 | ( Eq 39 | -- ^ @since 0.0.0.0 40 | ) 41 | deriving anyclass 42 | ( Exception 43 | -- ^ @since 0.0.0.0 44 | ) 45 | 46 | {- | Open a given file in a browser. The function has the following algorithm: 47 | 48 | * Check the @BROWSER@ environment variable 49 | * If it's not set, try to guess browser depending on OS 50 | * If unsuccsessful, print a message 51 | 52 | __Throws:__ 'BrowseException' if can't find a browser. 53 | 54 | @since 0.0.0.0 55 | -} 56 | openInBrowser :: FilePath -> IO () 57 | openInBrowser file = 58 | lookupEnv "BROWSER" >>= \case 59 | Just browser -> runCommand browser [file] 60 | Nothing -> case os of 61 | "darwin" -> runCommand "open" [file] 62 | "mingw32" -> runCommand "cmd" ["/c", "start", file] 63 | curOs -> do 64 | browserExe <- 65 | findFirstExecutable 66 | [ "xdg-open" 67 | , "cygstart" 68 | , "x-www-browser" 69 | , "firefox" 70 | , "opera" 71 | , "mozilla" 72 | , "netscape" 73 | ] 74 | case browserExe of 75 | Just browser -> runCommand browser [file] 76 | Nothing -> throwIO $ BrowserNotFoundException curOs 77 | 78 | -- | Execute a command with arguments. 79 | runCommand :: FilePath -> [String] -> IO () 80 | runCommand cmd args = do 81 | let cmdStr = showCommandForUser cmd args 82 | putStrLn $ "⚙ " ++ cmdStr 83 | callCommand cmdStr 84 | 85 | findFirstExecutable :: [FilePath] -> IO (Maybe FilePath) 86 | findFirstExecutable = \case 87 | [] -> pure Nothing 88 | exe : exes -> 89 | findExecutable exe >>= \case 90 | Nothing -> findFirstExecutable exes 91 | Just path -> pure $ Just path 92 | -------------------------------------------------------------------------------- /src/Iris/Cli.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | CLI options parsing. 10 | 11 | @since 0.0.0.0 12 | -} 13 | module Iris.Cli ( 14 | -- $browse 15 | module Iris.Cli.Browse, 16 | -- $parserInfo 17 | module Iris.Cli.ParserInfo, 18 | -- $version 19 | module Iris.Cli.Version, 20 | ) where 21 | 22 | import Iris.Cli.Browse 23 | import Iris.Cli.ParserInfo 24 | import Iris.Cli.Version 25 | 26 | {- $browse 27 | CLI parsers for @--browse@ flags. 28 | -} 29 | 30 | {- $parserInfo 31 | Info needed to create a CLI parser. 32 | -} 33 | 34 | {- $version 35 | CLI parsers for @--version@ and @--numeric-version@ flags. 36 | -} 37 | -------------------------------------------------------------------------------- /src/Iris/Cli/Browse.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli.Browse 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | CLI options parsing for @--browse@ and @--browse=FILE_PATH@. 10 | 11 | @since 0.0.0.0 12 | -} 13 | module Iris.Cli.Browse ( 14 | browseP, 15 | browseFileP, 16 | ) where 17 | 18 | import qualified Options.Applicative as Opt 19 | 20 | {- | A CLI option parser yields a boolean value if a file needs to be open in 21 | a browser. 22 | 23 | Use 'Iris.Browse.openInBrowser' to open the file of your choice in a 24 | browser. 25 | 26 | @since 0.0.0.0 27 | -} 28 | browseP 29 | :: String 30 | -- ^ Flag description 31 | -> Opt.Parser Bool 32 | browseP description = 33 | Opt.switch $ 34 | mconcat 35 | [ Opt.long "browse" 36 | , Opt.help description 37 | ] 38 | 39 | {- | A CLI option parser for a 'FilePath' that needs to be open with a browser. 40 | 41 | Use 'Iris.Browse.openInBrowser' to open the passed file in a browser. 42 | 43 | @since 0.0.0.0 44 | -} 45 | browseFileP 46 | :: String 47 | -- ^ Flag description 48 | -> Opt.Parser FilePath 49 | browseFileP description = 50 | Opt.option Opt.str $ 51 | mconcat 52 | [ Opt.long "browse" 53 | , Opt.metavar "FILE_PATH" 54 | , Opt.help description 55 | ] 56 | -------------------------------------------------------------------------------- /src/Iris/Cli/Colour.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli.Colour 3 | Copyright : (c) 2023 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | CLI parser for the @--colour@ and @--no-colour@ options. 10 | 11 | @since 0.1.0.0 12 | -} 13 | module Iris.Cli.Colour ( 14 | ColourOption (..), 15 | colourOptionP, 16 | ) where 17 | 18 | import Control.Applicative ((<|>)) 19 | 20 | import qualified Options.Applicative as Opt 21 | 22 | {- | Data type that tells whether the user wants the colouring option enabled, 23 | disabled or autodetected. 24 | 25 | See 'colourOptionP' for the parser of this option. 26 | 27 | @since 0.1.0.0 28 | -} 29 | data ColourOption 30 | = -- | @since 0.1.0.0 31 | Always 32 | | -- | @since 0.1.0.0 33 | Never 34 | | -- | @since 0.1.0.0 35 | Auto 36 | deriving stock 37 | ( Show 38 | -- ^ @since 0.1.0.0 39 | , Eq 40 | -- ^ @since 0.1.0.0 41 | , Ord 42 | -- ^ @since 0.1.0.0 43 | , Enum 44 | -- ^ @since 0.1.0.0 45 | , Bounded 46 | -- ^ @since 0.1.0.0 47 | ) 48 | 49 | {- | A CLI option parser for the desired coloured output mode in the terminal. 50 | 51 | It parses @--colour@ and @--no-colour@ flags explicitly. Otherwise, it defaults 52 | to 'Auto'. 53 | 54 | @since 0.1.0.0 55 | -} 56 | colourOptionP :: Opt.Parser ColourOption 57 | colourOptionP = alwaysP <|> neverP <|> pure Auto 58 | where 59 | alwaysP = 60 | Opt.flag' Always (Opt.long "colour" <> Opt.help "Always output colours") 61 | <|> Opt.flag' Always (Opt.long "color" <> Opt.internal) 62 | 63 | neverP = 64 | Opt.flag' Never (Opt.long "no-colour" <> Opt.help "Never output colours") 65 | <|> Opt.flag' Never (Opt.long "no-color" <> Opt.internal) 66 | <|> Opt.flag' Never (Opt.long "disable-color" <> Opt.internal) 67 | <|> Opt.flag' Never (Opt.long "disable-coulor" <> Opt.internal) 68 | -------------------------------------------------------------------------------- /src/Iris/Cli/Interactive.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli.Interactive 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Interactive mode datatype and CLI parser. 10 | 11 | @since 0.0.0.0 12 | -} 13 | module Iris.Cli.Interactive ( 14 | InteractiveMode (..), 15 | interactiveModeP, 16 | handleInteractiveMode, 17 | ) where 18 | 19 | import Options.Applicative ((<|>)) 20 | import qualified Options.Applicative as Opt 21 | import System.Console.ANSI (hSupportsANSI) 22 | import System.IO (stdin) 23 | 24 | {- | Datatype for specifying if the terminal is interactive. 25 | 26 | @since 0.0.0.0 27 | -} 28 | data InteractiveMode 29 | = -- | @since 0.0.0.0 30 | Interactive 31 | | -- | @since 0.0.0.0 32 | NonInteractive 33 | deriving stock 34 | ( Show 35 | -- ^ @since 0.0.0.0 36 | , Eq 37 | -- ^ @since 0.0.0.0 38 | ) 39 | 40 | {- | A CLI option parser for switching to non-interactive mode 41 | if the @--no-input@ flag is passed. 42 | 43 | @since 0.0.0.0 44 | -} 45 | interactiveModeP :: Opt.Parser InteractiveMode 46 | interactiveModeP = nonInteractiveP <|> pure Interactive 47 | where 48 | nonInteractiveP :: Opt.Parser InteractiveMode 49 | nonInteractiveP = 50 | Opt.flag' NonInteractive $ 51 | mconcat 52 | [ Opt.long "no-input" 53 | , Opt.help "Enter the terminal in non-interactive mode" 54 | ] 55 | 56 | {- | Forces non interactive mode when the terminal is not interactive 57 | 58 | Use this function to check whether you can get input from the terminal: 59 | 60 | @ 61 | 'handleInteractiveMode' requestedInteractiveMode 62 | @ 63 | 64 | If the terminal is non interactive i.e. the program is run in a pipe, 65 | interactive mode is set to false no matter what 66 | 67 | @since 0.1.0.0 68 | -} 69 | handleInteractiveMode :: InteractiveMode -> IO InteractiveMode 70 | handleInteractiveMode optionMode = do 71 | supportsANSI <- hSupportsANSI stdin 72 | pure $ if supportsANSI then optionMode else NonInteractive 73 | -------------------------------------------------------------------------------- /src/Iris/Cli/Internal.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli.Cmd 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Internal 7 | Portability : Portable 8 | 9 | Wrapper around the user-defined command. 10 | 11 | __⚠️ This module is internal and doesn't follow PVP.__ 12 | -} 13 | module Iris.Cli.Internal ( 14 | Cmd (..), 15 | ) where 16 | 17 | import Data.Kind (Type) 18 | import Iris.Cli.Colour (ColourOption) 19 | import Iris.Cli.Interactive (InteractiveMode) 20 | 21 | -- | Wrapper around @cmd@ with additional predefined fields 22 | data Cmd (cmd :: Type) = Cmd 23 | { cmdInteractiveMode :: InteractiveMode 24 | , cmdColourOption :: ColourOption 25 | , cmdCmd :: cmd 26 | } 27 | -------------------------------------------------------------------------------- /src/Iris/Cli/ParserInfo.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | 4 | {- | 5 | Module : Iris.Cli.ParserInfo 6 | Copyright : (c) 2022 Dmitrii Kovanikov 7 | SPDX-License-Identifier : MPL-2.0 8 | Maintainer : Dmitrii Kovanikov 9 | Stability : Experimental 10 | Portability : Portable 11 | 12 | Parser information for the default CLI parser. 13 | 14 | @since 0.1.0.0 15 | -} 16 | module Iris.Cli.ParserInfo (cmdParserInfo) where 17 | 18 | import Iris.Cli.Interactive (interactiveModeP) 19 | import Iris.Cli.Internal (Cmd (..)) 20 | import Iris.Cli.Version (mkVersionParser) 21 | import Iris.Settings (CliEnvSettings (..)) 22 | 23 | import Iris.Cli.Colour (colourOptionP) 24 | import qualified Options.Applicative as Opt 25 | 26 | {- | 27 | 28 | @since 0.1.0.0 29 | -} 30 | cmdParserInfo :: forall cmd appEnv. CliEnvSettings cmd appEnv -> Opt.ParserInfo (Cmd cmd) 31 | cmdParserInfo CliEnvSettings{..} = 32 | Opt.info 33 | ( Opt.helper 34 | <*> mkVersionParser cliEnvSettingsVersionSettings 35 | <*> cmdP 36 | ) 37 | $ mconcat 38 | [ Opt.fullDesc 39 | , Opt.header cliEnvSettingsHeaderDesc 40 | , Opt.progDesc cliEnvSettingsProgDesc 41 | ] 42 | where 43 | cmdP :: Opt.Parser (Cmd cmd) 44 | cmdP = do 45 | cmdInteractiveMode <- interactiveModeP 46 | cmdCmd <- cliEnvSettingsCmdParser 47 | cmdColourOption <- colourOptionP 48 | 49 | pure Cmd{..} 50 | -------------------------------------------------------------------------------- /src/Iris/Cli/Version.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Cli.Version 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | CLI options parsing for @--version@ and @--numeric-version@ 10 | 11 | __Enabled with config__ 12 | 13 | @since 0.0.0.0 14 | -} 15 | module Iris.Cli.Version ( 16 | -- * Settings 17 | VersionSettings (..), 18 | defaultVersionSettings, 19 | 20 | -- * CLI parser 21 | fullVersionP, 22 | 23 | -- * Internal helpers 24 | mkVersionParser, 25 | ) where 26 | 27 | import Data.Version (Version, showVersion) 28 | 29 | import qualified Options.Applicative as Opt 30 | 31 | {- | 32 | 33 | @since 0.0.0.0 34 | -} 35 | data VersionSettings = VersionSettings 36 | { versionSettingsVersion :: Version 37 | -- ^ @since 0.0.0.0 38 | , versionSettingsMkDesc :: String -> String 39 | -- ^ @since 0.0.0.0 40 | } 41 | 42 | {- | 43 | 44 | @since 0.0.0.0 45 | -} 46 | defaultVersionSettings :: Version -> VersionSettings 47 | defaultVersionSettings version = 48 | VersionSettings 49 | { versionSettingsVersion = version 50 | , versionSettingsMkDesc = id 51 | } 52 | 53 | {- | 54 | 55 | @since 0.0.0.0 56 | -} 57 | mkVersionParser :: Maybe VersionSettings -> Opt.Parser (a -> a) 58 | mkVersionParser = maybe (pure id) fullVersionP 59 | 60 | {- | 61 | 62 | @since 0.0.0.0 63 | -} 64 | fullVersionP :: VersionSettings -> Opt.Parser (a -> a) 65 | fullVersionP VersionSettings{..} = versionP <*> numericVersionP 66 | where 67 | versionStr :: String 68 | versionStr = showVersion versionSettingsVersion 69 | 70 | versionP :: Opt.Parser (a -> a) 71 | versionP = 72 | Opt.infoOption (versionSettingsMkDesc versionStr) $ 73 | mconcat 74 | [ Opt.long "version" 75 | , Opt.help "Show application version" 76 | ] 77 | 78 | numericVersionP :: Opt.Parser (a -> a) 79 | numericVersionP = 80 | Opt.infoOption versionStr $ 81 | mconcat 82 | [ Opt.long "numeric-version" 83 | , Opt.help "Show only numeric application version" 84 | ] 85 | -------------------------------------------------------------------------------- /src/Iris/Colour.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Colour 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Functions to handle colouring of the output to terminal. 10 | 11 | @since 0.0.0.0 12 | -} 13 | module Iris.Colour ( 14 | -- $mode 15 | module Iris.Colour.Mode, 16 | -- $formatting 17 | module Iris.Colour.Formatting, 18 | ) where 19 | 20 | import Iris.Colour.Formatting 21 | import Iris.Colour.Mode 22 | 23 | {- $mode 24 | Colouring mode in application. 25 | -} 26 | 27 | {- $formatting 28 | Formatting of Terminal output 29 | -} 30 | -------------------------------------------------------------------------------- /src/Iris/Colour/Formatting.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | 3 | {- | 4 | Module : Iris.Colour.Formatting 5 | Copyright : (c) 2022 Dmitrii Kovanikov 6 | SPDX-License-Identifier : MPL-2.0 7 | Maintainer : Dmitrii Kovanikov 8 | Stability : Experimental 9 | Portability : Portable 10 | 11 | Helper functions to print with colouring. 12 | 13 | @since 0.0.0.0 14 | -} 15 | module Iris.Colour.Formatting ( 16 | putStdoutColouredLn, 17 | putStderrColouredLn, 18 | putStdoutColoured, 19 | putStderrColoured, 20 | ) where 21 | 22 | import Control.Monad.IO.Class (MonadIO (..)) 23 | import Control.Monad.Reader (MonadReader) 24 | import Data.Text (Text) 25 | import qualified Data.Text.IO as T 26 | import System.IO (stderr) 27 | 28 | import Iris.Colour.Mode (ColourMode (..)) 29 | import Iris.Env (CliEnv (..), asksCliEnv) 30 | 31 | import qualified Data.Text.IO as TIO 32 | 33 | {- | Print 'Text' to 'System.IO.stdout' by providing a custom 34 | formatting function. 35 | 36 | This works especially well with the @pretty-terminal@ package: 37 | 38 | @ 39 | 'putStdoutColouredLn' 40 | (style Bold . color Green) 41 | "my message" 42 | @ 43 | 44 | @since 0.0.0.0 45 | -} 46 | putStdoutColouredLn 47 | :: ( MonadReader (CliEnv cmd appEnv) m 48 | , MonadIO m 49 | ) 50 | => (Text -> Text) 51 | -> Text 52 | -> m () 53 | putStdoutColouredLn formatWithColour str = do 54 | colourMode <- asksCliEnv cliEnvStdoutColourMode 55 | liftIO $ T.putStrLn $ case colourMode of 56 | DisableColour -> str 57 | EnableColour -> formatWithColour str 58 | 59 | {- | Print 'Text' to 'System.IO.stderr' by providing a custom 60 | formatting function. 61 | 62 | This works especially well with the @pretty-terminal@ package: 63 | 64 | @ 65 | 'putStderrColouredLn' 66 | (style Bold . color Green) 67 | "my message" 68 | @ 69 | 70 | @since 0.0.0.0 71 | -} 72 | putStderrColouredLn 73 | :: ( MonadReader (CliEnv cmd appEnv) m 74 | , MonadIO m 75 | ) 76 | => (Text -> Text) 77 | -> Text 78 | -> m () 79 | putStderrColouredLn formatWithColour str = do 80 | colourMode <- asksCliEnv cliEnvStderrColourMode 81 | liftIO $ T.hPutStrLn stderr $ case colourMode of 82 | DisableColour -> str 83 | EnableColour -> formatWithColour str 84 | 85 | {- | Print 'Text' to 'System.IO.stdout' by providing a custom 86 | formatting function. Doesn't breaks output line that differs from 87 | `putStdoutColouredLn` 88 | 89 | This works especially well with the @pretty-terminal@ package: 90 | 91 | @ 92 | 'putStdoutColoured' 93 | (style Bold . color Green) 94 | "my message" 95 | @ 96 | 97 | @since 0.1.0.0 98 | -} 99 | putStdoutColoured 100 | :: ( MonadReader (CliEnv cmd appEnv) m 101 | , MonadIO m 102 | ) 103 | => (Text -> Text) 104 | -> Text 105 | -> m () 106 | putStdoutColoured formatWithColour str = do 107 | colourMode <- asksCliEnv cliEnvStdoutColourMode 108 | liftIO $ TIO.putStr $ case colourMode of 109 | DisableColour -> str 110 | EnableColour -> formatWithColour str 111 | 112 | {- | Print 'Text' to 'System.IO.stderr' by providing a custom 113 | formatting function. Doesn't breaks output line that differs from 114 | `putStderrColouredLn` 115 | 116 | This works especially well with the @pretty-terminal@ package: 117 | 118 | @ 119 | 'putStderrColoured' 120 | (style Bold . color Green) 121 | "my message" 122 | @ 123 | 124 | @since 0.1.0.0 125 | -} 126 | putStderrColoured 127 | :: ( MonadReader (CliEnv cmd appEnv) m 128 | , MonadIO m 129 | ) 130 | => (Text -> Text) 131 | -> Text 132 | -> m () 133 | putStderrColoured formatWithColour str = do 134 | colourMode <- asksCliEnv cliEnvStderrColourMode 135 | liftIO $ TIO.hPutStr stderr $ case colourMode of 136 | DisableColour -> str 137 | EnableColour -> formatWithColour str 138 | -------------------------------------------------------------------------------- /src/Iris/Colour/Mode.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Colour.Mode 3 | Copyright : 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | The 'ColourMode' data type that allows disabling and enabling of 10 | colouring. 11 | 12 | @since 0.0.0.0 13 | -} 14 | module Iris.Colour.Mode ( 15 | ColourMode (..), 16 | detectColourMode, 17 | 18 | -- * Internal 19 | handleColourMode, 20 | ) where 21 | 22 | import Data.Char (toLower, toUpper) 23 | import Data.Maybe (isJust) 24 | import System.Console.ANSI (hSupportsANSIColor) 25 | import System.Environment (lookupEnv) 26 | import System.IO (Handle) 27 | 28 | import Iris.Cli.Colour (ColourOption (..)) 29 | 30 | {- | Data type that tells whether the colouring is enabled or 31 | disabled. Its value is detected automatically on application start and 32 | stored in 'Iris.Env.CliEnv'. 33 | 34 | @since 0.0.0.0 35 | -} 36 | data ColourMode 37 | = -- | @since 0.0.0.0 38 | DisableColour 39 | | -- | @since 0.0.0.0 40 | EnableColour 41 | deriving stock 42 | ( Show 43 | -- ^ @since 0.0.0.0 44 | , Eq 45 | -- ^ @since 0.0.0.0 46 | , Ord 47 | -- ^ @since 0.0.0.0 48 | , Enum 49 | -- ^ @since 0.0.0.0 50 | , Bounded 51 | -- ^ @since 0.0.0.0 52 | ) 53 | 54 | {- | Returns 'ColourMode' of a 'Handle' ignoring environment and CLI options. 55 | You can use this function on output 'Handle's to find out whether they support 56 | colouring or not. 57 | 58 | Use a function like this to check whether you can print with colour 59 | to terminal: 60 | 61 | @ 62 | 'handleColourMode' 'System.IO.stdout' 63 | @ 64 | 65 | @since 0.0.0.0 66 | -} 67 | handleColourMode :: Handle -> IO ColourMode 68 | handleColourMode handle = do 69 | supportsANSI <- hSupportsANSIColor handle 70 | pure $ if supportsANSI then EnableColour else DisableColour 71 | 72 | {- | This function performs a full check of the 'Handle' colouring support, env 73 | variables and user-specified settings to detect whether the given handle 74 | supports colouring. 75 | 76 | Per CLI Guidelines, the algorithm for detecting the colouring support is the 77 | following: 78 | 79 | __Disable color if your program is not in a terminal or the user requested it. 80 | These things should disable colors:__ 81 | 82 | * @stdout@ or @stderr@ is not an interactive terminal (a TTY). It’s best to 83 | individually check—if you’re piping stdout to another program, it’s still 84 | useful to get colors on stderr. 85 | * The @NO_COLOR@ environment variable is set. 86 | * The @TERM@ environment variable has the value @dumb@. 87 | * The user passes the option @--no-color@. 88 | * You may also want to add a @MYAPP_NO_COLOR@ environment variable in case users 89 | want to disable color specifically for your program. 90 | 91 | ℹ️ Iris performs this check on the application start automatically so you don't 92 | need to call this function manually. 93 | 94 | @since 0.1.0.0 95 | -} 96 | detectColourMode 97 | :: Handle 98 | -- ^ A terminal handle (e.g. 'System.IO.stderr') 99 | -> ColourOption 100 | -- ^ User settings 101 | -> Maybe String 102 | -- ^ Application name 103 | -> IO ColourMode 104 | detectColourMode handle colour maybeAppName = case colour of 105 | Never -> pure DisableColour 106 | Always -> pure EnableColour 107 | Auto -> autoDetectColour 108 | where 109 | autoDetectColour :: IO ColourMode 110 | autoDetectColour = disabledToMode <$> checkIfDisabled 111 | 112 | disabledToMode :: Bool -> ColourMode 113 | disabledToMode isDisabled = 114 | if isDisabled then DisableColour else EnableColour 115 | 116 | checkIfDisabled :: IO Bool 117 | checkIfDisabled = 118 | orM 119 | [ isHandleColouringDisabled 120 | , hasNoColourEnvVars 121 | , isTermDumb 122 | ] 123 | 124 | isHandleColouringDisabled :: IO Bool 125 | isHandleColouringDisabled = (== DisableColour) <$> handleColourMode handle 126 | 127 | hasNoColourEnvVars :: IO Bool 128 | hasNoColourEnvVars = orM $ map hasEnvVar allVarNames 129 | 130 | isTermDumb :: IO Bool 131 | isTermDumb = 132 | lookupEnv "TERM" >>= \mVal -> pure $ case mVal of 133 | Nothing -> False 134 | Just val -> map toLower val == "dumb" 135 | 136 | hasEnvVar :: String -> IO Bool 137 | hasEnvVar var = isJust <$> lookupEnv var 138 | 139 | noColourVarNames :: [String] 140 | noColourVarNames = ["NO_COLOR", "NO_COLOUR"] 141 | 142 | prepend :: String -> String -> String 143 | prepend appName envName = map toUpper appName <> "_" <> envName 144 | 145 | allVarNames :: [String] 146 | allVarNames = case maybeAppName of 147 | Nothing -> noColourVarNames 148 | Just appName -> noColourVarNames <> map (prepend appName) noColourVarNames 149 | 150 | (||^) :: Monad m => m Bool -> m Bool -> m Bool 151 | mx ||^ my = do 152 | x <- mx 153 | if x 154 | then pure True 155 | else my 156 | 157 | orM :: Monad m => [m Bool] -> m Bool 158 | orM = foldr (||^) (pure False) 159 | -------------------------------------------------------------------------------- /src/Iris/Env.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | 4 | {- | 5 | Module : Iris.Env 6 | Copyright : (c) 2022 Dmitrii Kovanikov 7 | SPDX-License-Identifier : MPL-2.0 8 | Maintainer : Dmitrii Kovanikov 9 | Stability : Experimental 10 | Portability : Portable 11 | 12 | Environment of a CLI app. 13 | 14 | @since 0.0.0.0 15 | -} 16 | module Iris.Env ( 17 | -- * CLI application environment 18 | 19 | -- ** Constructing 20 | CliEnv (..), 21 | mkCliEnv, 22 | 23 | -- ** Querying 24 | asksCliEnv, 25 | asksAppEnv, 26 | ) where 27 | 28 | import Control.Monad.Reader (MonadReader, asks) 29 | import Data.Kind (Type) 30 | import System.IO (stderr, stdout) 31 | 32 | import Iris.Cli.Interactive (InteractiveMode, handleInteractiveMode) 33 | import Iris.Cli.Internal (Cmd (..)) 34 | import Iris.Cli.ParserInfo (cmdParserInfo) 35 | import Iris.Colour.Mode (ColourMode, detectColourMode) 36 | import Iris.Settings (CliEnvSettings (..)) 37 | 38 | import qualified Options.Applicative as Opt 39 | 40 | {- | CLI application environment. It contains default settings for 41 | every CLI app and parameter 42 | 43 | Has the following type parameters: 44 | 45 | * @cmd@: application commands 46 | * @appEnv@: application-specific environment; use @()@ if you don't 47 | have custom app environment 48 | 49 | @since 0.0.0.0 50 | -} 51 | data CliEnv (cmd :: Type) (appEnv :: Type) = CliEnv 52 | { cliEnvCmd :: cmd 53 | -- ^ @since 0.0.0.0 54 | , cliEnvStdoutColourMode :: ColourMode 55 | -- ^ @since 0.0.0.0 56 | , cliEnvStderrColourMode :: ColourMode 57 | -- ^ @since 0.0.0.0 58 | , cliEnvAppEnv :: appEnv 59 | -- ^ @since 0.0.0.0 60 | , cliEnvInteractiveMode :: InteractiveMode 61 | -- ^ @since 0.0.0.0 62 | } 63 | 64 | {- | 65 | 66 | @since 0.0.0.0 67 | -} 68 | mkCliEnv 69 | :: forall cmd appEnv 70 | . CliEnvSettings cmd appEnv 71 | -> IO (CliEnv cmd appEnv) 72 | mkCliEnv cliEnvSettings@CliEnvSettings{..} = do 73 | Cmd{..} <- Opt.execParser $ cmdParserInfo cliEnvSettings 74 | stdoutColourMode <- detectColourMode stdout cmdColourOption cliEnvSettingsAppName 75 | stderrColourMode <- detectColourMode stderr cmdColourOption cliEnvSettingsAppName 76 | interactive <- handleInteractiveMode cmdInteractiveMode 77 | 78 | pure 79 | CliEnv 80 | { cliEnvCmd = cmdCmd 81 | , cliEnvStdoutColourMode = stdoutColourMode 82 | , cliEnvStderrColourMode = stderrColourMode 83 | , cliEnvAppEnv = cliEnvSettingsAppEnv 84 | , cliEnvInteractiveMode = interactive 85 | } 86 | 87 | {- | Get a field from the global environment 'CliEnv'. 88 | 89 | @since 0.0.0.0 90 | -} 91 | asksCliEnv 92 | :: MonadReader (CliEnv cmd appEnv) m 93 | => (CliEnv cmd appEnv -> field) 94 | -> m field 95 | asksCliEnv = asks 96 | 97 | {- | Get a field from custom application-specific environment 98 | @appEnv@. 99 | 100 | @since 0.0.0.0 101 | -} 102 | asksAppEnv 103 | :: MonadReader (CliEnv cmd appEnv) m 104 | => (appEnv -> field) 105 | -> m field 106 | asksAppEnv getField = asksCliEnv (getField . cliEnvAppEnv) 107 | -------------------------------------------------------------------------------- /src/Iris/Interactive.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Interactive 3 | Copyright : (c) 2023 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Functions to handle interactive mode. 10 | 11 | @since x.x.x.x 12 | -} 13 | module Iris.Interactive ( 14 | -- $question 15 | module Iris.Interactive.Question, 16 | ) where 17 | 18 | import Iris.Interactive.Question 19 | 20 | {- $question 21 | Asking Questions and receiving an answer: 22 | -} 23 | -------------------------------------------------------------------------------- /src/Iris/Interactive/Question.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | 3 | {- | 4 | Module : Iris.Interactive.Question 5 | Copyright : (c) 2023 Dmitrii Kovanikov 6 | SPDX-License-Identifier : MPL-2.0 7 | Maintainer : Dmitrii Kovanikov 8 | Stability : Experimental 9 | Portability : Portable 10 | 11 | Asking Questions. Receiving answers. 12 | 13 | @since x.x.x.x 14 | -} 15 | module Iris.Interactive.Question ( 16 | yesno, 17 | YesNo (..), 18 | parseYesNo, 19 | ) where 20 | 21 | import Control.Monad.IO.Class (MonadIO (..)) 22 | import Control.Monad.Reader (MonadReader) 23 | 24 | import Data.Text (Text) 25 | import qualified Data.Text as Text 26 | import qualified Data.Text.IO as Text 27 | import System.IO (hFlush, stdout) 28 | 29 | import Iris.Cli.Interactive (InteractiveMode (..)) 30 | import Iris.Env (CliEnv (..), asksCliEnv) 31 | 32 | {- 33 | @since x.x.x.x 34 | -} 35 | parseYesNo :: Text -> Maybe YesNo 36 | parseYesNo t = case Text.toUpper . Text.strip $ t of 37 | "Y" -> Just Yes 38 | "YES" -> Just Yes 39 | "YS" -> Just Yes 40 | "N" -> Just No 41 | "NO" -> Just No 42 | _ -> Nothing 43 | 44 | {- | Parsed as Yes: "Y", "YES", "YS" (lower- or uppercase) 45 | 46 | Parsed as No: "N", "NO" (lower- or uppercase) 47 | 48 | @since x.x.x.x 49 | -} 50 | data YesNo 51 | = No 52 | | Yes 53 | deriving stock 54 | ( Show 55 | -- ^ @since x.x.x.x 56 | , Eq 57 | -- ^ @since x.x.x.x 58 | , Ord 59 | -- ^ @since x.x.x.x 60 | , Enum 61 | -- ^ @since x.x.x.x 62 | , Bounded 63 | -- ^ @since x.x.x.x 64 | ) 65 | 66 | {- | Ask a yes/no question to stdout, read the reply from terminal, return an Answer. 67 | 68 | In case of running non-interactively, return the provided default 69 | 70 | Example usage: 71 | 72 | @ 73 | app :: App () 74 | app = do 75 | answer <- Iris.yesno "Would you like to proceed?" Iris.Yes 76 | case answer of 77 | Iris.Yes -> proceed 78 | Iris.No -> Iris.outLn "Aborting" 79 | 80 | 81 | \$ ./irisapp 82 | Would you like to proceed? (yes/no) 83 | I don't understand your answer: '' 84 | Please, answer yes or no (or y, or n) 85 | Would you like to proceed? (yes/no) ne 86 | I don't understand your answer: 'ne' 87 | Please, answer yes or no (or y, or n) 88 | Would you like to proceed? (yes/no) NO 89 | Aborting 90 | 91 | @ 92 | 93 | @since x.x.x.x 94 | -} 95 | yesno 96 | :: (MonadIO m, MonadReader (CliEnv cmd appEnv) m) 97 | => Text 98 | -- ^ Question Text 99 | -> YesNo 100 | -- ^ Default answer when @--no-input@ is provided 101 | -> m YesNo 102 | yesno question defaultAnswer = do 103 | interactiveMode <- asksCliEnv cliEnvInteractiveMode 104 | case interactiveMode of 105 | NonInteractive -> pure defaultAnswer 106 | Interactive -> liftIO loop 107 | where 108 | loop :: IO YesNo 109 | loop = do 110 | Text.putStr $ question <> " (yes/no) " 111 | hFlush stdout 112 | input <- Text.getLine 113 | case parseYesNo input of 114 | Just answer -> pure answer 115 | Nothing -> do 116 | Text.putStrLn $ "I don't understand your answer: '" <> input <> "'" 117 | Text.putStrLn "Please, answer yes or no (or y, or n)" 118 | loop 119 | -------------------------------------------------------------------------------- /src/Iris/Settings.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Settings 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Settings of a CLI app environment. 10 | 11 | You're encouraged to create a separate module @MyApp.Settings@ and put settings 12 | for your custom application there following the below pattern: 13 | 14 | @ 15 | __module__ MyApp.Settings (appSettings) __where__ 16 | 17 | -- data data for your CLI arguments and CLI parser 18 | __import__ MyApp.Cli (Options, optionsP) 19 | 20 | -- custom application environment 21 | __import__ MyApp.Env (Env) 22 | 23 | __import qualified__ "Iris" 24 | __import qualified__ Paths_myapp __as__ Autogen 25 | 26 | 27 | appSettings :: Env -> Iris.'CliEnvSettings' Options Env 28 | appSettings env = Iris.defaultCliEnvSettings 29 | { -- CLI parser for Options 30 | Iris.'cliEnvSettingsCmdParser' = optionsP 31 | 32 | -- Custom app environment 33 | , Iris.'cliEnvSettingsAppEnv' = env 34 | 35 | -- Application name 36 | , Iris.'cliEnvSettingsAppName' = 37 | Just "myapp" 38 | 39 | -- Short app description 40 | , Iris.'cliEnvSettingsHeaderDesc' = 41 | "myapp - short description" 42 | 43 | -- Long app description to appear in --help 44 | , Iris.'cliEnvSettingsProgDesc' = 45 | "A tool for ..." 46 | 47 | -- How to print app version with the --version flag 48 | , Iris.'cliEnvSettingsVersionSettings' = 49 | Just (Iris.'Iris.Cli.Version.defaultVersionSettings' Autogen.version) 50 | { Iris.'Iris.Cli.Version.versionSettingsMkDesc' = \v -> "MyApp v" <> v 51 | } 52 | } 53 | @ 54 | 55 | @since 0.1.0.0 56 | -} 57 | module Iris.Settings ( 58 | -- * Settings for the CLI app 59 | CliEnvSettings (..), 60 | defaultCliEnvSettings, 61 | ) where 62 | 63 | import Data.Kind (Type) 64 | import Iris.Cli.Version (VersionSettings) 65 | 66 | import qualified Options.Applicative as Opt 67 | 68 | {- | The Iris settings type. 69 | 70 | Use 'defaultCliEnvSettings' to specify only used fields. 71 | 72 | @since 0.0.0.0 73 | -} 74 | data CliEnvSettings (cmd :: Type) (appEnv :: Type) = CliEnvSettings 75 | { cliEnvSettingsCmdParser :: Opt.Parser cmd 76 | -- ^ @since 0.0.0.0 77 | , cliEnvSettingsAppEnv :: appEnv 78 | -- ^ @since 0.0.0.0 79 | , cliEnvSettingsHeaderDesc :: String 80 | -- ^ @since 0.0.0.0 81 | , cliEnvSettingsProgDesc :: String 82 | -- ^ @since 0.0.0.0 83 | , cliEnvSettingsVersionSettings :: Maybe VersionSettings 84 | -- ^ @since 0.0.0.0 85 | , cliEnvSettingsAppName :: Maybe String 86 | -- ^ @since 0.1.0.0 87 | } 88 | 89 | {- | Default Iris app settings. 90 | 91 | @since 0.0.0.0 92 | -} 93 | defaultCliEnvSettings :: CliEnvSettings () () 94 | defaultCliEnvSettings = 95 | CliEnvSettings 96 | { cliEnvSettingsCmdParser = pure () 97 | , cliEnvSettingsAppEnv = () 98 | , cliEnvSettingsHeaderDesc = "Simple CLI program" 99 | , cliEnvSettingsProgDesc = "CLI tool build with iris - a Haskell CLI framework" 100 | , cliEnvSettingsAppName = Nothing 101 | , cliEnvSettingsVersionSettings = Nothing 102 | } 103 | -------------------------------------------------------------------------------- /src/Iris/Tool.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module : Iris.Tool 3 | Copyright : (c) 2022 Dmitrii Kovanikov 4 | SPDX-License-Identifier : MPL-2.0 5 | Maintainer : Dmitrii Kovanikov 6 | Stability : Experimental 7 | Portability : Portable 8 | 9 | Utilities to check required tools and their minimal version for a CLI app. 10 | 11 | Sometimes, your CLI application 12 | 13 | @since 0.0.0.0 14 | -} 15 | module Iris.Tool ( 16 | -- * Requiring an executable 17 | need, 18 | Tool (..), 19 | ToolSelector (..), 20 | defaultToolSelector, 21 | 22 | -- * Tool requirements check 23 | ToolCheckResult (..), 24 | ToolCheckError (..), 25 | ToolCheckException (..), 26 | checkTool, 27 | ) where 28 | 29 | import Control.Exception (Exception, throwIO) 30 | import Control.Monad.IO.Class (MonadIO, liftIO) 31 | import Data.Foldable (traverse_) 32 | import Data.String (IsString (..)) 33 | import Data.Text (Text) 34 | import System.Directory (findExecutable) 35 | import System.Process (readProcess) 36 | 37 | import qualified Data.Text as Text 38 | 39 | {- | 40 | 41 | @since 0.0.0.0 42 | -} 43 | data Tool = Tool 44 | { toolName :: Text 45 | -- ^ @since 0.0.0.0 46 | , toolSelector :: Maybe ToolSelector 47 | -- ^ @since 0.0.0.0 48 | } 49 | 50 | {- | 51 | 52 | @since 0.0.0.0 53 | -} 54 | instance IsString Tool where 55 | fromString :: String -> Tool 56 | fromString s = 57 | Tool 58 | { toolName = fromString s 59 | , toolSelector = Nothing 60 | } 61 | 62 | {- | 63 | 64 | @since 0.0.0.0 65 | -} 66 | data ToolSelector = ToolSelector 67 | { toolSelectorFunction :: Text -> Bool 68 | -- ^ @since 0.0.0.0 69 | , toolSelectorVersionArg :: Maybe Text 70 | -- ^ @since 0.0.0.0 71 | } 72 | 73 | {- | 74 | 75 | @since 0.0.0.0 76 | -} 77 | defaultToolSelector :: ToolSelector 78 | defaultToolSelector = 79 | ToolSelector 80 | { toolSelectorFunction = const True 81 | , toolSelectorVersionArg = Nothing 82 | } 83 | 84 | {- | 85 | 86 | @since 0.0.0.0 87 | -} 88 | data ToolCheckResult 89 | = -- | 90 | -- 91 | -- @since 0.1.0.0 92 | ToolCheckError ToolCheckError 93 | | -- | 94 | -- 95 | -- @since 0.0.0.0 96 | ToolOk 97 | deriving stock 98 | ( Show 99 | -- ^ @since 0.0.0.0 100 | , Eq 101 | -- ^ @since 0.0.0.0 102 | ) 103 | 104 | {- | 105 | 106 | @since 0.1.0.0 107 | -} 108 | data ToolCheckError 109 | = -- | 110 | -- 111 | -- @since 0.1.0.0 112 | ToolNotFound Text 113 | | -- | 114 | -- 115 | -- @since 0.1.0.0 116 | ToolWrongVersion Text 117 | deriving stock 118 | ( Show 119 | -- ^ @since 0.1.0.0 120 | , Eq 121 | -- ^ @since 0.1.0.0 122 | ) 123 | 124 | {- | 125 | 126 | @since 0.0.0.0 127 | -} 128 | checkTool :: Tool -> IO ToolCheckResult 129 | checkTool Tool{..} = 130 | findExecutable (Text.unpack toolName) >>= \case 131 | Nothing -> pure $ ToolCheckError $ ToolNotFound toolName 132 | Just exe -> case toolSelector of 133 | Nothing -> pure ToolOk 134 | Just ToolSelector{..} -> case toolSelectorVersionArg of 135 | Nothing -> pure ToolOk 136 | Just versionArg -> do 137 | toolVersionOutput <- readProcess exe [Text.unpack versionArg] "" 138 | let version = Text.strip $ Text.pack toolVersionOutput 139 | 140 | if toolSelectorFunction version 141 | then pure ToolOk 142 | else pure $ ToolCheckError $ ToolWrongVersion version 143 | 144 | {- | An exception thrown by 'need' when there's an error requiring a tool. 145 | 146 | @since 0.1.0.0 147 | -} 148 | newtype ToolCheckException = ToolCheckException ToolCheckError 149 | deriving stock 150 | ( Show 151 | -- ^ @since 0.1.0.0 152 | ) 153 | deriving newtype 154 | ( Eq 155 | -- ^ @since 0.1.0.0 156 | ) 157 | deriving anyclass 158 | ( Exception 159 | -- ^ @since 0.1.0.0 160 | ) 161 | 162 | {- | Use this function to require specific CLI tools for your CLI application. 163 | 164 | The function can be used in the beginning of each command in the following way: 165 | 166 | @ 167 | app :: App () 168 | app = Iris.'Iris.Env.asksCliEnv' Iris.'Iris.Env.cliEnvCmd' >>= __\\case__ 169 | Download url -> do 170 | Iris.'need' ["curl"] 171 | runDownload url 172 | Evaluate hs -> do 173 | Iris.'need' ["ghc", "cabal"] 174 | runEvaluate hs 175 | @ 176 | 177 | __Throws:__ 'ToolCheckException' if can't find a tool or if it has wrong version. 178 | 179 | @since 0.0.0.0 180 | -} 181 | need :: MonadIO m => [Tool] -> m () 182 | need = traverse_ $ \tool -> 183 | liftIO $ 184 | checkTool tool >>= \case 185 | ToolOk -> pure () 186 | ToolCheckError toolErr -> throwIO $ ToolCheckException toolErr 187 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-20.5 2 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import Test.Hspec (hspec) 4 | 5 | import Test.Iris (irisSpec) 6 | 7 | main :: IO () 8 | main = hspec irisSpec 9 | -------------------------------------------------------------------------------- /test/Test/Iris.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris (irisSpec) where 2 | 3 | import Test.Hspec (Spec, describe) 4 | 5 | import Test.Iris.Cli (cliSpec, cliSpecParserConflicts) 6 | import Test.Iris.Colour (colourSpec) 7 | import Test.Iris.Interactive (interactiveSpec) 8 | import Test.Iris.Tool (toolSpec) 9 | 10 | irisSpec :: Spec 11 | irisSpec = describe "Iris" $ do 12 | cliSpec 13 | cliSpecParserConflicts 14 | colourSpec 15 | toolSpec 16 | interactiveSpec 17 | -------------------------------------------------------------------------------- /test/Test/Iris/Cli.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Cli (cliSpec, cliSpecParserConflicts) where 2 | 3 | import Options.Applicative (getParseResult) 4 | import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, shouldBe, shouldReturn) 5 | 6 | import Iris (CliEnvSettings (..)) 7 | import Iris.Cli (VersionSettings (versionSettingsMkDesc)) 8 | import Iris.Cli.Colour (ColourOption (..)) 9 | import Iris.Cli.Interactive (InteractiveMode (..), handleInteractiveMode) 10 | import Iris.Cli.Internal 11 | import Iris.Cli.ParserInfo (cmdParserInfo) 12 | import Iris.Cli.Version (defaultVersionSettings) 13 | import Iris.Settings (defaultCliEnvSettings) 14 | 15 | import Test.Iris.Common (checkCI) 16 | 17 | import qualified Options.Applicative as Opt 18 | import qualified Paths_iris as Autogen 19 | 20 | expectedHelpText :: String 21 | expectedHelpText = 22 | "Simple CLI program\n\ 23 | \\n\ 24 | \Usage: [--no-input] [--colour | --no-colour]\n\ 25 | \\n\ 26 | \ CLI tool build with iris - a Haskell CLI framework\n\ 27 | \\n\ 28 | \Available options:\n\ 29 | \ -h,--help Show this help text\n\ 30 | \ --no-input Enter the terminal in non-interactive mode\n\ 31 | \ --colour Always output colours\n\ 32 | \ --no-colour Never output colours" 33 | 34 | expectedHelpTextWithVersion :: String 35 | expectedHelpTextWithVersion = 36 | "Simple CLI program\n\ 37 | \\n\ 38 | \Usage: [--version] [--numeric-version] [--no-input] \n\ 39 | \ [--colour | --no-colour]\n\ 40 | \\n\ 41 | \ CLI tool build with iris - a Haskell CLI framework\n\ 42 | \\n\ 43 | \Available options:\n\ 44 | \ -h,--help Show this help text\n\ 45 | \ --version Show application version\n\ 46 | \ --numeric-version Show only numeric application version\n\ 47 | \ --no-input Enter the terminal in non-interactive mode\n\ 48 | \ --colour Always output colours\n\ 49 | \ --no-colour Never output colours" 50 | 51 | expectedNumericVersion :: String 52 | expectedNumericVersion = "0.1.0.0" 53 | 54 | cliSpec :: Spec 55 | cliSpec = describe "Cli Options" $ do 56 | let parserPrefs = Opt.defaultPrefs 57 | it "help without version environment" $ do 58 | let parserInfo = cmdParserInfo defaultCliEnvSettings 59 | let result = Opt.execParserPure parserPrefs parserInfo ["--help"] 60 | parseResultHandlerFailure result expectedHelpText 61 | it "help with version environment" $ do 62 | let cliEnvSettings = defaultCliEnvSettings{cliEnvSettingsVersionSettings = Just (defaultVersionSettings Autogen.version)} 63 | let parserInfo = cmdParserInfo cliEnvSettings 64 | let result = Opt.execParserPure parserPrefs parserInfo ["--help"] 65 | parseResultHandlerFailure result expectedHelpTextWithVersion 66 | it "--numeric-version returns correct version" $ do 67 | let cliEnvSettings = defaultCliEnvSettings{cliEnvSettingsVersionSettings = Just (defaultVersionSettings Autogen.version)} 68 | let parserInfo = cmdParserInfo cliEnvSettings 69 | let result = Opt.execParserPure parserPrefs parserInfo ["--numeric-version"] 70 | parseResultHandlerFailure result expectedNumericVersion 71 | it "CI interactivity check" $ do 72 | handleInteractiveMode NonInteractive `shouldReturn` NonInteractive 73 | isCi <- checkCI 74 | if isCi 75 | then handleInteractiveMode Interactive `shouldReturn` NonInteractive 76 | else handleInteractiveMode Interactive `shouldReturn` Interactive 77 | it "Handles colour mode" $ do 78 | let parserInfo = cmdParserInfo defaultCliEnvSettings 79 | let coloption args = getParseResult $ cmdColourOption <$> Opt.execParserPure parserPrefs parserInfo args 80 | coloption ["--colour"] `shouldBe` pure Always 81 | coloption ["--no-colour"] `shouldBe` pure Never 82 | coloption [] `shouldBe` pure Auto 83 | 84 | it "--version returns correct version text" $ do 85 | let expectedVersionMkDescription = ("Version " ++) 86 | let cliEnvSettings = defaultCliEnvSettings{cliEnvSettingsVersionSettings = Just $ (defaultVersionSettings Autogen.version){versionSettingsMkDesc = expectedVersionMkDescription}} 87 | let parserInfo = cmdParserInfo cliEnvSettings 88 | let expectedVersion = expectedVersionMkDescription expectedNumericVersion 89 | let result = Opt.execParserPure parserPrefs parserInfo ["--version"] 90 | parseResultHandlerFailure result expectedVersion 91 | 92 | newtype UserDefinedParser a = UserDefinedParser {noInteractive :: a} 93 | 94 | userDefinedNoInputOption :: Opt.Parser (UserDefinedParser String) 95 | userDefinedNoInputOption = 96 | UserDefinedParser 97 | <$> Opt.strOption (Opt.long "no-input") 98 | 99 | userDefinedNoInputSwitch :: Opt.Parser (UserDefinedParser Bool) 100 | userDefinedNoInputSwitch = 101 | UserDefinedParser 102 | <$> Opt.switch (Opt.long "no-input") 103 | 104 | userDefinedNoInputOnCommand :: Opt.Parser (UserDefinedParser Bool) 105 | userDefinedNoInputOnCommand = 106 | Opt.subparser 107 | ( Opt.command 108 | "test-command" 109 | (Opt.info userDefinedNoInputSwitch Opt.fullDesc) 110 | ) 111 | 112 | customParserSettings :: Opt.Parser (UserDefinedParser a) -> CliEnvSettings (UserDefinedParser a) () 113 | customParserSettings parser = 114 | CliEnvSettings 115 | { cliEnvSettingsCmdParser = parser 116 | , cliEnvSettingsAppEnv = () 117 | , cliEnvSettingsHeaderDesc = "Simple CLI program" 118 | , cliEnvSettingsProgDesc = "CLI tool build with iris - a Haskell CLI framework" 119 | , cliEnvSettingsVersionSettings = Nothing 120 | , cliEnvSettingsAppName = Nothing 121 | } 122 | 123 | argValue :: String 124 | argValue = "someValue" 125 | 126 | expectedErrorTextUserDefinedNoInputArg :: String 127 | expectedErrorTextUserDefinedNoInputArg = 128 | "Invalid argument `" 129 | <> argValue 130 | <> "'\n\ 131 | \\n\ 132 | \Usage: [--no-input] --no-input ARG [--colour | --no-colour]\n\ 133 | \\n\ 134 | \ CLI tool build with iris - a Haskell CLI framework" 135 | 136 | expectedErrorTextUserDefinedNoInputNoArg :: String 137 | expectedErrorTextUserDefinedNoInputNoArg = 138 | "Missing: --no-input ARG\n\ 139 | \\n\ 140 | \Usage: [--no-input] --no-input ARG [--colour | --no-colour]\n\ 141 | \\n\ 142 | \ CLI tool build with iris - a Haskell CLI framework" 143 | 144 | cliSpecParserConflicts :: Spec 145 | cliSpecParserConflicts = describe "Cli Parser Conflicts" $ do 146 | let parserPrefs = Opt.defaultPrefs 147 | it "--no-input=someValue defined by user - arg provided" $ do 148 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputOption 149 | let result = Opt.execParserPure parserPrefs parserInfo ["--no-input", argValue] 150 | parseResultHandlerFailure result expectedErrorTextUserDefinedNoInputArg 151 | it "--no-input=someValue defined by user - no arg provided" $ do 152 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputOption 153 | let result = Opt.execParserPure parserPrefs parserInfo ["--no-input"] 154 | parseResultHandlerFailure result expectedErrorTextUserDefinedNoInputNoArg 155 | it "--no-input=someValue defined by user - not provided at all" $ do 156 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputOption 157 | let result = Opt.execParserPure parserPrefs parserInfo [] 158 | parseResultHandlerFailure result expectedErrorTextUserDefinedNoInputNoArg 159 | it "--no-input switch defined by user - provided" $ do 160 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputSwitch 161 | let result = Opt.execParserPure parserPrefs parserInfo ["--no-input"] 162 | parseResultHandlerSuccess result ["NonInteractive", "False"] 163 | it "--no-input switch defined by user - not provided" $ do 164 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputSwitch 165 | let result = Opt.execParserPure parserPrefs parserInfo [] 166 | parseResultHandlerSuccess result ["Interactive", "False"] 167 | it "--no-input switch with command defined by user - user provided" $ do 168 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputOnCommand 169 | let result = Opt.execParserPure parserPrefs parserInfo ["test-command", "--no-input"] 170 | parseResultHandlerSuccess result ["Interactive", "True"] 171 | it "--no-input switch with command defined by user - internal provided" $ do 172 | let parserInfo = cmdParserInfo $ customParserSettings userDefinedNoInputOnCommand 173 | let result = Opt.execParserPure parserPrefs parserInfo ["--no-input", "test-command"] 174 | parseResultHandlerSuccess result ["NonInteractive", "False"] 175 | 176 | parseResultHandlerSuccess :: Show b => Opt.ParserResult (Cmd (UserDefinedParser b)) -> [String] -> Expectation 177 | parseResultHandlerSuccess parseResult expected = 178 | case parseResult of 179 | Opt.Failure _ -> expectationFailure "Expected 'Success' but got 'Failure' " 180 | Opt.Success a -> do 181 | let internalNoInput = show $ cmdInteractiveMode a 182 | let userDefinedNoInput = show . noInteractive . cmdCmd $ a 183 | shouldBe [internalNoInput, userDefinedNoInput] expected 184 | Opt.CompletionInvoked completionResult -> expectationFailure $ "Expected 'Success' but got: " <> show completionResult 185 | 186 | parseResultHandlerFailure :: Opt.ParserResult a -> String -> Expectation 187 | parseResultHandlerFailure parseResult expected = 188 | case parseResult of 189 | -- The help functionality is baked into optparse-applicative and presents itself as a ParserFailure. 190 | Opt.Failure (Opt.ParserFailure getFailure) -> do 191 | let (helpText, _exitCode, _int) = getFailure "" 192 | show helpText `shouldBe` expected 193 | Opt.Success _ -> expectationFailure "Expected 'Failure' but got 'Success' " 194 | Opt.CompletionInvoked completionResult -> expectationFailure $ "Expected 'Failure' but got: " <> show completionResult 195 | -------------------------------------------------------------------------------- /test/Test/Iris/Colour.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Colour (colourSpec) where 2 | 3 | import Test.Hspec (Spec, describe) 4 | 5 | import Test.Iris.Colour.Mode (modeSpec) 6 | 7 | colourSpec :: Spec 8 | colourSpec = describe "Colour" $ do 9 | modeSpec 10 | -------------------------------------------------------------------------------- /test/Test/Iris/Colour/Mode.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Colour.Mode (modeSpec) where 2 | 3 | import Data.Foldable (for_) 4 | import System.Environment (setEnv, unsetEnv) 5 | import System.IO (stderr, stdout) 6 | import Test.Hspec (Spec, before_, describe, it, shouldReturn) 7 | 8 | import Iris.Cli.Colour (ColourOption (..)) 9 | import Iris.Colour.Mode (ColourMode (..), detectColourMode) 10 | 11 | import Test.Iris.Common (checkCI) 12 | 13 | modeSpec :: Spec 14 | modeSpec = before_ clearAppEnv $ describe "Mode" $ do 15 | let detectStdoutColour option = detectColourMode stdout option (Just "myapp") 16 | let detectStderrColour option = detectColourMode stderr option (Just "myapp") 17 | 18 | it "DisableColour when --no-colour" $ do 19 | detectStdoutColour Never `shouldReturn` DisableColour 20 | detectStderrColour Never `shouldReturn` DisableColour 21 | 22 | it "EnableColour when --colour" $ do 23 | detectStdoutColour Always `shouldReturn` EnableColour 24 | detectStderrColour Always `shouldReturn` EnableColour 25 | 26 | it "EnableColour in clear environment" $ do 27 | ciColour <- colourWithCI 28 | detectStdoutColour Auto `shouldReturn` ciColour 29 | detectStderrColour Auto `shouldReturn` ciColour 30 | 31 | it "DisableColour when NO_COLOR is set" $ do 32 | setEnv "NO_COLOR" "1" 33 | detectStdoutColour Auto `shouldReturn` DisableColour 34 | detectStderrColour Auto `shouldReturn` DisableColour 35 | 36 | it "DisableColour when NO_COLOUR is set" $ do 37 | setEnv "NO_COLOUR" "1" 38 | detectStdoutColour Auto `shouldReturn` DisableColour 39 | detectStderrColour Auto `shouldReturn` DisableColour 40 | 41 | it "DisableColour when MYAPP_NO_COLOR is set" $ do 42 | setEnv "MYAPP_NO_COLOR" "1" 43 | detectStdoutColour Auto `shouldReturn` DisableColour 44 | detectStderrColour Auto `shouldReturn` DisableColour 45 | 46 | it "DisableColour when MYAPP_NO_COLOUR is set" $ do 47 | setEnv "MYAPP_NO_COLOUR" "1" 48 | detectStdoutColour Auto `shouldReturn` DisableColour 49 | detectStderrColour Auto `shouldReturn` DisableColour 50 | 51 | it "DisableColour when TERM=dumb" $ do 52 | setEnv "TERM" "dumb" 53 | detectStdoutColour Auto `shouldReturn` DisableColour 54 | detectStderrColour Auto `shouldReturn` DisableColour 55 | 56 | it "EnableColour when TERM=xterm-256color" $ do 57 | setEnv "TERM" "xterm-256color" 58 | ciColour <- colourWithCI 59 | detectStdoutColour Auto `shouldReturn` ciColour 60 | detectStderrColour Auto `shouldReturn` ciColour 61 | 62 | it "DisableColour when CI is set" $ do 63 | ciColour <- colourWithCI 64 | detectStdoutColour Auto `shouldReturn` ciColour 65 | detectStderrColour Auto `shouldReturn` ciColour 66 | 67 | -- Helper functions 68 | 69 | testEnvVars :: [String] 70 | testEnvVars = 71 | [ "NO_COLOR" 72 | , "NO_COLOUR" 73 | , "MYAPP_NO_COLOR" 74 | , "MYAPP_NO_COLOUR" 75 | , "TERM" 76 | ] 77 | 78 | clearAppEnv :: IO () 79 | clearAppEnv = for_ testEnvVars unsetEnv 80 | 81 | colourWithCI :: IO ColourMode 82 | colourWithCI = do 83 | isCi <- checkCI 84 | pure $ if isCi then DisableColour else EnableColour 85 | -------------------------------------------------------------------------------- /test/Test/Iris/Common.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Common ( 2 | checkCI, 3 | ) where 4 | 5 | import Data.Maybe (isJust) 6 | import System.Environment (lookupEnv) 7 | 8 | checkCI :: IO Bool 9 | checkCI = isJust <$> lookupEnv "CI" 10 | -------------------------------------------------------------------------------- /test/Test/Iris/Interactive.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Interactive (interactiveSpec) where 2 | 3 | import Test.Hspec (Spec, describe) 4 | 5 | import Test.Iris.Interactive.Question (questionSpec) 6 | 7 | interactiveSpec :: Spec 8 | interactiveSpec = describe "Interactive" $ do 9 | questionSpec 10 | -------------------------------------------------------------------------------- /test/Test/Iris/Interactive/Question.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Interactive.Question (questionSpec) where 2 | 3 | import Control.Monad (forM_) 4 | import Data.Text (Text) 5 | import qualified Data.Text as Text 6 | import Test.Hspec (Spec, SpecWith, describe, it, shouldBe) 7 | 8 | import Iris.Interactive.Question ( 9 | -- under test 10 | YesNo (..), 11 | parseYesNo, 12 | ) 13 | 14 | yesAnswers :: [Text] 15 | yesAnswers = "y" : "Y" : [y <> e <> s | y <- ["y", "Y"], e <- ["e", "E", ""], s <- ["s", "S"]] 16 | 17 | questionSpec :: Spec 18 | questionSpec = 19 | describe "Question - parse YesNo" $ do 20 | checkElements yesAnswers (Just Yes) 21 | checkElements ["n", "N", "NO", "no", "No", "nO"] (Just No) 22 | checkElements ["a", "ye", "NOone", "yesterday", "oui"] Nothing 23 | it "Empty string parses to Nothing" $ 24 | parseYesNo "" `shouldBe` Nothing 25 | 26 | checkElements 27 | :: [Text] 28 | -> Maybe YesNo 29 | -> SpecWith () 30 | checkElements values expected = do 31 | describe ("should parse to " ++ show expected) $ 32 | forM_ values $ \strValue -> 33 | it (Text.unpack strValue) $ 34 | parseYesNo strValue `shouldBe` expected 35 | -------------------------------------------------------------------------------- /test/Test/Iris/Tool.hs: -------------------------------------------------------------------------------- 1 | module Test.Iris.Tool (toolSpec) where 2 | 3 | import Data.Text (Text) 4 | import Data.Version (Version, makeVersion, parseVersion) 5 | import Test.Hspec (Spec, describe, it, shouldReturn, shouldSatisfy, shouldThrow) 6 | import Text.ParserCombinators.ReadP (readP_to_S) 7 | 8 | import Iris.Tool ( 9 | Tool (..), 10 | ToolCheckError (..), 11 | ToolCheckException (..), 12 | ToolCheckResult (..), 13 | ToolSelector (..), 14 | checkTool, 15 | defaultToolSelector, 16 | need, 17 | ) 18 | 19 | import qualified Data.Text as Text 20 | 21 | ghc100 :: Tool 22 | ghc100 = 23 | "ghc" 24 | { toolSelector = 25 | Just 26 | defaultToolSelector 27 | { toolSelectorFunction = \version -> case getVersion version of 28 | Nothing -> False 29 | Just v -> v >= makeVersion [100] 30 | , toolSelectorVersionArg = Just "--numeric-version" 31 | } 32 | } 33 | 34 | toolSpec :: Spec 35 | toolSpec = describe "Tool" $ do 36 | it "should find 'curl'" $ do 37 | checkTool "curl" `shouldReturn` ToolOk 38 | 39 | it "shouldn't find 'xxx_unknown_executable'" $ do 40 | checkTool "xxx_unknown_executable" 41 | `shouldReturn` ToolCheckError (ToolNotFound "xxx_unknown_executable") 42 | 43 | it "shouldn't find 'ghc' version 100" $ do 44 | let isToolWrongVersion :: ToolCheckResult -> Bool 45 | isToolWrongVersion (ToolCheckError (ToolWrongVersion _)) = True 46 | isToolWrongVersion _other = False 47 | 48 | checkTool ghc100 >>= (`shouldSatisfy` isToolWrongVersion) 49 | 50 | it "should not fail when 'need'ing 'curl'" (need ["curl"] :: IO ()) 51 | 52 | it "should fail when 'need'ing not found tools" $ do 53 | let tool = "xxx_unknown_executable" 54 | let expectedExceptionSel (err :: ToolCheckException) = case err of 55 | ToolCheckException (ToolNotFound e) -> e == tool 56 | _other -> False 57 | 58 | need [Tool tool Nothing] `shouldThrow` expectedExceptionSel 59 | 60 | it "should fail when 'need'ing tools with wrong version" $ do 61 | let expectedExceptionSel (err :: ToolCheckException) = case err of 62 | ToolCheckException (ToolWrongVersion _) -> True 63 | _other -> False 64 | need [ghc100] `shouldThrow` expectedExceptionSel 65 | 66 | getVersion :: Text -> Maybe Version 67 | getVersion = extractVersion . readP_to_S parseVersion . Text.unpack 68 | where 69 | extractVersion :: [(Version, String)] -> Maybe Version 70 | extractVersion [(version, "")] = Just version 71 | extractVersion _ = Nothing 72 | --------------------------------------------------------------------------------