Skip to content
This repository has been archived by the owner on May 4, 2024. It is now read-only.

Commit

Permalink
Rewrite theme support for greater extensibility
Browse files Browse the repository at this point in the history
  • Loading branch information
iarna committed Mar 9, 2016
1 parent 023575c commit 0b0710c
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 87 deletions.
87 changes: 67 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ optional:
so it would stop your program from exiting– if you want to use this
feature with 0.8 just make sure you call `gauge.disable()` before you
expect your program to exit.
* **theme**: The theme to use as output. Defaults to something usable on
your combination of OS, color and unicode support. In practice this means
that Windows always gets ASCII by default, Mac almost always gets unicode,
and everyone almost always gets color. Unicode is detected with
[has-unicode] and color is detected by checking platform and TERM env var.
Theme selection is done by the internal [themes] module, which you can use
directly.
* **themes**: A themeset to use when selecting the theme to use. Defaults
to `gauge/themes`, see the [themes] documentation for details.
* **theme**: Alternatively you can specificy a specific theme to use. If you
use this, there's no reason to use **themes**.

The default theme is selected using **themes** to something usable on your
combination of OS, color and unicode support. Unicode support is detected
with [has-unicode] and color is detected by checking platform and TERM env
var.
* **template**: Describes what you want your gauge to look like. The
default is what npm uses. Detailed [documentation] is later in this
document.
Expand Down Expand Up @@ -145,37 +147,82 @@ progress bar interface.
### THEMES

```
var theme = require('gauge/themes')
var themes = require('gauge/themes')
// fetch the default color unicode theme for this platform
var ourTheme = theme({hasUnicode: true, hasColor: true})
var ourTheme = themes({hasUnicode: true, hasColor: true})
// fetch the default non-color unicode theme for osx
var ourTheme = theme({hasUnicode: true, hasColor: false, platform: 'darwin'})
var ourTheme = themes({hasUnicode: true, hasColor: false, platform: 'darwin'})
// create a new theme based on the color ascii theme for this platform
// that brackets the progress bar with arrows
var ourTheme = theme.newTheme(theme(hasUnicode: false, hasColor: true}), {
var ourTheme = themes.newTheme(theme(hasUnicode: false, hasColor: true}), {
preProgressbar: '→',
postProgressbar: '←'
})
```

#### theme(opts)
The object returned by `gauge/themes` is an instance of the `ThemeSet` class.

Fetches a theme object based on platform settings.
```
var ThemeSet = require('gauge/theme-set')
var themes = new ThemeSet()
```

#### themes(opts)
#### themes.getDefault(opts)

Theme objects are a function that fetches the default theme based on
platform, unicode and color support.

Options is an object with the following properties:

* **hasUnicode** - If true, fetch a unicode theme.
* **hasColor** - If true, fetch a color theme.
* **platform** (optional) - Defaults to `process.platform`. The platform code of the theme we want.
* **hasUnicode** - If true, fetch a unicode theme, if no unicode theme is
available then a non-unicode theme will be used.
* **hasColor** - If true, fetch a color theme, if no color theme is
available a non-color theme will be used.
* **platform** (optional) - Defaults to `process.platform`. If no
platform match is available then `fallback` is used instead.

If no compatible theme can be found then an error will be thrown with a
`code` of `EMISSINGTHEME`.

#### themes.addTheme(themeName, themeObj)
#### themes.addTheme(themeName, [parentTheme], newTheme)

Adds a named theme to the themeset. You can pass in either a theme object,
as returned by `themes.newTheme` or the arguments you'd pass to
`themes.newTheme`.

#### themes.getTheme(name)

Returns the theme object from this theme set named `name`.

If `name` does not exist in this themeset an error will be thrown with
a `code` of `EMISSINGTHEME`.

#### themes.setDefault([opts], themeName)

`opts` is an object with the following properties.

* **platform** - Defaults to `'fallback'`. If your theme is platform
specific, specify that here with the platform from `process.platform`, eg,
`win32`, `darwin`, etc.
* **hasUnicode** - Defaults to `false`. If your theme uses unicode you
should set this to true.
* **hasColor** - Defaults to `false`. If your theme uses color you should
set this to true.

`themeName` is the name of the theme (as given to `addTheme`) to use for
this set of `opts`.

#### theme.newTheme([parent,] newTheme)
#### themes.newTheme([parentTheme,] newTheme)

Create a new theme object based on `parent`. If no `parent` is provided
then a minimal parent that defines functions for rendering the activity
indicator (spinner) and progress bar will be defined.
Create a new theme object based on `parentTheme`. If no `parentTheme` is
provided then a minimal parentTheme that defines functions for rendering the
activity indicator (spinner) and progress bar will be defined. (This
fallback parent is defined in `gauge/base-theme`.)

newTheme should be a bare object– we'll start by discussing the properties
defined by the default themes:
Expand Down
14 changes: 14 additions & 0 deletions base-theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'
var spin = require('./spin.js')
var progressBar = require('./progress-bar.js')

module.exports = {
activityIndicator: function (values, theme, width) {
if (values.spun == null) return
return spin(theme, values.spun)
},
progressbar: function (values, theme, width) {
if (values.completed == null) return
return progressBar(theme, width, values.completed)
}
}
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ var Plumbing = require('./plumbing.js')
var hasUnicode = require('has-unicode')
var hasColor = require('./has-color.js')
var onExit = require('signal-exit')
var themes = require('./themes')
var defaultThemes = require('./themes')

module.exports = Gauge

Expand Down Expand Up @@ -37,6 +37,7 @@ function Gauge (writeTo, options) {
(writeTo === process.stderr && process.stdout.isTTY && process.stdout) ||
(writeTo.isTTY && writeTo)

var themes = options.themes || defaultThemes
var theme = options.theme || themes({hasUnicode: hasUnicode(), hasColor: hasColor})
var template = options.template || [
{type: 'progressbar', length: 20},
Expand Down
2 changes: 1 addition & 1 deletion test/plumbing.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var stripInvisible = function (str) {
var template = [
{type: 'name'}
]
var plumbing = new Plumbing(themes.fallback.noUnicode.noColor, template, 10)
var plumbing = new Plumbing(themes.getTheme('ASCII'), template, 10)

// These three produce fixed strings and are entirely static, so as long as
// they produce _something_ they're probably ok. Actually testing them will
Expand Down
82 changes: 82 additions & 0 deletions test/theme-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict'
var test = require('tap').test
var ThemeSet = require('../theme-set.js')

var themes = new ThemeSet()

test('setup', function (t) {
themes.addTheme('fallback', {id: 0})
themes.addTheme('test1', {id: 1})
themes.addTheme('test2', {id: 2})
themes.addTheme('test3', {id: 3})
themes.addTheme('test4', {id: 4})
themes.addTheme('testz', themes.getTheme('fallback'), {id: 'z'})
themes.setDefault('fallback')
themes.setDefault({platform: 'aa', hasUnicode: false, hasColor: false}, 'test1')
themes.setDefault({platform: 'bb', hasUnicode: true, hasColor: true}, 'test2')
themes.setDefault({platform: 'ab', hasUnicode: false, hasColor: true}, 'test3')
themes.setDefault({platform: 'ba', hasUnicode: true, hasColor: false}, 'test4')

themes.setDefault({platform: 'zz', hasUnicode: false, hasColor: false}, 'test1')
themes.setDefault({platform: 'zz', hasUnicode: true, hasColor: true}, 'test2')
themes.setDefault({platform: 'zz', hasUnicode: false, hasColor: true}, 'test3')
themes.setDefault({platform: 'zz', hasUnicode: true, hasColor: false}, 'test4')
t.done()
})

test('themeset', function (t) {
t.is(themes().id, 0, 'fallback')

t.is(themes({platform: 'aa'}).id, 1, 'aa ff')
t.is(themes({platform: 'aa', hasUnicode: true}).id, 1, 'aa tf')
t.is(themes({platform: 'aa', hasColor: true}).id, 1, 'aa ft')
t.is(themes({platform: 'aa', hasUnicode: true, hasColor: true}).id, 1, 'aa tt')
t.is(themes({platform: 'bb'}).id, 0, 'bb ff')
t.is(themes({platform: 'bb', hasUnicode: true}).id, 0, 'bb tf')
t.is(themes({platform: 'bb', hasColor: true}).id, 0, 'bb ft')
t.is(themes({platform: 'bb', hasUnicode: true, hasColor: true}).id, 2, 'bb tt')

t.is(themes({platform: 'ab'}).id, 0, 'ab ff')
t.is(themes({platform: 'ab', hasUnicode: true}).id, 0, 'ab tf')
t.is(themes({platform: 'ab', hasColor: true}).id, 3, 'ab ft')
t.is(themes({platform: 'ab', hasUnicode: true, hasColor: true}).id, 3, 'ab tt')

t.is(themes({platform: 'ba'}).id, 0, 'ba ff')
t.is(themes({platform: 'ba', hasUnicode: true}).id, 4, 'ba tf')
t.is(themes({platform: 'ba', hasColor: true}).id, 0, 'ba ft')
t.is(themes({platform: 'ba', hasUnicode: true, hasColor: true}).id, 4, 'ba tt')

t.is(themes({platform: 'zz'}).id, 1, 'zz ff')
t.is(themes({platform: 'zz', hasUnicode: true}).id, 4, 'zz tf')
t.is(themes({platform: 'zz', hasColor: true}).id, 3, 'zz ft')
t.is(themes({platform: 'zz', hasUnicode: true, hasColor: true}).id, 2, 'zz tt')

try {
themes.getTheme('does not exist')
t.fail('missing theme')
} catch (ex) {
t.is(ex.code, 'EMISSINGTHEME', 'missing theme')
}

t.is(themes.getTheme('testz').id, 'z', 'testz')

var empty = new ThemeSet()

try {
empty()
t.fail('no themes')
} catch (ex) {
t.is(ex.code, 'EMISSINGTHEME', 'no themes')
}

empty.addTheme('exists', {id: 'exists'})
empty.setDefault({hasUnicode: true, hasColor: true}, 'exists')
try {
empty()
t.fail('no fallback')
} catch (ex) {
t.is(ex.code, 'EMISSINGTHEME', 'no fallback')
}
t.done()
})

28 changes: 5 additions & 23 deletions test/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,12 @@
var test = require('tap').test
var themes = require('../themes.js')

test('themes', function (t) {
Object.keys(themes).forEach(function (platform) {
if (typeof themes[platform] === 'function') return
var platformThemeGroup = themes[platform]
;['hasUnicode', 'noUnicode'].forEach(function (unicode) {
var unicodeThemeGroup = platformThemeGroup[unicode]
if (t.ok(unicodeThemeGroup, platform + ' has theming for ' + unicode)) {
['hasColor', 'noColor'].forEach(function (color) {
var colorTheme = unicodeThemeGroup[color]
t.ok(colorTheme, platform + ' ' + unicode + ' has theming for ' + color)
})
}
})
})
t.end()
})

test('selector', function (t) {
t.is(themes({hasUnicode: false, hasColor: false, platform: 'unknown'}), themes.fallback.noUnicode.noColor, 'fallback')
t.is(themes({hasUnicode: false, hasColor: false, platform: 'darwin'}), themes.darwin.noUnicode.noColor, 'ff darwin')
t.is(themes({hasUnicode: true, hasColor: false, platform: 'darwin'}), themes.darwin.hasUnicode.noColor, 'tf drawin')
t.is(themes({hasUnicode: false, hasColor: true, platform: 'darwin'}), themes.darwin.noUnicode.hasColor, 'ft darwin')
var theme = themes[process.platform] || themes.fallback
t.is(themes({hasUnicode: true, hasColor: true}), theme.hasUnicode.hasColor, 'tt')
t.is(themes({hasUnicode: false, hasColor: false, platform: 'unknown'}), themes.getTheme('ASCII'), 'fallback')
t.is(themes({hasUnicode: false, hasColor: false, platform: 'darwin'}), themes.getTheme('ASCII'), 'ff darwin')
t.is(themes({hasUnicode: true, hasColor: false, platform: 'darwin'}), themes.getTheme('brailleSpinner'), 'tf drawin')
t.is(themes({hasUnicode: false, hasColor: true, platform: 'darwin'}), themes.getTheme('colorASCII'), 'ft darwin')
t.is(themes({hasUnicode: true, hasColor: true, platform: 'darwin'}), themes.getTheme('colorBrailleSpinner'), 'ft darwin')
t.end()
})

Expand Down
98 changes: 98 additions & 0 deletions theme-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'
var objectAssign = require('object-assign')
var baseTheme = require('./base-theme.js')

module.exports = function () {
return ThemeSetProto.newThemeSet()
}

var ThemeSetProto = {}

ThemeSetProto.newTheme = function (parent, theme) {
if (!theme) {
theme = parent
parent = baseTheme
}
return objectAssign({}, parent, theme)
}

ThemeSetProto.addTheme = function (name, parent, theme) {
this.themes[name] = this.newTheme(parent, theme)
}

ThemeSetProto.getTheme = function (name) {
if (!this.themes[name]) throw this.newMissingThemeError(name)
return this.themes[name]
}

ThemeSetProto.setDefault = function (opts, name) {
if (name == null) {
name = opts
opts = {}
}
var platform = opts.platform == null ? 'fallback' : opts.platform
var hasUnicode = !!opts.hasUnicode
var hasColor = !!opts.hasColor
if (!this.defaults[platform]) this.defaults[platform] = {true: {}, false: {}}
this.defaults[platform][hasUnicode][hasColor] = name
}

ThemeSetProto.getDefault = function (opts) {
if (!opts) opts = {}
var platformName = opts.platform || process.platform
var platform = this.defaults[platformName] || this.defaults.fallback
var hasUnicode = !!opts.hasUnicode
var hasColor = !!opts.hasColor
if (!platform) throw this.newMissingDefaultThemeError(platformName, hasUnicode, hasColor)
if (!platform[hasUnicode][hasColor]) {
if (hasUnicode && hasColor && platform[!hasUnicode][hasColor]) {
hasUnicode = false
} else if (hasUnicode && hasColor && platform[hasUnicode][!hasColor]) {
hasColor = false
} else if (hasUnicode && hasColor && platform[!hasUnicode][!hasColor]) {
hasUnicode = false
hasColor = false
} else if (hasUnicode && !hasColor && platform[!hasUnicode][hasColor]) {
hasUnicode = false
} else if (!hasUnicode && hasColor && platform[hasUnicode][!hasColor]) {
hasColor = false
} else if (platform === this.defaults.fallback) {
throw this.newMissingDefaultThemeError(platformName, hasUnicode, hasColor)
}
}
if (platform[hasUnicode][hasColor]) {
return this.getTheme(platform[hasUnicode][hasColor])
} else {
return this.getDefault(objectAssign({}, opts, {platform: 'fallback'}))
}
}

ThemeSetProto.newMissingThemeError = function newMissingThemeError (name) {
var err = new Error('Could not find a gauge theme named "' + name + '"')
Error.captureStackTrace.call(err, newMissingThemeError)
err.theme = name
err.code = 'EMISSINGTHEME'
return err
}

ThemeSetProto.newMissingDefaultThemeError = function newMissingDefaultThemeError (platformName, hasUnicode, hasColor) {
var err = new Error(
'Could not find a gauge theme for your platform/unicode/color use combo:\n' +
' platform = ' + platformName + '\n' +
' hasUnicode = ' + hasUnicode + '\n' +
' hasColor = ' + hasColor)
Error.captureStackTrace.call(err, newMissingDefaultThemeError)
err.platform = platformName
err.hasUnicode = hasUnicode
err.hasColor = hasColor
err.code = 'EMISSINGTHEME'
return err
}

ThemeSetProto.newThemeSet = function () {
var themeset = function (opts) {
return themeset.getDefault(opts)
}
return objectAssign(themeset, ThemeSetProto, {themes: {}, defaults: {}})
}

Loading

0 comments on commit 0b0710c

Please sign in to comment.