├── .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 | [](https://github.com/chshersh/iris/actions)
4 | [](https://hackage.haskell.org/package/iris)
5 | [](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 |
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 | 
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 |
--------------------------------------------------------------------------------