question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Unexpected reset behavior

See original GitHub issue

With this code:

const kleur = require('kleur')

console.log(kleur.red(kleur.reset('a' + kleur.blue('b') + 'c')))

The result is that the last character logged is unexpectedly red, when it should be default (black):

Screen Shot 2021-01-21 at 6 33 04 pm

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
lukeedcommented, Jan 22, 2021

You’re welcome. I’ll try a different way, without HTML references because it doesn’t actually share any behaviors 😃 I chose it on a whim just to show the nesting hierarchy, but I think the inline comments might be better.

So, RESET (when caps, refers to ANSI) terminates all style codes when encountered. For this reason, it has no close-code pairing. This makes it unique from the others in that everything else is “apply until X” whereas RESET is just “apply”. In other words, RED will continue applying RED until its assigned close-code is found, BOLD still applies BOLD until closed. RESET doesn’t have an “until” clause. It applies at that point, immediately closing all previously-open windows:

k.red(' RED ' + k.reset(' RESET ') + ' RED?' );
//=> '\x1B[31m RED \x1B[0m RESET \x1B[0m RED?\x1B[39m'
//=> '\x1B[31m RED \x1B[0m RESET \x1B[0m RED?\x1B[39m'
//=>      +R         RESET        RESET         -R
// (effect)           -R

However, ANSI codes of the same type share close codes. Because of this, when nesting colors, your RED has to re-initialize itself on the back of any close-code occurrences it finds within the payload. If it doesn’t, then RED terminates once the other color does too:

k.red('RED ' + k.blue('BLUE') + ' RED?')
// CURRENT (Correct)
// => '\x1B[31mRED \x1B[34mBLUE\x1B[39m\x1B[31m RED?\x1B[39m'
// =>      +R          +B        -B|-R     +R           -R

// WITHOUT REOPEN (Incorrect)
// => '\x1B[31mRED \x1B[34mBLUE\x1B[39m RED?\x1B[39m'
// =>      +R          +B        -B|-R          -R (R not active)

If we look back at the original issue (I only changed text for visibility) and the step-by-step, now with diagrams:

k.red(
  k.reset(' XXX ' + k.blue(' YYY ') + ' ZZZ ')
);

// What kleur produces, in stages:
// 1)                     "\x1B[34m YYY \x1B[39m"
//                              +B          -B
// 2)         "\x1B[0m XXX \x1B[34m YYY \x1B[39m ZZZ \x1B[0m"
//                RESET         +B          -B         RESET
// 3) "\x1B[31m\x1B[0m XXX \x1B[34m YYY \x1B[39m\x1B[31m ZZZ \x1B[0m\x1B[39m" (final)
//          +R    RESET         +B          -B|-R    +R         RESET   -R
//                 -R                                ^ADDED      -R

We see the same behavior happening. Between (2) and (3), kleur.red is inheriting a payload that already contains the [39m that would close RED as well as the BLUE. Because of that, RED re-inserts itself so as to not break (like the first example). Now, this means that +R is added immediately after the -B|-R – but this so happens to be before the ZZZ text too.

It might be helpful to see the same chain, but replacing RESET with another non-color code to be alongside RED/BLUE:

k.red(
  k.underline(' XXX ' + k.blue(' YYY ') + ' ZZZ ')
);

// What kleur produces, in stages:
// 1)                     "\x1B[34m YYY \x1B[39m"
//                              +B          -B
// 2)         "\x1B[4m XXX \x1B[34m YYY \x1B[39m ZZZ \x1B[24m"
//                 +U           +B          -B           -U
// 3) "\x1B[31m\x1B[4m XXX \x1B[34m YYY \x1B[39m\x1B[31m ZZZ \x1B[24m\x1B[39m" (final)
//          +R     +U           +B          -B|-R     +R          -U      -R
//                                                    ^ADDED

It’s the exact same thing as before – RED still injects itself once BLUE closes (because BLUE also terminates RED) – but there are no surprises here because UNDERLINE is stateful. It underlines until it no longer does.

The “surprise” is that nesting kleur.reset inside of payloads will not (and cannot) act like other wrappers. Everything except RESET is given an open->close window. If we had to go back to bad HTML metaphors, RESET would be like a <br/>… but only if using <br/> inside other <b>, <strong>, <em>, …etc tags caused them to self-close and reset cleanly. HTML has a lot of self-closing rules, but that isn’t one of them 😄

Changing gears a bit – If the API was a kleur.RESET (constant) instead, I think we’d still have surprises:

k.red(
  k.RESET + ' XXX ' + k.blue(' YYY ') + ' ZZZ '
);

//=> "\x1B[31m\x1B[0m XXX \x1B[34m YYY \x1B[39m\x1B[31m ZZZ \x1B[39m"
//          +R    RESET        +B          -B|-R    +R          -R
//                 -R                               ^ADDED      

While the k.RESET would have reset anything that came before it, the parent k.red() still is told to wrap the payload. Given that, it sees the [39m and reinserts itself to make ZZZ red.

0reactions
jaydensericcommented, Jan 22, 2021

Hope that maybe helps more than last time

Thank you, I appreciate the considerable detail in your comment. I apologise though as I’m struggling to grok all of the explanation.

The ANSI reset code doesn’t have a related end code, but do we need it to? Can’t nested color calls only restore the parent formatting if there is no reset code before it at the same level?

HTML doesn’t need end tags for some things, like <li>. It can automatically close the last one when it encounters the next one:

https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#optional-tags

Could a similar concept be applied to an implementation here for reset nesting?

If there truly is no solution, then kleur.reset() should probably not be a chainable/nestable function like the other color and formatting options. It should just be removed from the API, or perhaps exist as a standalone function or constant for manual insertion. Is the rule that kleur.reset() can be nested inside other kleur functions, but not the other way around? Could misuse be detected and throw an error?

Read more comments on GitHub >

github_iconTop Results From Across the Web

When Unexpected Behavior Occurs
1. Reminder - My teacher will give me a reminder of what the expected behavior is. · 2. Reset - When a reminder...
Read more >
Unexpected Reset - a courageous guide to intentional living
Unexpected Reset's purpose is to revive our senses and look critically at why we do what we do and question if we have...
Read more >
Behavior Reset Teaching Resources - TPT
RESET : Responding to Behaviors​​ When unexpected behaviors happen, it can be hard to bring children back to a place where they feel...
Read more >
ESXi host platform reset behavior with Intel TPM/TXT's reset ...
The error message is the expected behavior when platform is reset in an ungraceful manner and BIOS does not implement Intel's desired workflow. ......
Read more >
Unusual behavior in Dreamweaver? Try restoring preferences.
... to default settings to fix issues, crashes, errors, and other unexpected behavior. ... Dreamweaver 2014.1 and later - Reset preferences.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found