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

Enable themes #852

Merged
merged 52 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b4a49a7
feat: create type for storing theme colors
AllanOricil Nov 5, 2023
095c6a7
feat: include color and @types/color, and simiplify theme schema
AllanOricil Nov 6, 2023
ad5f324
feat: set theme's default colors to white
AllanOricil Nov 6, 2023
26a418a
feat: set FORCE_COLOR to 0||3 to follow the NO_COLOR manifest
AllanOricil Nov 6, 2023
4c587b9
feat: create DEFAULT_THEME to store default colors
AllanOricil Nov 7, 2023
f7c5912
revert: remove NO_COLOR manifest
AllanOricil Nov 7, 2023
8dbaebd
feat: add new theme variables to style $, flag, and flag options
AllanOricil Nov 7, 2023
cbdf4ca
feat: add color to section headers
AllanOricil Nov 7, 2023
0e823f2
feat: configure default colors
AllanOricil Nov 7, 2023
0f91695
feat: add colors for bin, command summary and version
AllanOricil Nov 7, 2023
94ea7af
feat: topics, commands, bin, version, sections, dollar sign are color…
AllanOricil Nov 7, 2023
3208ea3
feat: configure default colors
AllanOricil Nov 7, 2023
20a150c
feat: add feature flag to enabled/disable theme
AllanOricil Nov 7, 2023
07d7501
feat: add colorize function to simplify the way colors are added to s…
AllanOricil Nov 7, 2023
1e91233
feat: change all chalk.hex calls to colorize
AllanOricil Nov 7, 2023
27987e4
feat: configure OCLIF_ENABLE_THEME to have precence over PJSON prop
AllanOricil Nov 7, 2023
180b179
feat: all theme colors are optional
AllanOricil Nov 7, 2023
8d561f7
fix: error TS2322: Type 'string' is not assignable to type 'boolean'
AllanOricil Nov 7, 2023
22d3e6d
fix: error TS2345: arg of type 'Color<ColorParam>|undefined' not assi…
AllanOricil Nov 7, 2023
39bf5b2
fix: runtime error TypeError: color.hex is not a function
AllanOricil Nov 7, 2023
7be8136
fix: this.pjson.oclif.enableTheme was not being evaluated when OCLIF…
AllanOricil Nov 7, 2023
653c1c2
chore(deps): update yarn.lock
AllanOricil Nov 7, 2023
6673ec5
refactor: simplified code removing OCLIF_ENABLE_THEME constant
AllanOricil Nov 7, 2023
80421ee
fix: command summary was not changing its color when running the root…
AllanOricil Nov 7, 2023
fd3d17e
feat: theme is now read from ~/config/<CLI>/theme.json if one exists
AllanOricil Nov 8, 2023
f6c7744
fix: add colors to other ARGUMENTS, EXAMPLES, DESCRIPTION, FLAGS DESC…
AllanOricil Nov 8, 2023
7a35ca7
feat: add color token for aliases
AllanOricil Nov 8, 2023
92ff5ff
fix(test): change template strings to avoid expected results printing…
AllanOricil Nov 8, 2023
7691535
fix(test): add a missing parenthesis back so that all options are wra…
AllanOricil Nov 8, 2023
c252a13
fix(test): remove single quotes wraping default flag values
AllanOricil Nov 8, 2023
9f8591b
feat: add test:debug script to ease debuging
AllanOricil Nov 8, 2023
02fc7aa
refactor: add a constant with all possible THEME_KEYS
AllanOricil Nov 8, 2023
6b36522
test: add tests to prove parsing untyped json object with color strin…
AllanOricil Nov 8, 2023
66c7cde
chore(package): add new scripts to ease development while writing tests
AllanOricil Nov 8, 2023
1a6b462
revert: remove theme from PJSON because a theme will be loaded from c…
AllanOricil Nov 8, 2023
7124de1
feat: add scopedEnvVarBoolean because scopedEnvVarTrue does not consi…
AllanOricil Nov 8, 2023
dce013d
feat(test): add tests to prove the behavior that enables theme
AllanOricil Nov 8, 2023
2834517
refactor: replace all scopedEnvVarTrue by scopedEnvVarBoolean, which …
AllanOricil Nov 8, 2023
c8d671d
test: add tests to prove the enableTheme behavior
AllanOricil Nov 8, 2023
24ca8f5
refactor: simplify parseTheme method
AllanOricil Nov 9, 2023
84db405
chore(package): remove localhost:4873 from lock file
AllanOricil Nov 9, 2023
4927e47
revert: rever scopedEnvVarBoolean back to scopedEnvVarTrue
AllanOricil Nov 9, 2023
c7488bb
test: add tests to prove when this.theme is set
AllanOricil Nov 9, 2023
37588ce
feat: ensure colorize returns string only
AllanOricil Nov 9, 2023
714d989
test: add tests to prove colorize works as expected
AllanOricil Nov 9, 2023
fa1f414
revert: rollback scopedEnvVarTrue as it was before
AllanOricil Nov 9, 2023
2c9da22
refactor: move parseTheme to src/util/util.js
AllanOricil Nov 9, 2023
49a1446
refactor: make Theme type dinamically based on the values of THEME_KE…
AllanOricil Nov 9, 2023
bc369b3
fix: err TS1259: Module /@types/color/index can only be default-impor…
AllanOricil Nov 9, 2023
f9dbcbb
revert: remove enableTheme from pjson because we don't want to cli de…
AllanOricil Nov 9, 2023
caaa353
revert: remove undefined as a return type for scopedEnvVarTrue
AllanOricil Nov 9, 2023
00a6ccd
refactor: remove config.enableTheme
AllanOricil Nov 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"chalk": "^4.1.2",
"clean-stack": "^3.0.1",
"cli-progress": "^3.12.0",
"color": "^4.2.3",
"debug": "^4.3.4",
"ejs": "^3.1.9",
"get-package-type": "^0.1.0",
Expand Down Expand Up @@ -44,6 +45,7 @@
"@types/chai-as-promised": "^7.1.5",
"@types/clean-stack": "^2.1.1",
"@types/cli-progress": "^3.11.0",
"@types/color": "^3.0.5",
"@types/debug": "^4.1.10",
"@types/ejs": "^3.1.3",
"@types/indent-string": "^4.0.1",
Expand Down Expand Up @@ -107,6 +109,7 @@
"access": "public"
},
"scripts": {
"build:dev": "shx rm -rf lib && tsc --sourceMap",
"build": "shx rm -rf lib && tsc",
"commitlint": "commitlint",
"compile": "tsc",
Expand All @@ -117,9 +120,11 @@
"prepare": "husky install",
"pretest": "yarn build && tsc -p test --noEmit --skipLibCheck",
"test:circular-deps": "madge lib/ -c",
"test:debug": "nyc mocha --debug-brk --inspect \"test/**/*.test.ts\"",
"test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --parallel --timeout 1200000",
"test:esm-cjs": "cross-env DEBUG=e2e:* ts-node test/integration/esm-cjs.ts",
"test:perf": "ts-node test/perf/parser.perf.ts",
"test:dev": "nyc mocha \"test/**/*.test.ts\"",
"test": "nyc mocha --forbid-only \"test/**/*.test.ts\""
},
"types": "lib/index.d.ts"
Expand Down
18 changes: 13 additions & 5 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import * as ejs from 'ejs'
import WSL from 'is-wsl'
import {arch, userInfo as osUserInfo, release, tmpdir, type} from 'node:os'
import {join, sep} from 'node:path'
import {join, resolve, sep} from 'node:path'
import {URL, fileURLToPath} from 'node:url'

import {ux} from '../cli-ux'
import {Command} from '../command'
import {CLIError, error, exit, warn} from '../errors'
import {getHelpFlagAdditions} from '../help/util'
import {Hook, Hooks, PJSON, Topic} from '../interfaces'
import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, VersionDetails} from '../interfaces/config'
import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, Theme, VersionDetails} from '../interfaces/config'
import {Plugin as IPlugin, Options} from '../interfaces/plugin'
import {loadWithData} from '../module-loader'
import {OCLIF_MARKER_OWNER, Performance} from '../performance'
import {settings} from '../settings'
import {requireJson} from '../util/fs'
import {requireJson, safeReadJson} from '../util/fs'
import {getHomeDir, getPlatform} from '../util/os'
import {compact, isProd} from '../util/util'
import {compact, isProd, parseTheme} from '../util/util'
import Cache from './cache'
import PluginLoader from './plugin-loader'
import {tsPath} from './ts-node'
Expand Down Expand Up @@ -91,6 +91,7 @@ export class Config implements IConfig {
public plugins: Map<string, IPlugin> = new Map()
public root!: string
public shell!: string
public theme?: Theme
public topicSeparator: ' ' | ':' = ':'
public userAgent!: string
public userPJSON?: PJSON.User
Expand Down Expand Up @@ -312,6 +313,7 @@ export class Config implements IConfig {
if (this.pjson.oclif.topicSeparator && [' ', ':'].includes(this.pjson.oclif.topicSeparator))
this.topicSeparator = this.pjson.oclif.topicSeparator!
if (this.platform === 'win32') this.dirname = this.dirname.replace('/', '\\')

this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}`
this.shell = this._shell()
this.debug = this._debug()
Expand All @@ -325,6 +327,12 @@ export class Config implements IConfig {

this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry

if (!this.scopedEnvVarTrue('DISABLE_THEME')) {
const themeFilePath = resolve(this.configDir, 'theme.json')
const theme = await safeReadJson<Record<string, string>>(themeFilePath)
this.theme = theme ? parseTheme(theme) : undefined
}

this.pjson.oclif.update = this.pjson.oclif.update || {}
this.pjson.oclif.update.node = this.pjson.oclif.update.node || {}
const s3 = this.pjson.oclif.update.s3 || {}
Expand Down Expand Up @@ -605,7 +613,7 @@ export class Config implements IConfig {
}

public scopedEnvVarTrue(k: string): boolean {
AllanOricil marked this conversation as resolved.
Show resolved Hide resolved
const v = process.env[this.scopedEnvVarKeys(k).find((k) => process.env[k]) as string]
const v = this.scopedEnvVar(k)
return v === '1' || v === 'true'
}

Expand Down
67 changes: 43 additions & 24 deletions src/help/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ import {ensureArgObject} from '../util/ensure-arg-object'
import {castArray, compact, sortBy} from '../util/util'
import {DocOpts} from './docopts'
import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter'
import {colorize} from './util'

// Don't use os.EOL because we need to ensure that a string
// written on any platform, that may use \r\n or \n, will be
// split on any platform, not just the os specific EOL at runtime.
const POSSIBLE_LINE_FEED = /\r\n|\n/

let {dim} = chalk

if (process.env.ConEmuANSI === 'ON') {
// eslint-disable-next-line unicorn/consistent-destructuring
dim = chalk.gray
}

export class CommandHelp extends HelpFormatter {
constructor(
public command: Command.Loadable,
Expand All @@ -31,7 +25,15 @@ export class CommandHelp extends HelpFormatter {

protected aliases(aliases: string[] | undefined): string | undefined {
if (!aliases || aliases.length === 0) return
const body = aliases.map((a) => ['$', this.config.bin, a].join(' ')).join('\n')
const body = aliases
.map((a) =>
[
colorize(this.config?.theme?.dollarSign, '$'),
colorize(this.config?.theme?.bin, this.config.bin),
colorize(this.config?.theme?.alias, a),
].join(' '),
)
.join('\n')
return body
}

Expand All @@ -47,9 +49,14 @@ export class CommandHelp extends HelpFormatter {
return args.map((a) => {
const name = a.name.toUpperCase()
let description = a.description || ''
if (a.default) description = `[default: ${a.default}] ${description}`
if (a.options) description = `(${a.options.join('|')}) ${description}`
return [name, description ? dim(description) : undefined]
if (a.default)
description = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}`
if (a.options)
description = `${colorize(this.config?.theme?.flagOptions, `(${a.options.join('|')})`)} ${description}`
return [
colorize(this.config?.theme?.flag, name),
description ? colorize(this.config?.theme?.sectionDescription, description) : undefined,
]
})
}

Expand Down Expand Up @@ -82,7 +89,7 @@ export class CommandHelp extends HelpFormatter {
}

if (description) {
return this.wrap(description.join('\n'))
return this.wrap(colorize(this.config?.theme?.commandSummary, description.join('\n')))
}
}

Expand Down Expand Up @@ -125,7 +132,7 @@ export class CommandHelp extends HelpFormatter {
return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommands}`
})
.join('\n\n')
return body
return colorize(this.config?.theme?.sectionDescription, body)
}

protected flagHelpLabel(flag: Command.Flag.Any, showOptions = false): string {
Expand All @@ -142,7 +149,7 @@ export class CommandHelp extends HelpFormatter {
}
}

label = labels.join(', ')
label = labels.join(colorize(this.config?.theme?.flagSeparator, ', '))
}

if (flag.type === 'option') {
Expand All @@ -163,20 +170,20 @@ export class CommandHelp extends HelpFormatter {
if (flags.length === 0) return

return flags.map((flag) => {
const left = this.flagHelpLabel(flag)
const left = colorize(this.config?.theme?.flag, this.flagHelpLabel(flag))

let right = flag.summary || flag.description || ''
if (flag.type === 'option' && flag.default) {
right = `[default: ${flag.default}] ${right}`
right = `${colorize(this.config?.theme?.flagDefaultValue, `[default: ${flag.default}]`)} ${right}`
}

if (flag.required) right = `(required) ${right}`
if (flag.required) right = `${colorize(this.config?.theme?.flagRequired, '(required)')} ${right}`

if (flag.type === 'option' && flag.options && !flag.helpValue && !this.opts.showFlagOptionsInTitle) {
right += `\n<options: ${flag.options.join('|')}>`
right += colorize(this.config?.theme?.flagOptions, `\n<options: ${flag.options.join('|')}>`)
}

return [left, dim(right.trim())]
return [left, colorize(this.config?.theme?.sectionDescription, right.trim())]
})
}

Expand All @@ -197,7 +204,7 @@ export class CommandHelp extends HelpFormatter {
})
.join('\n\n')

return body
return colorize(this.config?.theme?.sectionDescription, body)
}

generate(): string {
Expand Down Expand Up @@ -305,7 +312,16 @@ export class CommandHelp extends HelpFormatter {
const body = (usage ? castArray(usage) : [this.defaultUsage()])
.map((u) => {
const allowedSpacing = this.opts.maxWidth - this.indentSpacing
const line = `$ ${this.config.bin} ${u}`.trim()

const dollarSign = colorize(this.config?.theme?.dollarSign, '$')
const bin = colorize(this.config?.theme?.bin, this.config.bin)
const command = colorize(this.config?.theme?.command, '<%= command.id %>')
const commandDescription = colorize(
this.config?.theme?.sectionDescription,
u.replace('<%= command.id %>', '').trim(),
)

const line = `${dollarSign} ${bin} ${command} ${commandDescription}`.trim()
if (line.length > allowedSpacing) {
const splitIndex = line.slice(0, Math.max(0, allowedSpacing)).lastIndexOf(' ')
return (
Expand All @@ -323,13 +339,16 @@ export class CommandHelp extends HelpFormatter {

private formatIfCommand(example: string): string {
example = this.render(example)
if (example.startsWith(this.config.bin)) return dim(`$ ${example}`)
if (example.startsWith(`$ ${this.config.bin}`)) return dim(example)
const dollarSign = colorize(this.config?.theme?.dollarSign, '$')
if (example.startsWith(this.config.bin)) return `${dollarSign} ${example}`
if (example.startsWith(`$ ${this.config.bin}`)) return `${dollarSign}${example.replace(`$`, '')}`
return example
}

private isCommand(example: string): boolean {
return stripAnsi(this.formatIfCommand(example)).startsWith(`$ ${this.config.bin}`)
return stripAnsi(this.formatIfCommand(example)).startsWith(
`${colorize(this.config?.theme?.dollarSign, '$')} ${this.config.bin}`,
)
}
}
export default CommandHelp
4 changes: 2 additions & 2 deletions src/help/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import wrap from 'wrap-ansi'
import {Command} from '../command'
import * as Interfaces from '../interfaces'
import {stdtermwidth} from '../screen'
import {template} from './util'
import {colorize, template} from './util'

export type HelpSectionKeyValueTable = {description: string; name: string}[]
export type HelpSection =
Expand Down Expand Up @@ -176,7 +176,7 @@ export class HelpFormatter {
}

const output = [
chalk.bold(header),
colorize(this.config?.theme?.sectionHeader, chalk.bold(header)),
this.indent(
Array.isArray(newBody) ? this.renderList(newBody, {indentation: 2, stripAnsi: this.opts.stripAnsi}) : newBody,
),
Expand Down
40 changes: 30 additions & 10 deletions src/help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import {compact, sortBy, uniqBy} from '../util/util'
import {CommandHelp} from './command'
import {HelpFormatter} from './formatter'
import RootHelp from './root'
import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFromArgv, toConfiguredId} from './util'
import {
colorize,
formatCommandDeprecationWarning,
getHelpFlagAdditions,
standardizeIDFromArgv,
toConfiguredId,
} from './util'

export {CommandHelp} from './command'
export {getHelpFlagAdditions, normalizeArgv, standardizeIDFromArgv} from './util'
Expand Down Expand Up @@ -80,10 +86,10 @@ export class Help extends HelpBase {
protected description(c: Command.Loadable): string {
const description = this.render(c.description || '')
if (c.summary) {
return description
return colorize(this.config?.theme?.sectionDescription, description)
}

return description.split('\n').slice(1).join('\n')
return colorize(this.config?.theme?.sectionDescription, description.split('\n').slice(1).join('\n'))
}

protected formatCommand(command: Command.Loadable): string {
Expand All @@ -103,7 +109,11 @@ export class Help extends HelpBase {
.filter((c) => (this.opts.hideAliasesFromRoot ? !c.aliases?.includes(c.id) : true))
.map((c) => {
if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator)
return [c.id, this.summary(c)]
const summary = this.summary(c)
return [
colorize(this.config?.theme?.command, c.id),
summary && colorize(this.config?.theme?.sectionDescription, summary),
]
}),
{
indentation: 2,
Expand All @@ -127,9 +137,16 @@ export class Help extends HelpBase {
let topicID = `${topic.name}:COMMAND`
if (this.config.topicSeparator !== ':') topicID = topicID.replaceAll(':', this.config.topicSeparator)
let output = compact([
summary,
this.section(this.opts.usageHeader || 'USAGE', `$ ${this.config.bin} ${topicID}`),
description && this.section('DESCRIPTION', this.wrap(description)),
colorize(this.config?.theme?.commandSummary, summary),
this.section(
this.opts.usageHeader || 'USAGE',
`${colorize(this.config?.theme?.dollarSign, '$')} ${colorize(
this.config?.theme?.bin,
this.config.bin,
)} ${topicID}`,
),
description &&
this.section('DESCRIPTION', this.wrap(colorize(this.config?.theme?.sectionDescription, description))),
]).join('\n\n')
if (this.opts.stripAnsi) output = stripAnsi(output)
return output + '\n'
Expand All @@ -140,7 +157,10 @@ export class Help extends HelpBase {
const body = this.renderList(
topics.map((c) => {
if (this.config.topicSeparator !== ':') c.name = c.name.replaceAll(':', this.config.topicSeparator)
return [c.name, c.description && this.render(c.description.split('\n')[0])]
return [
colorize(this.config?.theme?.topic, c.name),
c.description && this.render(colorize(this.config?.theme?.sectionDescription, c.description.split('\n')[0])),
]
}),
{
indentation: 2,
Expand Down Expand Up @@ -334,9 +354,9 @@ export class Help extends HelpBase {
}

protected summary(c: Command.Loadable): string | undefined {
if (c.summary) return this.render(c.summary.split('\n')[0])
if (c.summary) return colorize(this.config?.theme?.commandSummary, this.render(c.summary.split('\n')[0]))

return c.description && this.render(c.description).split('\n')[0]
return c.description && colorize(this.config?.theme?.commandSummary, this.render(c.description).split('\n')[0])
}

/*
Expand Down
Loading