[RFC] Ability to disable and enable colouring
It would be nice if colourista could support automatic disabling and enabling of colouring, so we can easily disable/enable colours in our code. We want to have a solution that satisfies the following requirements:
- Allows specifying the colour only in a single place in the application.
- Colours the text by default, if no option is specified.
- Doesn't involve code duplication.
- Doesn't require code recompilation to change colouring settings.
I'll describe one possible solution below.
-XImplicitParams
One way to do this is to use the ImplicitParams Haskell language extension. It contains the following parts:
- Implement simple enum to control colouring mode.
data ColourMode = EnableColour | DisableColour - Introduce a constraint with implicit params:
type HasColourMode = (?colourMode :: ColourMode) - Patch each function to pattern-math on a colour:
withColourMode :: (HasColourMode, IsString str) => str -> str withColourMode = case ?colourMode of EnableColour -> str DisableColour -> "" red :: (HasColourMode, IsString str) => str red = withColourMode $ fromString $ setSGRCode [SetColor Foreground Vivid Red] {-# SPECIALIZE red :: HasColourMode => String #-} {-# SPECIALIZE red :: HasColourMode => Text #-} {-# SPECIALIZE red :: HasColourMode => ByteString #-} - Implement magic instance described in this blog post to make
EnableColourdefault:-- ?color = EnableColor instance IP "colourMode" ColourMode where ip = EnableColour
Cons and pros
Implementation cost: patch each function.
Pros:
- All existing code should work without any changes.
- We can easily disable/enable colouring by defining a single variable in a single module after we parse
--no-colouroption or something like this.
Cons
- Each function that performs colouring or calls colouring function should add
HasColourModeconstraint. - You don't have compile-time guarantees if you forget to add such constraint somewhere.
- This involves using non-common GHC feature.
Additionally we need to add tests for this:
- [ ] Check that disabling colour mode actually disables colour codes
An alternative solution is to use the reflection for this purpose. It's similar to ImplicitParams. I've spent some time diving into this library, and I'm going to describe the approach and provide a comparison with ImplicitParams.
How to implement?
- Add the
reflectionlibrary to dependencies. - Implement simple enum for colouring mode (same as in the previous approach):
data ColourMode = EnableColour | DisableColour - Helper function:
withColourMode :: (Reifies s ColourMode, IsString str) => str -> str withColourMode = case (reflect $ Proxy @mode) of EnableColour -> str DisableColour -> "" - Use helper function this:
formatWith
:: (Reifies s ColourMode, IsString str, Semigroup str)
=> [str]
-> str
-> str
formatWith formatting str = case formatting of
[] -> str
x:xs -> withColourMode @s (sconcat (x :| xs)) <> str <> withColourMode @s reset
Costs of implementation
- Pros: :shrug:
- Cons
- Extra dependency
- SPECIALIZE doesn't work anymore
- Requires to use AllowAmbiguousTypes ann pass the parameter explicitly actually
- Still not clear how to provide the initial
ColourModeand pass it implicitly through
Conclusion
After looking at the reflection package, I came to the conclusion that its primary usage is constraint of some typeclasses where you want typeclasses to depend on some runtime data. This is not our case, so ImplicitParams should work just fine :+1:
Alternative ideas
We can skip step 4 in the solution with the ImplicitParams extensions, so each function that uses colouring or calls a function that uses colouring must specify HasColourMode constraint. With this approach, some code needs to be changed (need to try on some project to see how much code needs to be changed, hard to estimate), but we also gain some extra type-safety and guarantees that our explicitly specified value of ColourMode actually will be used.
Thanks for such detailed analysis and overview, @chshersh ! :brain: :tada:
I would definitely like to try the ImplicitParameters options first with the test phase on an actual (and preferably not the smallest one). The plan for that is very clear already :+1:
I am a bit sceptical as well about the reflection library in our use case, seems like the cost is much bigger.
Btw, we can continue documenting decisions under this issue, it is super helpful :pray:
The implementation was reverted due to a bug in GHC and not reliable behaviour.