Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable themes (#852) #862

Merged
merged 19 commits into from
Nov 20, 2023
Merged

feat: enable themes (#852) #862

merged 19 commits into from
Nov 20, 2023

Conversation

mdonnalley
Copy link
Contributor

@mdonnalley mdonnalley commented Nov 9, 2023

Merge #852 to main

* feat: create type for storing theme colors

* feat: include color and @types/color, and simiplify theme schema

* feat: set theme's default colors to white

* feat: set FORCE_COLOR to 0||3 to follow the NO_COLOR manifest

* feat: create DEFAULT_THEME to store default colors

* revert: remove NO_COLOR manifest

* feat: add new theme variables to style $, flag, and flag options

* feat: add color to section headers

* feat: configure default colors

* feat: add colors for bin, command summary and version

* feat: topics, commands, bin, version, sections, dollar sign are colorized

* feat: configure default colors

* feat: add feature flag to enabled/disable theme

* feat: add colorize function to simplify the way colors are added to strings

* feat: change all chalk.hex calls to colorize

* feat: configure OCLIF_ENABLE_THEME to have precence over PJSON prop

* feat: all theme colors are optional

* fix: error TS2322: Type 'string' is not assignable to type 'boolean'

* fix: error TS2345: arg of type 'Color<ColorParam>|undefined' not assignable to 'Color<ColorParam>'

* fix: runtime error TypeError: color.hex is not a function

* fix:  this.pjson.oclif.enableTheme was not being evaluated when OCLIF_ENABLE_THEME was unset

* chore(deps): update yarn.lock

* refactor: simplified code removing OCLIF_ENABLE_THEME constant

* fix: command summary was not changing its color when running the root command

* feat: theme is now read from ~/config/<CLI>/theme.json if one exists

* fix: add colors to other ARGUMENTS, EXAMPLES, DESCRIPTION, FLAGS DESCRIPTIONS sections

* feat: add color token for aliases

* fix(test): change template strings to avoid expected results printing bin twice

* fix(test): add a missing parenthesis back so that all options are wraped by ()

* fix(test): remove single quotes wraping default flag values

* feat: add test:debug script to ease debuging

* refactor: add a constant with all possible THEME_KEYS

* test: add tests to prove parsing untyped json object with color strings work

* chore(package): add new scripts to ease development while writing tests

* revert: remove theme from PJSON because a theme will be loaded from config_dir/<CLI>/theme.json

* feat: add scopedEnvVarBoolean because scopedEnvVarTrue does not consider unset env vars

* feat(test): add tests to prove the behavior that enables theme

* refactor: replace all scopedEnvVarTrue by scopedEnvVarBoolean, which considers unset env variables

* test: add tests to prove the enableTheme behavior

* refactor: simplify parseTheme method

* chore(package): remove localhost:4873 from lock file

* revert: rever scopedEnvVarBoolean back to scopedEnvVarTrue

* test: add tests to prove when this.theme is set

* feat: ensure colorize returns string only

* test: add tests to prove colorize works as expected

* revert: rollback scopedEnvVarTrue as it was before

* refactor: move parseTheme to src/util/util.js

* refactor: make Theme type dinamically based on the values of THEME_KEYS at runtime

* fix: err TS1259: Module /@types/color/index can only be default-imported using 'esModuleInterop'

* revert: remove enableTheme from pjson because we don't want to cli devs to force users to use theme

* revert: remove undefined as a return type for scopedEnvVarTrue

* refactor: remove config.enableTheme
@AllanOricil
Copy link
Contributor

AllanOricil commented Nov 10, 2023

@mdonnalley
I forgot to change http to https on those dependency entries in the log file.
Merge this fix in your branch: #869

@AllanOricil
Copy link
Contributor

@mdonnalley when you release this feature could you please make some sort of "promotion" with my name? Now that I have time, I'm working on open source projects that I have interest in order to increase my chances of leaving this s*** country when my parents are gone. If you can't do it, there is no problem. Thank you in advance.

@mdonnalley
Copy link
Contributor Author

when you release this feature could you please make some sort of "promotion" with my name?

@AllanOricil No problem. You're name will be in the commit history and we'll mention your name in the sf release notes once we integrate this feature there. Does that work for you?

@AllanOricil
Copy link
Contributor

@mdonnalley I would like to also have my name somewhere on those posts/videos that Salesforce usually release to notify devs about new features

@AllanOricil
Copy link
Contributor

AllanOricil commented Nov 13, 2023

@mdonnalley you removed the test that tests that a certain key is present in the parsed theme. If a developer remove alias from Theme all tests will pass and we wont be able to see a regression. Those tests were there to prove that the key can not be removed from Theme without causing a regression

@mdonnalley
Copy link
Contributor Author

@AllanOricil Are you referring to these tests?

If so, I don't see what value that provides that this test doesn't. They're the exact same tests except one has more properties in the parsed theme

@AllanOricil
Copy link
Contributor

AllanOricil commented Nov 13, 2023

@AllanOricil Are you referring to these tests?

If so, I don't see what value that provides that this test doesn't. They're the exact same tests except one has more properties in the parsed theme

@mdonnalley in the first, if you remove alias from Theme, the code will not compile with a type error. In the second, because there is no ref to the key name, the code compiles. The benefit is that a dev will see that removing alias from Theme causes a regression and he can must not do it. Or Im saying nonsense. Let me test it. one moment.

EDIT: I was actually wrong. The code does compile, but 1 test fail when removing a key from THEME_KEYS, which is good to tell developers that a regression occurred and therefore a key must not be removed. Sorry for the mistake.

@AllanOricil
Copy link
Contributor

@mdonnalley look, when I removed alias from THEME_KEYS, 1 single test failed so that a developer can know exactly the root cause of the problem. It is like if it was a real world experiment, you change 1 variable, it changes 1 single behaviour.

image

@AllanOricil
Copy link
Contributor

@mdonnalley Yes, with the current code there is no way to know if another developer deleted any of the mandatory Theme keys because there is no tests to test the which keys are valid in the Theme specification.

Comment on lines 4 to 41
export const STANDARD_CHALK = [
'white',
'black',
'blue',
'yellow',
'green',
'red',
'magenta',
'cyan',
'gray',
'blackBright',
'redBright',
'greenBright',
'yellowBright',
'blueBright',
'magentaBright',
'cyanBright',
'whiteBright',
'bgBlack',
'bgRed',
'bgGreen',
'bgYellow',
'bgBlue',
'bgMagenta',
'bgCyan',
'bgWhite',
'bgGray',
'bgBlackBright',
'bgRedBright',
'bgGreenBright',
'bgYellowBright',
'bgBlueBright',
'bgMagentaBright',
'bgCyanBright',
'bgWhiteBright',
] as const

export type StandardChalk = (typeof STANDARD_CHALK)[number]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to pass 'red' to color without a problem . What happens if this is not in the code?

Copy link
Contributor

@AllanOricil AllanOricil Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, I tested and there are some colors, like 'blackBright' that can't be parsed by the color package. Really good finding!

For those who want to see a proof
image

Comment on lines 67 to 81
alias: 'cyan',
bin: 'cyan',
command: 'cyan',
commandSummary: 'cyan',
dollarSign: 'cyan',
flag: 'cyan',
flagDefaultValue: 'cyan',
flagOptions: 'cyan',
flagRequired: 'cyan',
flagSeparator: 'cyan',
sectionDescription: 'cyan',
sectionHeader: 'cyan',
topic: 'cyan',
version: 'cyan',
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cyan is supported by the colors package. Use a color, like blackBright, that would fail to be parsed without STANDARD_CHALK in order to make this a non false positive test.

Here is a test where I discovered that blackBright isn't supported by the color package
image

}

export function colorize(color: Color | StandardChalk | undefined, text: string): string {
if (isStandardChalk(color)) return standardChalk(color, text)
Copy link
Contributor

@AllanOricil AllanOricil Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove this function and just use return chalk[color](text)

Using a theme with standard chalk colors

image

Comment on lines 161 to 162
export function parseTheme(untypedTheme: Record<string, string>): Theme {
return Object.fromEntries(Object.entries(untypedTheme).map(([key, value]) => [key, getColor(value)]))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the theme feature should not waste time or memory storing tokens its spec does not support (strict). Its spec supports these tokens:

export const THEME_KEYS = [
  'alias',
  'bin',
  'command',
  'commandSummary',
  'dollarSign',
  'flag',
  'flagDefaultValue',
  'flagOptions',
  'flagRequired',
  'flagSeparator',
  'flagType',
  'sectionDescription',
  'sectionHeader',
  'topic',
  'version',
]

Then you have to add these tests back because they prove the independency and usage of each token of Theme Spec. We need it so that when tokens are changed we know exactly the regressions the change made. For example, if I remove the alias token and run the tests, I will see that its test failed, and this will show me a regression:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AllanOricil I want to leave the possibility for people to be able to use the theme beyond just the help output.

For instance, someone might want to have a warningMsg color that gets used on all their warning messages.

export default class MyCommand extends Command {
  public async run(): Promise<void> {
    this.logToStderr(colorize(this.config.theme.warningMsg, 'hello world'))
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdonnalley we would change logToStderr to the following piece of code so that they can keep using this.logToStderr without updating their code. The theme.json is what controls the color changing.

  public static logToStderr(format?: string, ...args: string[]): void {
    write.stderr(colorize(this.config?.theme?.warningMsg, utilFormat(format, ...args) + '\n'))
  }

and a new token would be added to the Theme Spec

const THEME_KEYS = {
  ...,
  "warningMsg"
}

Copy link
Contributor

@AllanOricil AllanOricil Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this.config?.theme?.warningMsg does not exist, this.logToStderr will output what is currently outputing. It would not require any code updates in CLIs using @oclif/core.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If CLI developers want to overwrite what the theme.json says, they can do this

export default class MyCommand extends Command {
  public async run(): Promise<void> {
    this.config.theme.warningMsg = getColor('brightBlack')
    this.logToStderr('hello world')
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and for logToStderr, a better Theme Key is stderrMessage,

  public static logToStderr(format?: string, ...args: string[]): void {
    write.stderr(colorize(this.config?.theme?.stderrMessage, utilFormat(format, ...args) + '\n'))
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a very poor assumption that everyone would be using logToStderr to only log warning messages. But my point was that by allowing any key in themes.json we give CLI developers the ability to create a more extensive theme for their users. I have no interest in standardizing themes for every single oclif CLI

Copy link
Contributor

@AllanOricil AllanOricil Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going back to what I said earlier, there are no tests to "prove" the Theme Spec the Core Specifies...

If the core is using certain keys everywhere in its code, there must exist tests to prove each single one of these keys....

so that when someone deletes or changes the key, a test failure happens to show the Regression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks for adding a test back. Now it is working again as I specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export type ThemeKey = (typeof THEME_KEYS)[number]

export type Theme = {
[key: string | ThemeKey]: Color | StandardChalk | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see a use case for this. CLI developers can extend the oclif's Theme Spec with their own theme specific keys so that plugins built with their own CLI API can take advantage of their own Theme Spec, which is just a combination of Oclif + CLI Theme Specs. Makes sense now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't Theme be a interface to allow it to be extensible in typescript world?

@AllanOricil
Copy link
Contributor

AllanOricil commented Nov 14, 2023

I don't see anything else I would change, so I've approved it. And I can't see why tests are failing to help fix it. I will go do something else.

Comment on lines +4 to +44
export const STANDARD_CHALK = [
'white',
'black',
'blue',
'yellow',
'green',
'red',
'magenta',
'cyan',
'gray',
'blackBright',
'redBright',
'greenBright',
'yellowBright',
'blueBright',
'magentaBright',
'cyanBright',
'whiteBright',
'bgBlack',
'bgRed',
'bgGreen',
'bgYellow',
'bgBlue',
'bgMagenta',
'bgCyan',
'bgWhite',
'bgGray',
'bgBlackBright',
'bgRedBright',
'bgGreenBright',
'bgYellowBright',
'bgBlueBright',
'bgMagentaBright',
'bgCyanBright',
'bgWhiteBright',
'bold',
'underline',
'dim',
'italic',
'strikethrough',
] as const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't these exports work? I did not check their values, but by looking at the variable names it seems to be what we need

https://github.com/chalk/chalk/blob/main/source/index.js#L207-L218

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind. That is for v5. V4 does not have those exports, unfortunately

Copy link
Contributor

@AllanOricil AllanOricil Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdonnalley

In v4 I think you could use

Object.getOwnPropertyNames(chalk)

edited: we don't have access to Chalk class, but it is possible to use an instance of it

Based on the following, all styles are direct props from the chalk prototype:

https://github.com/chalk/chalk/blob/95d74cbe8d3df3674dec1445a4608d3288d8b73c/source/index.js#L222

Comment on lines 402 to 418
public async loadThemes(): Promise<{
file: string
activeTheme: Theme | undefined
themes: Themes | undefined
}> {
const themesFile = this.pjson.oclif.themesFile
? resolve(this.root, this.pjson.oclif.themesFile)
: resolve(this.configDir, 'themes.json')
const themes = await safeReadJson<Themes>(themesFile)
const activeTheme = themes ? parseTheme(themes) : undefined
return {
activeTheme,
file: themesFile,
themes,
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why loading all themes in memory if only one is used at a time

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not answer the question. What is the use case to have all themes in memory?

@AllanOricil
Copy link
Contributor

I envisioned and started this feature and you took it over and started to make decisions without discussing with me. This is extremely unprofessional and it will be registered here for other people to see.

@mdonnalley
Copy link
Contributor Author

I envisioned and started this feature and you took it over and started to make decisions without discussing with me. This is extremely unprofessional and it will be registered here for other people to see.

It's an open PR - no decisions have been made. Your feedback is welcome, including on the plugin.

Your reviews and comments on this PR have been incredibly helpful thus far - I'm sure you've noticed that many of the commits here are addressing your feedback. From my perspective, this has been a collaborative effort.

The current implementation in this PR and the plugin-theme PR was discussed here. As Cristian mentioned in his comment, we're not willing to take on the complexity of having themes be installable via npm so we're looking for a more file-based solution. We'd love if you could add your thoughts there so that we can starting working towards a solution that we're all happy with

@AllanOricil
Copy link
Contributor

Hi @mdonnalley sorry for what I said earlier. From time to time I have stupid emotional outages and I end up saying stupid things because my mind starts to think about things that doesnt exist. For example, I think that Im losing something and being left behind, or I get envy, again for no reason. I take some medication but it just doesn't work well sometimes. You can continue this and follow your ideas. You actually did most of the good decisions. I hope to use it when it is finished. Again, sorry.

@mdonnalley mdonnalley merged commit da2bd5b into main Nov 20, 2023
35 of 37 checks passed
@mdonnalley mdonnalley deleted the mdonnalley/852 branch November 20, 2023 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants