a lot of theming

master
Valerie 2021-06-18 02:02:50 -04:00
parent 9769fc6cbb
commit 35c3680268
12 changed files with 205 additions and 71 deletions

View File

@ -13,6 +13,7 @@
"chalk": "^4.1.1",
"faker": "^5.5.3",
"frigid": "^1.3.8",
"fs-extra": "^10.0.0",
"get-port": "^5.1.1",
"logger": "^0.0.1",
"neo-blessed": "^0.2.0",

View File

@ -1,5 +1,6 @@
import { Serializable } from 'frigid';
import { Renderable } from './ui/UI';
import { getTheme } from './ui/Theme.js';
import { Renderable } from './ui/UI.js';
export type ItemID = string;
@ -41,6 +42,6 @@ export class ItemState extends Serializable implements Renderable {
}
render() {
return ` ${this.item.name}{|}${this.qty} `;
return getTheme().normal(` ${this.item.name}{|}${this.qty} `);
}
}

View File

@ -1,19 +1,27 @@
import chalk from "chalk";
import { getTheme } from "./ui/Theme.js";
export enum ProgressbarStyle {
indicator = 'indicator',
progress = 'progress'
}
export function progressbar(completion, width, style: ProgressbarStyle = ProgressbarStyle.indicator) {
export const barCache: Map<string, string> = new Map();
export function progressbar(completion: number, width: number, style: ProgressbarStyle = ProgressbarStyle.indicator) {
const cacheKey = `${completion}-${width}-${style}`;
if(barCache.has(cacheKey)) {
stats.cacheHits ++;
return barCache.get(cacheKey);
}
let chalkFn
if(style === ProgressbarStyle.indicator) {
if(completion > 0.8) chalkFn = chalk.bgBlue.cyan;
else if(completion > 0.5) chalkFn = chalk.bgBlue.green;
else if(completion > 0.2) chalkFn = chalk.bgBlue.yellow;
else chalkFn = chalk.bgBlue.red;
if(completion > getTheme().progressBar.indicator.buckets[2]) chalkFn = getTheme().progressBar.indicator.excellent;
else if(completion > getTheme().progressBar.indicator.buckets[1]) chalkFn = getTheme().progressBar.indicator.normal;
else if(completion > getTheme().progressBar.indicator.buckets[0]) chalkFn = getTheme().progressBar.indicator.warning;
else chalkFn = getTheme().progressBar.indicator.critical;
} else if(style === ProgressbarStyle.progress) {
chalkFn = chalk.bgBlue.cyan;
chalkFn = getTheme().progressBar.normal;
}
const chars = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
let str = '';
@ -22,5 +30,12 @@ export function progressbar(completion, width, style: ProgressbarStyle = Progres
const char = chars[remainder];
str += chalkFn(char);
}
stats.cacheMisses ++;
barCache.set(cacheKey, str);
return str;
}
export const stats = {
cacheHits: 0,
cacheMisses: 0
}

View File

@ -2,6 +2,7 @@ import chalk from "chalk";
import { Serializable } from "frigid";
import { isThisTypeNode } from "typescript";
import log from "./log.js";
import { getTheme } from "./ui/Theme.js";
import { Renderable } from "./ui/UI.js";
const daysInMonth = [
@ -18,7 +19,7 @@ const months = [
'Oct', 'Nov', 'Dec'
]
export default class Time extends Serializable implements Renderable{
export default class Time extends Serializable implements Renderable {
rate: number;
paused = true;
@ -56,10 +57,22 @@ export default class Time extends Serializable implements Renderable{
render() {
const sym = (this.hour >= 6 && this.hour < 20) ?
chalk.yellowBright('☼') :
chalk.blue('☾')
chalk.ansi256(226).bgAnsi256(27)(' ⬤ ') :
chalk.ansi256(254).bgAnsi256(17)(' ☾ ')
return `${sym} ${this.hour.toString().padStart(2, ' ')}:${this.minute.toString().padStart(2, '0')} ${months[this.month]} ${this.day + 1}, ${this.normalizedYear}`
return `${sym} ${
getTheme().normal(`${
this.hour.toString().padStart(2, ' ')
}:${
this.minute.toString().padStart(2, '0')
} ${
months[this.month]
} ${
this.day + 1
}, ${
this.normalizedYear
}`)
}`;
// return '☾' || '☼';
}

View File

@ -1,7 +1,11 @@
import { Game } from './Game.js';
import { render } from './ui/UI.js';
import { ensureDirSync } from 'fs-extra';
import { parse } from 'path';
const saveFile = process.argv[2];
const saveFile = process.argv[2] || 'data/world01.json';
const game = Game.create(saveFile || 'data/world01.json');
ensureDirSync(parse(saveFile).dir);
const game = Game.create(saveFile);
render(game);

View File

@ -54,7 +54,7 @@ export async function ready(name, onThing?) {
})
pawns.push(pawn);
}
new Popup(`${(() => {
Popup.show(`${(() => {
if(pawns.length === 0) return `A care package has arrived from ${from}.`;
if(pawns.length === 1) return `A traveler from ${from} named ${pawns[0].toString()} has arrived.`;
if(pawns.length > 1) return `A caravan of ${pawns.length} people from ${from} has arrived.`

View File

@ -4,6 +4,7 @@ import { Game } from '../Game.js';
import { ItemState } from '../Item.js';
import { Player } from "../multiplayer/Player";
import { Pawn } from '../Pawn.js';
import { getTheme } from './Theme.js';
import { boxStyle, screen } from './UI.js';
export class GiftPopup {
@ -68,7 +69,7 @@ export class GiftPopup {
this.box.setContent(`${(() => {
let pawns = [];
for (const [pawn, qty] of this.pawns.entries()) {
const style = i === this.selected ? chalk.underline : _ => _;
const style = i === this.selected ? getTheme().selected : getTheme().normal;
if(qty > 0) {
pawns.push(style(`{|}${pawn.toString()} `))
} else {
@ -78,7 +79,7 @@ export class GiftPopup {
i ++;
}
return pawns.join('\n')
})()}\n\n{|}${chalk.green('escape')}: Cancel \n{|}${chalk.green('enter')}: Okay `);
})()}\n\n{|}${getTheme().hotkey('escape')}${getTheme().normal(': Cancel ')}\n{|}${getTheme().hotkey('enter')}${getTheme().normal(': Okay ')}`);
screen.render();
}
}

View File

@ -1,14 +1,15 @@
import { Pawn } from '../Pawn.js';
import log from '../log.js';
import { menuPanel, tags, Renderable } from './UI.js';
import chalk from 'chalk';
import { menuPanel, Renderable } from './UI.js';
import { Game } from '../Game.js';
import { ChopTreeTask } from '../tasks/ChopTreeTask.js';
import { progressbar } from '../Progressbar.js';
import { progressbar, stats, barCache } from '../Progressbar.js';
import { Popup } from './Popup.js';
import mdns from '../multiplayer/mDNS.js';
import { GiftPopup } from './GiftPopup.js';
import { PawnDetails } from './PawnDetails.js';
import { defaultTheme, getTheme } from './Theme.js';
import { inspect } from 'util';
enum SubMenu {
NONE = 'NONE',
@ -38,9 +39,9 @@ export class Menu implements Renderable {
} else if (key.full === 'q') {
this.subMenu = SubMenu.TREES;
} else if (key.full === '1') {
new Popup('this is a test!');
Popup.show(inspect(stats));
} else if (key.full === '2') {
new Popup('Etiam hendrerit elit sit amet metus congue dictum nec eu lacus. Sed aliquam in justo efficitur faucibus. Duis tellus diam, congue volutpat lorem et, semper consectetur erat. Nunc ac velit dignissim, tincidunt augue eget, tristique orci. Duis lacus sapien, bibendum id pharetra vel, semper et nunc. Vestibulum eu tellus imperdiet, lacinia ante ac, porta nisl. Donec at eleifend risus, ac dictum odio.');
Popup.show('Etiam hendrerit elit sit amet metus congue dictum nec eu lacus. Sed aliquam in justo efficitur faucibus. Duis tellus diam, congue volutpat lorem et, semper consectetur erat. Nunc ac velit dignissim, tincidunt augue eget, tristique orci. Duis lacus sapien, bibendum id pharetra vel, semper et nunc. Vestibulum eu tellus imperdiet, lacinia ante ac, porta nisl. Donec at eleifend risus, ac dictum odio.');
} else if (key.full === 'escape') {
this.subMenu = SubMenu.NONE;
}
@ -103,29 +104,30 @@ export class Menu implements Renderable {
const colSpace = ((menuPanel.width - 2) / 2);
return (` Menus:${' '.repeat(colSpace - 8)}Actions:\n ${
chalk.greenBright('q')
}: ${
(this.subMenu !== SubMenu.TREES ? chalk.bold.black : _ => _)('Chop Trees')
return (` ${getTheme().header('Menus')}${getTheme().normal(':')}${
' '.repeat(colSpace - 8)
}${getTheme().header('Actions')}${getTheme().normal(':')}\n ${
getTheme().hotkey('q')
}${getTheme().normal(': ')}${
(this.subMenu !== SubMenu.TREES ? getTheme().normal : getTheme().selected)('Chop Trees')
}${
' '.repeat(colSpace - 15)
}${chalk.greenBright('z')}: ${
(this.subMenu !== SubMenu.NONE ? chalk.bold.black : _ => _)('Create Pawn')
}${getTheme().hotkey('z')}${getTheme().normal(': ')}${
(this.subMenu !== SubMenu.NONE ? getTheme().normal : getTheme().selected)('Create Pawn')
}\n${
' '.repeat(colSpace)
}${
chalk.greenBright('x')
}: ${
(this.subMenu !== SubMenu.NONE ? chalk.bold.black : _ => _)('Clear Tasks')
}\
`);
getTheme().hotkey('x')
}${getTheme().normal(': ')}${
(this.subMenu !== SubMenu.NONE ? getTheme().normal : getTheme().selected)('Clear Tasks')
}`);
}
renderTopBar() {
const idlers = Game.current.pawns.filter(pawn => pawn.idle);
return ` ${Game.current.clock.render()}{|}Idle: ${idlers.length} `;
return ` ${Game.current.clock.render()}{|}${getTheme().normal(`Idle: ${idlers.length}`)} `;
}
renderPawns() {
@ -134,10 +136,10 @@ export class Menu implements Renderable {
const selected = pawn === Game.current.selected;
let str = '';
if(selected) {
str += ` ${tags.white.fg} ${pawn.toString()}${tags.reset}{|}${pawn.status} \n`;
str += ` Energy{|}${progressbar(pawn.energy / 100, (menuPanel.width - 4) / 2)} \n`;
str += ` ${getTheme().selected(` ${pawn.toString()}`)}{|}${pawn.status} \n`;
str += ` ${getTheme().normal('Energy')}{|}${progressbar(pawn.energy / 100, (menuPanel.width - 4) / 2)} \n`;
} else {
str += ` ${tags.bright}${tags.black.fg} ${pawn.toString()}${tags.reset}{|}${pawn.status} `;
str += ` ${getTheme().normal(pawn.toString())}{|}${pawn.status} `;
}
return str;
})()}`).join('\n')
@ -152,9 +154,9 @@ export class Menu implements Renderable {
}${(() => {
return Object.values(View).map(view => {
if(view === this.view) {
return chalk.cyan.inverse(` ${view} `);
return getTheme().tab.selected(` ${view} `);
} else {
return chalk.cyan(` ${view} `);
return getTheme().tab.normal(` ${view} `);
}
}).join('');
})()}{/center}\n\n${(() => {
@ -169,10 +171,10 @@ export class Menu implements Renderable {
multiplayerSelected = 0;
renderMultiplayer() {
if(mdns.players.length === 0) return `{center}${tags.bright}${tags.black.fg}No friends online{/center}`;
if(mdns.players.length === 0) return `{center}${getTheme().normal('No friends online')}{/center}`;
return mdns.players.map((player, i) => {
if(i === this.multiplayerSelected) return ' ' + player.toString();
else return ' ' + chalk.bold.black(player.toString());
if(i === this.multiplayerSelected) return ' ' + getTheme().selected(' ' + player.toString());
else return ' ' + getTheme().normal(player.toString());
}).join('\n');
}
@ -184,7 +186,7 @@ export class Menu implements Renderable {
return `${(() => {
switch(this.subMenu) {
case SubMenu.NONE:
return `{center}${tags.bright}${tags.black.fg}* Select a menu above for options *`;
return `{center}${getTheme().normal('* Select a menu above for options *')}{/center}`;
case SubMenu.TREES:
return this.renderTreesSubMenu();
}
@ -194,15 +196,15 @@ export class Menu implements Renderable {
renderTreesSubMenu() {
return [
`{center}Chop Trees`,
`{left} ${chalk.greenBright('-=_+')}: ${this.trees}`,
`{left} ${chalk.greenBright('enter')}: Create Task`,
`{left} ${chalk.greenBright('escape')}: Cancel`
`{left} ${getTheme().hotkey('-=_+')}: ${this.trees}`,
`{left} ${getTheme().hotkey('enter')}: Create Task`,
`{left} ${getTheme().hotkey('escape')}: Cancel`
].join('\n');
}
render() {
const width = menuPanel.width - 2;
const hr = chalk.bold.black('━'.repeat(width));
const hr = getTheme().normal('━'.repeat(width));
const content = [
this.renderTopBar(),
hr,

View File

@ -1,18 +1,23 @@
import chalk from 'chalk';
import blessed from 'neo-blessed';
import { Game } from '../Game.js';
import { getTheme } from './Theme.js';
import { boxStyle, screen } from './UI.js';
export class Popup {
box;
constructor(content) {
static show(content) {
new Popup(content)
}
private constructor(content) {
this.box = blessed.box({
top: 'center',
left: 'center',
width: 'shrink',
height: 'shrink',
content: content + `\n\n{|}` + chalk.green('enter') + `: Okay `,
content: getTheme().normal(content) + `\n\n{|}` + getTheme().hotkey('enter') + getTheme().normal(`: Okay `),
tags: true,
...boxStyle(),
});

77
src/ui/Theme.ts 100644
View File

@ -0,0 +1,77 @@
import chalk from "chalk";
type StyleFunction = (text: string) => string;
export type Theme = {
header: StyleFunction,
subheader: StyleFunction,
normal: StyleFunction,
selected: StyleFunction,
hotkey: StyleFunction,
tab: {
normal: StyleFunction,
selected: StyleFunction
},
border: {
focused: string,
normal: string
},
progressBar: {
indicator: {
critical: StyleFunction,
warning: StyleFunction,
normal: StyleFunction,
excellent: StyleFunction,
buckets: [number, number, number]
},
normal: StyleFunction
}
}
export const defaultTheme: Theme = {
header: chalk.ansi256(255).bold,
subheader: chalk.ansi256(243).bold,
normal: chalk.ansi256(243),
selected: chalk.ansi256(250),
hotkey: chalk.ansi256(40),
tab: {
normal: chalk.ansi256(117).bgAnsi256(232),
selected: chalk.ansi256(232).bgAnsi256(117)
},
border: {
focused: '#ffffff',
normal: '#222222'
},
progressBar: {
indicator: {
critical: chalk.bgAnsi256(235).ansi256(88),
warning: chalk.bgAnsi256(235).ansi256(202),
normal: chalk.bgAnsi256(235).ansi256(70),
excellent: chalk.bgAnsi256(235).ansi256(87),
buckets: [.1, .25, .95]
},
normal: chalk.bgAnsi256(235).ansi256(243)
}
}
const debugStyle = chalk.ansi256(213);
export const debugTheme: Theme = {
header: debugStyle.inverse,
subheader: debugStyle,
normal: debugStyle,
selected: debugStyle.inverse,
hotkey: debugStyle,
tab: {
normal: debugStyle,
selected: debugStyle.inverse,
},
border: {
focused: '#ff88ff',
normal: '#ff00ff'
},
progressBar: defaultTheme.progressBar
}
export function getTheme(): Theme {
return defaultTheme;
}

View File

@ -1,41 +1,26 @@
import blessed from 'neo-blessed';
import ansi from 'sisteransi';
import { getTheme } from './Theme.js';
export const screen = blessed.screen({
smartCSR: true
smartCSR: true,
terminal: 'xterm-256color'
});
export interface Renderable {
render: () => void
}
const fg = (color) => '{' + color + '-fg}';
const bg = (color) => '{' + color + '-bg}';
const color = (color) => { return { fg: fg(color), bg: bg(color) } }
export const tags = {
black: color('black'),
red: color('red'),
green: color('green'),
yellow: color('yellow'),
blue: color('blue'),
magenta: color('magenta'),
cyan: color('cyan'),
white: color('white'),
reset: '{/}',
bright: '{bold}'
};
export const boxStyle = () => {
return {
style: {
border: {
fg: 'white'
fg: getTheme().border.normal
},
focus: {
border: {
fg: 'cyan'
fg: getTheme().border.focused
}
}
},
@ -79,7 +64,7 @@ const titleBar = blessed.box({
});
export function setTitle(title) {
titleBar.setContent(` ${title}{|}{bold}{black-fg}v0.1.0 {/}`);
titleBar.setContent(` ${getTheme().header(title)}{|}${getTheme().subheader('v0.1.0')} {/}`);
}
setTitle('');
@ -94,7 +79,9 @@ process.stdout.write(ansi.cursor.hide);
screen.key(['C-c'], function(ch, key) {
process.stdout.write(ansi.cursor.show);
return process.exit(0);
setTimeout(_ => {
process.exit(0);
})
});
tasksPanel.key('f2', () => {

View File

@ -118,6 +118,15 @@ frigid@^1.3.8:
resolved "https://registry.yarnpkg.com/frigid/-/frigid-1.3.8.tgz#a16919821e5426344bc98d301099f7631d2bae8a"
integrity sha512-i3HgB/5hQsALyumWoRlBvEpAXfTmM3Xw+Ica6E9mTASUVYtqZQ8mzUX8/3zscTUM4bCKhSa7MSvXl9L7pt5ICg==
fs-extra@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -134,6 +143,11 @@ get-port@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
@ -169,6 +183,15 @@ is-regex@^1.0.4:
call-bind "^1.0.2"
has-symbols "^1.0.2"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
logger@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/logger/-/logger-0.0.1.tgz#cb08171f8a6f6f674b8499dadf50bed4befb72c4"
@ -232,6 +255,11 @@ typescript@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"