Skip to content

Commit

Permalink
add import from cli args with test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
vitormv committed Jan 22, 2024
1 parent 847e5f8 commit ad6ee9e
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 73 deletions.
2 changes: 0 additions & 2 deletions src/components/TerminalWindow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@
});
</script>

<!-- @todo: toggle show loading, header, preview -->

<ExportOptions />

<div bind:this={wrapperEl} class="wrapper" style={allTokenVariables}>
Expand Down
72 changes: 6 additions & 66 deletions src/data/export/exportToEnvVariable.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,10 @@
import { isValidColor, type ColorValues } from '~/data/colors.store';
import { isValidOption, type ThemeOption, type ThemeOptions } from '~/data/options.store';
import { fzfOptionsConfig, type FzfOptionDefinition } from '~/data/fzfOptions.config';
import { isValidOption, type ThemeOptions } from '~/data/options.store';
import { colorDefinitions } from '~/fzf/fzfColorDefinitions';
import { arrayChunk } from '~/utils/arrayChunk';
import { toFzfColorName } from '~/utils/colors/toFzfColorName';

type ExportItemDefinition<T extends ThemeOption> = {
exportVariable: string;
exportIf?: (val: string, allOptions: ThemeOptions) => boolean;
format?: (val: ThemeOptions[T], allOptions: ThemeOptions) => string;
};

export type ExportDefinitions = {
[K in ThemeOption]: ExportItemDefinition<K>;
};

const envExportConfiguration: ExportDefinitions = {
margin: {
exportVariable: 'margin',
exportIf: (val) => val !== '0',
},
padding: {
exportVariable: 'padding',
exportIf: (val) => val !== '0',
},
borderStyle: {
exportVariable: 'border',
exportIf: (val) => val !== 'none',
},
borderLabel: {
exportVariable: 'border-label',
exportIf: (_, allOptions) => allOptions.borderStyle !== 'none',
},
borderLabelPosition: {
exportVariable: 'border-label-pos',
exportIf: (_, allOptions) => !!allOptions.borderLabel,
},
previewBorderStyle: {
exportVariable: 'preview-window',
exportIf: (val) => val !== 'none',
format: (val) => `border-${val}`,
},
separator: {
exportVariable: 'separator',
},
scrollbar: {
exportVariable: 'scrollbar',
},
prompt: {
exportVariable: 'prompt',
},
pointer: {
exportVariable: 'pointer',
},
marker: {
exportVariable: 'marker',
},
layout: {
exportVariable: 'layout',
exportIf: (val) => val !== 'default',
},
info: {
exportVariable: 'info',
exportIf: (val) => val !== 'default',
},
};

const sanitize = (str: string) => {
return `"${str}"`;
};
Expand All @@ -89,19 +29,19 @@ const prepareForEnvExport = (themeOptions: ThemeOptions, colors: ColorValues) =>
Object.keys(themeOptions).forEach((key) => {
if (!isValidOption(key)) return;

const conf = envExportConfiguration[key] as ExportItemDefinition<typeof key>;
const conf = fzfOptionsConfig[key] as FzfOptionDefinition<typeof key>;
const storeValue = themeOptions[key];

if (!conf) return;

const formatted = conf.format
? conf.format(String(storeValue), themeOptions)
const formatted = conf.transformExport
? conf.transformExport(String(storeValue), themeOptions)
: String(storeValue);

// abort early if shouldn't export
if (conf.exportIf && !conf.exportIf(formatted, themeOptions)) return;

optionsVariables.set(conf.exportVariable, formatted);
optionsVariables.set(conf.argName, formatted);
});

return { optionsVariables, colorVariables };
Expand Down
63 changes: 63 additions & 0 deletions src/data/fzfOptions.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type ThemeOption, type ThemeOptions } from '~/data/options.store';

export type FzfOptionDefinition<T extends ThemeOption> = {
argName: string;
exportIf?: (val: string, allOptions: ThemeOptions) => boolean;
transformExport?: (val: ThemeOptions[T], allOptions: ThemeOptions) => string;
transformImport?: (val: any) => string;
};

export type FzfOptions = {
[K in ThemeOption]: FzfOptionDefinition<K>;
};

export const fzfOptionsConfig: FzfOptions = {
margin: {
argName: 'margin',
exportIf: (val) => val !== '0',
},
padding: {
argName: 'padding',
exportIf: (val) => val !== '0',
},
borderStyle: {
argName: 'border',
exportIf: (val) => val !== 'none',
},
borderLabel: {
argName: 'border-label',
exportIf: (_, allOptions) => allOptions.borderStyle !== 'none',
},
borderLabelPosition: {
argName: 'border-label-pos',
exportIf: (_, allOptions) => !!allOptions.borderLabel,
},
previewBorderStyle: {
argName: 'preview-window',
transformExport: (val) => `border-${val}`,
transformImport: (val: unknown) => String(val).substring(7), // remove "border-" prefix
},
separator: {
argName: 'separator',
},
scrollbar: {
argName: 'scrollbar',
},
prompt: {
argName: 'prompt',
},
pointer: {
argName: 'pointer',
},
marker: {
argName: 'marker',
},
layout: {
argName: 'layout',
exportIf: (val) => val !== 'default',
},
info: {
argName: 'info',
exportIf: (val) => val !== 'default',
},
};
147 changes: 147 additions & 0 deletions src/data/import/importFromEnvArgs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { expect, it, describe } from 'vitest';
import { type ColorValues } from '~/data/colors.store';
import { importFromEnvArgs } from '~/data/import/importFromEnvArgs';

import { initialOptions, type ThemeOptions } from '~/data/options.store';

const sampleOptions: ThemeOptions = {
borderStyle: 'block',
borderLabel: ' Geralt of Rivia ',
borderLabelPosition: 10,
previewBorderStyle: 'double',
padding: '2',
margin: '3',
prompt: 'Here: ',
marker: '->',
pointer: '*',
separator: 'LALA ',
scrollbar: '+',
layout: 'reverse',
info: 'right',
};

const sampleColors: ColorValues = {
'fg': '#3d3131',
'fg-plus': '#d0d0d0',
'bg': '#ffdfdf',
'bg-plus': '#262626',
'hl': '#b0b05f',
'hl-plus': '#5fd7ff',
'info': '#054545',
'marker': '#87ffe5',
'prompt': '#1500d6',
'spinner': '#b60000',
'pointer': '#5ebcff',
'header': '#00ff66',
'gutter': '#1d350f',
'border': '#2200ff',
'separator': '#9e5757',
'scrollbar': '#ff00d0',
'preview-fg': '#b2c602',
'preview-bg': '#002309',
'preview-border': '#9b2f2f',
'preview-scrollbar': '#ffe100',
'preview-label': '#4f1212',
'label': '#ff93e9',
'query': '#ff0000',
'disabled': '#9a0000',
};

describe('importFromEnvArgs()', () => {
it('should parse full cli args', () => {
const output = importFromEnvArgs(`
--color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272
--color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5
--color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66
--color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0
--color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100
--color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000
--border="block" --border-label=" Geralt of Rivia " --border-label-pos="10" --preview-window="border-double"
--margin="3" --padding="2" --prompt="Here: " --pointer="*"
--marker="->" --separator="LALA " --scrollbar="+" --layout="reverse"
--info="right"
`);

expect(output).toEqual({
themeOptions: sampleOptions,
colors: sampleColors,
});
});

it('should parse colors-only obj', () => {
const output = importFromEnvArgs(`
--color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272
--color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5
--color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66
--color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0
--color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100
--color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000
`);

expect(output).toEqual({
themeOptions: initialOptions, // will use default
colors: sampleColors,
});
});

it('should be able to parse unset colors', () => {
const output = importFromEnvArgs(`
--color=fg:-1,bg:-1
--color=fg+:#5f3952,,bg+:#af7272
--color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5
--color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66
--color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0
--color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100
--color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000
`);

expect(output).toEqual({
themeOptions: initialOptions, // will use default
colors: {
...sampleColors,
fg: '',
bg: '',
},
});
});

it('should be able to parse empty colors', () => {
// gutter is missing:
const output = importFromEnvArgs(`
--color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272
--color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5
--color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66
--color=border:#2200ff,separator:#9e5757,scrollbar:#ff00d0
--color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100
--color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000
`);

expect(output).toEqual({
themeOptions: initialOptions,
colors: {
...sampleColors,
gutter: '',
},
});
});

it('should recover from wrong option values', () => {
const output = importFromEnvArgs(`
--color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272
--color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5
--color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66
--color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0
--color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100
--color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000
--border="WHAT" --border-label-pos="THIS AINT RIGHT" --preview-window="NOPE"
--margin="3,3,3,3,3" --padding="2,2,2,2,2,2,2,2" --pointer="TOOLONG"
--marker="TOOLONG" --scrollbar="TOOLONG" --layout="WAT"
--info="WAT"
`);

expect(output).toEqual({
themeOptions: initialOptions,
colors: sampleColors,
});
});
});
42 changes: 42 additions & 0 deletions src/data/import/importFromEnvArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import parser from 'yargs-parser';
import { fzfOptionsConfig } from '~/data/fzfOptions.config';
import { validateAndParseColors } from '~/data/import/validateAndParseColors';
import { validateAndParseThemeOptions } from '~/data/import/validateAndParseThemeOptions';
import { isValidOption, type ThemeOption, type ThemeOptions } from '~/data/options.store';
import { toFzfColorName } from '~/utils/colors/toFzfColorName';

export const importFromEnvArgs = (argsStr: string) => {
const parsedObj = parser(argsStr.replace(/\n/g, ' '));
const themeOptions: Partial<ThemeOptions> = {};
const colors: Record<string, string> = {};

for (const argKey in parsedObj) {
if (argKey === 'color') {
if (Array.isArray(parsedObj[argKey])) {
const splitColors = String(parsedObj[argKey]).split(',');

for (const color of splitColors) {
let [colorName, colorValue] = String(color).split(':');

colorValue = String(colorValue).trim();

colors[toFzfColorName(colorName)] = colorValue === '-1' ? '' : colorValue;
}
}
}

const optionName = Object.keys(fzfOptionsConfig).find(
(i) => fzfOptionsConfig[i as ThemeOption]?.argName === argKey,
);

if (!optionName || !isValidOption(optionName)) continue;

themeOptions[optionName] =
fzfOptionsConfig[optionName].transformImport?.(parsedObj[argKey]) ?? parsedObj[argKey];
}

return {
themeOptions: validateAndParseThemeOptions(themeOptions),
colors: validateAndParseColors(colors),
};
};
2 changes: 1 addition & 1 deletion src/data/import/validateAndParseThemeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('validateAndParseThemeOptions()', () => {
borderLabelPosition: new Error(),
previewBorderStyle: 10,
padding: 0,
margin: 2,
margin: ',',
prompt: null,
marker: undefined,
pointer: 2,
Expand Down
2 changes: 1 addition & 1 deletion src/data/import/validateAndParseThemeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const validateAndParseThemeOptions = (rawObj: Record<string, any>) => {

const parsed = themeOptionsSchema.shape[key as ThemeOption].safeParse(receivedValue);

normalizedObj[key] = parsed.success ? receivedValue : defaultValue;
normalizedObj[key] = parsed.success ? (parsed.data as any) : defaultValue;
}

return normalizedObj as ThemeOptions;
Expand Down
6 changes: 3 additions & 3 deletions src/data/options.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export const themeOptionsSchema = z.object({
previewBorderStyle: z
.enum(['rounded', 'sharp', 'bold', 'double', 'block', 'thinblock'])
.default('rounded'),
margin: z
margin: z.coerce
.string()
.regex(/^[0-9]+(,[0-9]+){0,3}$/)
.default('0'),
padding: z
padding: z.coerce
.string()
.regex(/^[0-9]+(,[0-9]+){0,3}$/)
.default('0'),
Expand All @@ -23,5 +23,5 @@ export const themeOptionsSchema = z.object({
separator: z.string().default('─'),
scrollbar: z.string().max(1).default('│'),
layout: z.enum(['default', 'reverse', 'reverse-list']).default('default'),
info: z.enum(['default', 'right']).default('default').default('default'),
info: z.enum(['default', 'right']).default('default'),
});

0 comments on commit ad6ee9e

Please sign in to comment.