[GH-ISSUE #619] Dynamic color stack #454

Closed
opened 2026-03-04 01:05:09 +03:00 by kerem · 1 comment
Owner

Originally created by @marcelocantos on GitHub (Jun 12, 2021).
Original GitHub issue: https://github.com/rivo/tview/issues/619

It would be great to be able to manage dynamic color attributes with a stack, allowing pushing and popping.

Motivation

It is difficult to functionally decompose elements of a complex render. Let's say I want to underline a section of text, but that section itself has a bolded portion:

reportProblem(fmt.Sprintf("Function [blue::b]%s[::-] panicked", fname))

func reportProblem(problem string) {
	problem.SetText(fmt.Sprintf("Problem reported: [::u]%s", problem))
}

Unfortunately, the above will produce the following output, wherein the underline terminates prematurely (strike-through used in place of underline due to GitHub markdown limitations, and you'll just have to imagine the blue foo):

Problem reported: Function foo panicked

With a stack-based system (using the design proposed below) the above code would be:

reportProblem("Function [<blue::b]%s[::>] panicked", fname)

And the output would be as intended:

Problem reported: Function foo panicked

Proposed design

Extend the current […:…:…] syntax as follows:

  1. [<fg:bg:flags]: Push the current color to the stack and set the current color to the given values, with the attributes set to the union of the current flags and specified flags (if the current setting for bold is on, then [<::u] will leave it on).
  2. [::>]: Pop the most recently pushed color and use it to set the current color.
usage example
Nest color changes [red::b]Let's [<orange::u]do[::>] this!
Batch a number of changes under a single stack operation Colors: [<red::]R[green::]G[blue::]B[::>]
Temporarily escape out of a mode [<red::]red [<-::]default[::>] red again[::>]

Since this is a breaking change to the syntax, an opt-in method might be needed, something like view.SetDynamicColorWithStack(true), which would imply view.SetDynamicColor(true) (likewise, mutatis mutandis, when passing false).

Other considerations

  1. Clearing individual flags might be desirable in some cases. The above design doesn't solve this. Perhaps, riffing on #420, ! could turn off the flags that follow it: [::rbl]there is [<::u!rb]little to be said[::>] about the matter ([<::u!rb] preserves l, turns on u and turns off r and b).
  2. Whether the current syntax should also be changed to set flags individually is uncertain, though I'm leaning towards leaving them as is. Since stacks and fine-grained state management both arise from similar motivations, they sort of go hand-in-hand.

Other designs considered

  1. #420 proposes better control over turning attributes off. This would make the example easier to express as "Function [blue::b]%s[-::!b] panicked", but it wouldn't handle the case that the preceding color was already set to a non-default value: "Problem reported: [red::bu]%s".
  2. Since we're breaking compatibility anyway, the closing tag could be simpler: [>]. However, the proposed syntax feels more balanced.
  3. A slight variation is [fg:bg:flags<]text[>::], but this isn't as mnemonically clear, since it places the new color outside the <…> pair.
    • A slight variation of the slight variation is [fg:bg:flags(]text[)::], but the (…) doesn't stand out as well, due to the inner ]…[ pair.
  4. The default behaviour for the current syntax could be changed to pushing, and popping could be implement via a pseudo-color: [pop::].
    • This is a non-breaking change.
      • There is a small qualifier: [pop::] is currently equivalent to [-::]. But I'm guessing there's no existing code anywhere that relies on the current behaviour of [pop::].
    • It wouldn't affect existing code, which never pops anything, and therefore wouldn't strictly require an opt-in method.
    • It would perhaps result in excessive stack growth in pathological cases, though obviously the stack would be scoped to a single render and wouldn't just grow forever.
    • It doesn't address the challenge of setting flags independently of each other.
Originally created by @marcelocantos on GitHub (Jun 12, 2021). Original GitHub issue: https://github.com/rivo/tview/issues/619 It would be great to be able to manage dynamic color attributes with a stack, allowing pushing and popping. # Motivation It is difficult to functionally decompose elements of a complex render. Let's say I want to underline a section of text, but that section itself has a bolded portion: ```go reportProblem(fmt.Sprintf("Function [blue::b]%s[::-] panicked", fname)) ⋮ func reportProblem(problem string) { problem.SetText(fmt.Sprintf("Problem reported: [::u]%s", problem)) } ``` Unfortunately, the above will produce the following output, wherein the underline terminates prematurely (strike-through used in place of underline due to GitHub markdown limitations, and you'll just have to imagine the blue `foo`): > Problem reported: ~~Function <b>foo</b>~~ panicked With a stack-based system (using the design proposed below) the above code would be: ```go reportProblem("Function [<blue::b]%s[::>] panicked", fname) ``` And the output would be as intended: > Problem reported: ~~Function <b>foo</b> panicked~~ # Proposed design Extend the current `[…:…:…]` syntax as follows: 1. `[<fg:bg:flags]`: Push the current color to the stack and set the current color to the given values, with the attributes set to the union of the current flags and specified flags (if the current setting for bold is on, then `[<::u]` will leave it on). 2. `[::>]`: Pop the most recently pushed color and use it to set the current color. | usage | example | |-|-| | Nest color changes | `[red::b]Let's [<orange::u]do[::>] this!` | | Batch a number of changes under a single stack operation | `Colors: [<red::]R[green::]G[blue::]B[::>]` | | Temporarily escape out of a mode | `[<red::]red [<-::]default[::>] red again[::>]` | Since this is a breaking change to the syntax, an opt-in method might be needed, something like `view.SetDynamicColorWithStack(true)`, which would imply `view.SetDynamicColor(true)` (likewise, _mutatis mutandis_, when passing `false`). ## Other considerations 1. Clearing individual flags might be desirable in some cases. The above design doesn't solve this. Perhaps, riffing on #420, `!` could turn off the flags that follow it: `[::rbl]there is [<::u!rb]little to be said[::>] about the matter` (`[<::u!rb]` preserves `l`, turns on `u` and turns off `r` and `b`). 2. Whether the current syntax should also be changed to set flags individually is uncertain, though I'm leaning towards leaving them as is. Since stacks and fine-grained state management both arise from similar motivations, they sort of go hand-in-hand. # Other designs considered 1. #420 proposes better control over turning attributes off. This would make the example easier to express as `"Function [blue::b]%s[-::!b] panicked"`, but it wouldn't handle the case that the preceding color was already set to a non-default value: `"Problem reported: [red::bu]%s"`. 1. Since we're breaking compatibility anyway, the closing tag could be simpler: `[>]`. However, the proposed syntax feels more balanced. 1. A slight variation is `[fg:bg:flags<]text[>::]`, but this isn't as mnemonically clear, since it places the new color outside the `<…>` pair. - A slight variation of the slight variation is `[fg:bg:flags(]text[)::]`, but the `(…)` doesn't stand out as well, due to the inner `]…[` pair. 1. The default behaviour for the current syntax could be changed to pushing, and popping could be implement via a pseudo-color: `[pop::]`. - This is a non-breaking change. - There is a small qualifier: `[pop::]` is currently equivalent to `[-::]`. But I'm guessing there's no existing code anywhere that relies on the current behaviour of `[pop::]`. - It wouldn't affect existing code, which never pops anything, and therefore wouldn't strictly require an opt-in method. - It would perhaps result in excessive stack growth in pathological cases, though obviously the stack would be scoped to a single render and wouldn't just grow forever. - It doesn't address the challenge of setting flags independently of each other.
kerem closed this issue 2026-03-04 01:05:09 +03:00
Author
Owner

@rivo commented on GitHub (Nov 9, 2021):

I appreciate the detailed discussion. The current implementation is already quite complex in quite a few places. For example, in TextView, I need to determine and store the style at the start of each line. With this proposal, it becomes more complex (and slower and more memory intensive): I'd have to store the entire stack for each line. Similarly, things get more difficult in other places, e.g. consider a right-aligned title with stacked colors that does not fit into its available space: I have to be able to split a string into a substring and preserve the stack at its boundaries. It's not impossible but the effort to make this work may cause this issue to land fairly down the todo pile. We're getting into HTML-like territory here and as mentioned elsewhere, tview wasn't meant to replicate a browser's functionality.

I'm also not quite sure if the syntax is very accessible. I mean, [<::u!rb]little to be said[::>] looks quite complex to me. And from this part alone, I cannot tell what "little to be said" will look like because it also depends on everything that comes before.

If this is something that is definitely needed by many, I would rather tend to add a "converter" to the package, something similar to TranslateANSI() which receives text with a given syntax and translates it into colour tags used in tview, basically some kind of Flatten() function. What that syntax is supposed to be is up for discussion, it would probably make sense to find something that is already widespread. (E.g. Markdown stacks style changes, but it doesn't include background/foreground colour elements out of the box.)

<!-- gh-comment-id:964259795 --> @rivo commented on GitHub (Nov 9, 2021): I appreciate the detailed discussion. The current implementation is already quite complex in quite a few places. For example, in `TextView`, I need to determine and store the style at the start of each line. With this proposal, it becomes more complex (and slower and more memory intensive): I'd have to store the entire stack for each line. Similarly, things get more difficult in other places, e.g. consider a right-aligned title with stacked colors that does not fit into its available space: I have to be able to split a string into a substring and preserve the stack at its boundaries. It's not impossible but the effort to make this work may cause this issue to land fairly down the todo pile. We're getting into HTML-like territory here and as mentioned elsewhere, `tview` wasn't meant to replicate a browser's functionality. I'm also not quite sure if the syntax is very accessible. I mean, `[<::u!rb]little to be said[::>]` looks quite complex to me. And from this part alone, I cannot tell what "little to be said" will look like because it also depends on everything that comes before. If this is something that is definitely needed by many, I would rather tend to add a "converter" to the package, something similar to [`TranslateANSI()`](https://pkg.go.dev/github.com/rivo/tview#TranslateANSI) which receives text with a given syntax and translates it into colour tags used in `tview`, basically some kind of `Flatten()` function. What that syntax is supposed to be is up for discussion, it would probably make sense to find something that is already widespread. (E.g. Markdown stacks style changes, but it doesn't include background/foreground colour elements out of the box.)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#454
No description provided.