shell-logo 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-logo",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Render colorful ASCII art logos in the terminal from a config file",
6
6
  "license": "MIT",
package/src/config.js CHANGED
@@ -1,22 +1,35 @@
1
+ /**
2
+ * Config loading with XDG-first resolution and legacy migration.
3
+ *
4
+ * Load order:
5
+ * 1. XDG path (~/.config/shell-logo/folders/<hash>/config.json)
6
+ * 2. Legacy path (.shell-logo.json in the working directory)
7
+ *
8
+ * If a legacy config is found but no XDG config exists, the legacy config
9
+ * is automatically copied to XDG (the legacy file is left untouched).
10
+ */
11
+
1
12
  import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
13
  import chalk from 'chalk';
14
+ import { configPath, legacyConfigPath } from './paths.js';
15
+ import { writeConfig } from './generate.js';
16
+ import { SHAPE_NAMES } from './shapes.js';
4
17
 
5
18
  export const DEFAULTS = {
6
19
  colors: ['#ff6b6b', '#feca57', '#48dbfb'],
7
20
  font: 'Standard',
8
21
  };
9
22
 
10
- export function tryLoadConfig() {
11
- const configPath = join(process.cwd(), '.shell-logo.json');
12
-
13
- let raw;
14
- try {
15
- raw = readFileSync(configPath, 'utf-8');
16
- } catch {
17
- return null;
18
- }
19
-
23
+ /**
24
+ * Parse a JSON string and validate it as a shell-logo config.
25
+ *
26
+ * Supports two modes:
27
+ * - "text" (default): requires non-empty `text`, optional `colors`/`font`
28
+ * - "shape": requires valid `shape` name, optional `colors`, no `text`/`font` needed
29
+ *
30
+ * Returns a normalized config object, or null if invalid.
31
+ */
32
+ function parseAndValidate(raw) {
20
33
  let config;
21
34
  try {
22
35
  config = JSON.parse(raw);
@@ -24,68 +37,92 @@ export function tryLoadConfig() {
24
37
  return null;
25
38
  }
26
39
 
27
- if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
28
- return null;
29
- }
30
-
31
40
  if (config.colors !== undefined) {
32
41
  if (!Array.isArray(config.colors) || config.colors.length < 2) {
33
42
  return null;
34
43
  }
35
44
  }
36
45
 
46
+ const mode = config.mode ?? 'text';
47
+
48
+ if (mode === 'shape') {
49
+ if (!config.shape || !SHAPE_NAMES.includes(config.shape)) {
50
+ return null;
51
+ }
52
+ return {
53
+ mode: 'shape',
54
+ shape: config.shape,
55
+ colors: config.colors ?? DEFAULTS.colors,
56
+ };
57
+ }
58
+
59
+ // mode === 'text' (or absent for backward compatibility)
60
+ if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
61
+ return null;
62
+ }
63
+
37
64
  return {
65
+ mode: 'text',
38
66
  text: config.text.trim(),
39
67
  colors: config.colors ?? DEFAULTS.colors,
40
68
  font: config.font ?? DEFAULTS.font,
41
69
  };
42
70
  }
43
71
 
44
- export function loadConfig() {
45
- const configPath = join(process.cwd(), '.shell-logo.json');
46
-
47
- let raw;
72
+ /**
73
+ * Try to load config for `cwd`. Returns the parsed config or null.
74
+ * Checks XDG first, then falls back to legacy .shell-logo.json.
75
+ * Auto-migrates legacy configs to XDG on first load.
76
+ */
77
+ export function tryLoadConfig(cwd) {
78
+ // 1. Try XDG path
48
79
  try {
49
- raw = readFileSync(configPath, 'utf-8');
80
+ const raw = readFileSync(configPath(cwd), 'utf-8');
81
+ const config = parseAndValidate(raw);
82
+ if (config) return config;
50
83
  } catch {
51
- console.error(
52
- chalk.red('Error: ') +
53
- 'No .shell-logo.json found in the current directory.\n\n' +
54
- 'Create one with at least:\n\n' +
55
- ' { "text": "HELLO" }\n\n' +
56
- 'Or copy an example:\n\n' +
57
- ' cp node_modules/shell-logo/examples/.shell-logo.json .'
58
- );
59
- process.exit(1);
84
+ // Not found or unreadable — fall through to legacy
60
85
  }
61
86
 
62
- let config;
87
+ // 2. Try legacy .shell-logo.json in the working directory
88
+ let raw;
63
89
  try {
64
- config = JSON.parse(raw);
90
+ raw = readFileSync(legacyConfigPath(cwd), 'utf-8');
65
91
  } catch {
66
- console.error(chalk.red('Error: ') + '.shell-logo.json contains invalid JSON.');
67
- process.exit(1);
92
+ return null;
68
93
  }
69
94
 
70
- if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
71
- console.error(
72
- chalk.red('Error: ') + '"text" is required and must be a non-empty string in .shell-logo.json.'
95
+ const config = parseAndValidate(raw);
96
+ if (!config) return null;
97
+
98
+ // 3. Auto-migrate legacy config to XDG (legacy file is left untouched)
99
+ try {
100
+ writeConfig(config, cwd);
101
+ const xdgPath = configPath(cwd);
102
+ console.log(
103
+ chalk.dim(`Migrated config from .shell-logo.json → ${xdgPath}`)
73
104
  );
74
- process.exit(1);
105
+ } catch {
106
+ // Migration failed — still usable from the legacy path
75
107
  }
76
108
 
77
- if (config.colors !== undefined) {
78
- if (!Array.isArray(config.colors) || config.colors.length < 2) {
79
- console.error(
80
- chalk.red('Error: ') + '"colors" must be an array of at least 2 color strings.'
81
- );
82
- process.exit(1);
83
- }
84
- }
109
+ return config;
110
+ }
85
111
 
86
- return {
87
- text: config.text.trim(),
88
- colors: config.colors ?? DEFAULTS.colors,
89
- font: config.font ?? DEFAULTS.font,
90
- };
112
+ /**
113
+ * Load config for `cwd`, or exit with an error if none is found.
114
+ * Same resolution as tryLoadConfig but treats missing config as fatal.
115
+ */
116
+ export function loadConfig(cwd) {
117
+ const config = tryLoadConfig(cwd);
118
+ if (config) return config;
119
+
120
+ const xdg = configPath(cwd);
121
+ console.error(
122
+ chalk.red('Error: ') +
123
+ `No config found for this folder.\n\n` +
124
+ `Expected at: ${xdg}\n\n` +
125
+ 'Run shell-logo and choose "Generate" to create one.'
126
+ );
127
+ process.exit(1);
91
128
  }
package/src/generate.js CHANGED
@@ -1,8 +1,46 @@
1
- import { writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ /**
2
+ * Config persistence writes config to the XDG folder structure.
3
+ *
4
+ * Uses atomic writes (write to tmp file, then rename) to prevent corruption
5
+ * if two terminals write to the same folder config simultaneously.
6
+ */
3
7
 
4
- export function writeConfig(config) {
5
- const configPath = join(process.cwd(), '.shell-logo.json');
6
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
7
- return configPath;
8
+ import { writeFileSync, renameSync, mkdirSync } from 'node:fs';
9
+ import { resolve, join, dirname } from 'node:path';
10
+ import { randomBytes } from 'node:crypto';
11
+ import { configPath, folderConfigDir, metaPath } from './paths.js';
12
+
13
+ /**
14
+ * Write data to a file atomically: write to a temp file in the same
15
+ * directory, then rename. This avoids partial reads if another process
16
+ * is reading the file at the same time.
17
+ */
18
+ function atomicWriteSync(targetPath, data) {
19
+ const dir = dirname(targetPath);
20
+ const tmpPath = join(dir, `.tmp.${randomBytes(6).toString('hex')}`);
21
+ writeFileSync(tmpPath, data);
22
+ renameSync(tmpPath, targetPath);
23
+ }
24
+
25
+ /**
26
+ * Persist a config object to the XDG folder for `cwd`.
27
+ * Creates the directory tree if it doesn't exist yet.
28
+ * Also writes a meta.json with the original folder path (best-effort).
29
+ * Returns the path the config was written to.
30
+ */
31
+ export function writeConfig(config, cwd = process.cwd()) {
32
+ const dir = folderConfigDir(cwd);
33
+ mkdirSync(dir, { recursive: true });
34
+
35
+ atomicWriteSync(configPath(cwd), JSON.stringify(config, null, 2) + '\n');
36
+
37
+ // meta.json stores the original folder path so humans can identify
38
+ // which hash directory belongs to which project.
39
+ try {
40
+ atomicWriteSync(metaPath(cwd), JSON.stringify({ path: resolve(cwd) }, null, 2) + '\n');
41
+ } catch {
42
+ // Best-effort — not critical if this fails
43
+ }
44
+
45
+ return configPath(cwd);
8
46
  }
package/src/index.js CHANGED
@@ -1,67 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ /**
4
+ * Entry point for shell-logo.
5
+ *
6
+ * 1. Run the interactive UI to get a config + session mode (persistent or temporary).
7
+ * 2. If the user chose "Generate", persist the new config to disk.
8
+ * 3. Start the fullscreen render loop with arrow-key theme/font cycling.
9
+ */
10
+
5
11
  import { runInteractiveUI } from './ui.js';
6
12
  import { writeConfig } from './generate.js';
7
13
  import { render } from './renderer.js';
8
14
  import { getTerminalSize, clearScreen, hideCursor, showCursor, centerContent } from './terminal.js';
9
15
  import { THEMES, FONTS } from './themes.js';
16
+ import { SHAPES } from './shapes.js';
10
17
  import chalk from 'chalk';
11
18
  import * as p from '@clack/prompts';
12
19
 
13
- const { action, config } = await runInteractiveUI();
20
+ const { action, config, persistent } = await runInteractiveUI();
14
21
 
15
22
  if (action === 'generate') {
16
23
  const s = p.spinner();
17
- s.start('Writing .shell-logo.json...');
24
+ s.start('Saving config...');
18
25
  writeConfig(config);
19
26
  s.stop('Config saved!');
20
-
21
- const isGitRepo = existsSync(join(process.cwd(), '.git'));
22
- const gitignorePath = join(process.cwd(), '.gitignore');
23
- let shouldPrompt = isGitRepo;
24
-
25
- if (existsSync(gitignorePath)) {
26
- const content = readFileSync(gitignorePath, 'utf-8');
27
- const lines = content.split('\n').map(l => l.trim());
28
- if (lines.includes('.shell-logo.json')) {
29
- shouldPrompt = false;
30
- }
31
- }
32
-
33
- if (shouldPrompt) {
34
- const addToGitignore = await p.select({
35
- message: 'Add .shell-logo.json to .gitignore?',
36
- options: [
37
- { value: true, label: 'Yes (recommended)' },
38
- { value: false, label: 'No' },
39
- ],
40
- });
41
-
42
- if (p.isCancel(addToGitignore)) {
43
- p.cancel('Cancelled.');
44
- process.exit(0);
45
- }
46
-
47
- if (addToGitignore) {
48
- if (existsSync(gitignorePath)) {
49
- const existing = readFileSync(gitignorePath, 'utf-8');
50
- const separator = existing.endsWith('\n') ? '' : '\n';
51
- writeFileSync(gitignorePath, existing + separator + '.shell-logo.json\n');
52
- } else {
53
- writeFileSync(gitignorePath, '.shell-logo.json\n');
54
- }
55
- p.log.success('.gitignore updated.');
56
- }
57
- }
58
27
  }
59
28
 
60
- startRenderLoop(config);
61
-
62
- function startRenderLoop(config) {
29
+ startRenderLoop(config, persistent);
30
+
31
+ /**
32
+ * Fullscreen render loop. Draws the logo centered in the terminal and
33
+ * listens for arrow keys to cycle themes (up/down) and fonts (left/right).
34
+ *
35
+ * @param {object} config - Logo config: { text, colors, font }
36
+ * @param {boolean} persistent - When true, arrow key changes are saved to disk.
37
+ * When false (temporary session), changes are in-memory only.
38
+ */
39
+ function startRenderLoop(config, persistent) {
63
40
  let resizeTimer;
64
41
 
42
+ const isShapeMode = config.mode === 'shape';
43
+
65
44
  // Find the current theme index by matching colors
66
45
  let themeIndex = THEMES.findIndex(
67
46
  (t) => JSON.stringify(t.colors) === JSON.stringify(config.colors)
@@ -71,6 +50,11 @@ function startRenderLoop(config) {
71
50
  let fontIndex = FONTS.indexOf(config.font);
72
51
  if (fontIndex === -1) fontIndex = 0;
73
52
 
53
+ let shapeIndex = isShapeMode
54
+ ? SHAPES.findIndex((s) => s.name === config.shape)
55
+ : 0;
56
+ if (shapeIndex === -1) shapeIndex = 0;
57
+
74
58
  let showStatus = false;
75
59
  let statusTimer;
76
60
 
@@ -80,11 +64,15 @@ function startRenderLoop(config) {
80
64
  clearScreen();
81
65
  process.stdout.write(centerContent(art, columns, rows));
82
66
  if (showStatus) {
83
- const status = ` ${THEMES[themeIndex].name} · ${FONTS[fontIndex]} · ↑↓ theme ←→ font q quit`;
67
+ const lrLabel = isShapeMode
68
+ ? `${SHAPES[shapeIndex].name} · ←→ shape`
69
+ : `${FONTS[fontIndex]} · ←→ font`;
70
+ const status = ` ${THEMES[themeIndex].name} · ${lrLabel} · ↑↓ theme q quit`;
84
71
  process.stdout.write(`\x1B[${rows};1H` + chalk.dim(status));
85
72
  }
86
73
  }
87
74
 
75
+ /** Briefly show the status bar (theme name, font, controls) for 2 seconds. */
88
76
  function flashStatus() {
89
77
  showStatus = true;
90
78
  clearTimeout(statusTimer);
@@ -94,6 +82,7 @@ function startRenderLoop(config) {
94
82
  }, 2000);
95
83
  }
96
84
 
85
+ /** Debounce re-renders on terminal resize to avoid flickering. */
97
86
  function debouncedRender() {
98
87
  clearTimeout(resizeTimer);
99
88
  resizeTimer = setTimeout(renderLoop, 50);
@@ -131,16 +120,27 @@ function startRenderLoop(config) {
131
120
  } else if (key[2] === 66) { // Arrow Down — next theme
132
121
  themeIndex = (themeIndex + 1) % THEMES.length;
133
122
  config.colors = THEMES[themeIndex].colors;
134
- } else if (key[2] === 67) { // Arrow Right — next font
135
- fontIndex = (fontIndex + 1) % FONTS.length;
136
- config.font = FONTS[fontIndex];
137
- } else if (key[2] === 68) { // Arrow Left — prev font
138
- fontIndex = (fontIndex - 1 + FONTS.length) % FONTS.length;
139
- config.font = FONTS[fontIndex];
123
+ } else if (key[2] === 67) { // Arrow Right — next font/shape
124
+ if (isShapeMode) {
125
+ shapeIndex = (shapeIndex + 1) % SHAPES.length;
126
+ config.shape = SHAPES[shapeIndex].name;
127
+ } else {
128
+ fontIndex = (fontIndex + 1) % FONTS.length;
129
+ config.font = FONTS[fontIndex];
130
+ }
131
+ } else if (key[2] === 68) { // Arrow Left — prev font/shape
132
+ if (isShapeMode) {
133
+ shapeIndex = (shapeIndex - 1 + SHAPES.length) % SHAPES.length;
134
+ config.shape = SHAPES[shapeIndex].name;
135
+ } else {
136
+ fontIndex = (fontIndex - 1 + FONTS.length) % FONTS.length;
137
+ config.font = FONTS[fontIndex];
138
+ }
140
139
  } else {
141
140
  return;
142
141
  }
143
- writeConfig(config);
142
+ // Only persist theme/font changes in a persistent (non-temporary) session
143
+ if (persistent) writeConfig(config);
144
144
  flashStatus();
145
145
  renderLoop();
146
146
  }
package/src/paths.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Path resolution for XDG-based config storage.
3
+ *
4
+ * Configs are stored per-folder under:
5
+ * $XDG_CONFIG_HOME/shell-logo/folders/<hash>/config.json
6
+ *
7
+ * where <hash> is the first 16 hex chars of SHA-256(absolute CWD).
8
+ * Falls back to ~/.config/shell-logo/ when XDG_CONFIG_HOME is unset.
9
+ */
10
+
11
+ import { createHash } from 'node:crypto';
12
+ import { resolve, join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+
15
+ /** Root directory for all shell-logo config: $XDG_CONFIG_HOME/shell-logo/ */
16
+ export function configRoot() {
17
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
18
+ return join(base, 'shell-logo');
19
+ }
20
+
21
+ /** Deterministic 16-char hex hash of the absolute path for `cwd`. */
22
+ export function folderHash(cwd = process.cwd()) {
23
+ return createHash('sha256').update(resolve(cwd)).digest('hex').slice(0, 16);
24
+ }
25
+
26
+ /** Per-folder config directory: <configRoot>/folders/<hash>/ */
27
+ export function folderConfigDir(cwd = process.cwd()) {
28
+ return join(configRoot(), 'folders', folderHash(cwd));
29
+ }
30
+
31
+ /** Path to the per-folder config file (text, colors, font). */
32
+ export function configPath(cwd = process.cwd()) {
33
+ return join(folderConfigDir(cwd), 'config.json');
34
+ }
35
+
36
+ /** Path to the per-folder meta file (stores original absolute path for debugging). */
37
+ export function metaPath(cwd = process.cwd()) {
38
+ return join(folderConfigDir(cwd), 'meta.json');
39
+ }
40
+
41
+ /** Path to the legacy config file (.shell-logo.json in the working directory). */
42
+ export function legacyConfigPath(cwd = process.cwd()) {
43
+ return join(resolve(cwd), '.shell-logo.json');
44
+ }
package/src/renderer.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import figlet from 'figlet';
2
2
  import gradient from 'gradient-string';
3
3
  import { scaleArt } from './scaling.js';
4
+ import { SHAPES } from './shapes.js';
4
5
 
5
6
  function figletSync(text, font) {
6
7
  try {
@@ -11,6 +12,18 @@ function figletSync(text, font) {
11
12
  }
12
13
 
13
14
  export function render(config, columns, rows) {
15
+ if (config.mode === 'shape') {
16
+ const shape = SHAPES.find((s) => s.name === config.shape);
17
+ if (!shape) {
18
+ const grad = gradient(config.colors);
19
+ return grad(config.shape);
20
+ }
21
+ const trimmed = shape.art.trim();
22
+ const scaled = scaleArt(trimmed, columns, rows - 2);
23
+ const grad = gradient(config.colors);
24
+ return grad.multiline(scaled);
25
+ }
26
+
14
27
  const art = figletSync(config.text, config.font);
15
28
  if (!art) {
16
29
  const grad = gradient(config.colors);
package/src/shapes.js ADDED
@@ -0,0 +1,239 @@
1
+ export const SHAPES = [
2
+ {
3
+ name: 'Pixel',
4
+ art: `
5
+ ▄█▄ ▄█▄
6
+ █████ ▄▄▄ █████
7
+ ████████████████
8
+ ██ ▀ █████ ▀ ██
9
+ ████████████████
10
+ █████ ▀▀▀ █████
11
+ ██████████████
12
+ ████████████
13
+ ██ █████ ██
14
+ ▀▀ ▀▀
15
+ `,
16
+ },
17
+ {
18
+ name: 'Bloop',
19
+ art: `
20
+ ▄████████▄
21
+ ██████████████
22
+ ████████████████
23
+ ███▀██████▀█████
24
+ ████████████████
25
+ █████ ▀▀▀▀ █████
26
+ ████████████████
27
+ ██████████████
28
+ ▀██████████▀
29
+ ▀▀ ▀▀
30
+ `,
31
+ },
32
+ {
33
+ name: 'Octo',
34
+ art: `
35
+ ▄████████▄
36
+ ██████████████
37
+ ████████████████
38
+ ███▀██████▀█████
39
+ ████████████████
40
+ ██████████████
41
+ ▐█▌▐█▌ ██ ▐█▌▐█▌
42
+ ▐█▌▐█▌ ██ ▐█▌▐█▌
43
+ ▀ ▀ ▀▀ ▀ ▀
44
+ `,
45
+ },
46
+ {
47
+ name: 'Ghosty',
48
+ art: `
49
+ ▄████████▄
50
+ ██████████████
51
+ ████████████████
52
+ ████▀████▀██████
53
+ ████████████████
54
+ ████████████████
55
+ ████████████████
56
+ ████████████████
57
+ █▀█ █▀██ █▀█ █▀
58
+ `,
59
+ },
60
+ {
61
+ name: 'Foxie',
62
+ art: `
63
+ ▄██▄ ▄██▄
64
+ ████▄ ▄████
65
+ ██████▄▄▄▄▄▄▄▄██████
66
+ ████ ▀████████▀ ████
67
+ █████ ▀██████▀ █████
68
+ ████████████████████
69
+ ██████████████████
70
+ ████████████████
71
+ ██████████████
72
+ ▀██████████▀
73
+ `,
74
+ },
75
+ {
76
+ name: 'Ducky',
77
+ art: `
78
+ ▄████▄
79
+ ████████▄
80
+ ██▀████████▄
81
+ ██████████████
82
+ ████████████████
83
+ █████████████▀
84
+ ▀██████████
85
+ ▀████████▀
86
+ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
87
+ `,
88
+ },
89
+ {
90
+ name: 'Robo',
91
+ art: `
92
+ ▄█▄
93
+ ▄█████▄
94
+ ██████████
95
+ ███▀██▀███
96
+ ██████████
97
+ ███▄▀▀▄███
98
+ ██████████
99
+ ▐██████████▌
100
+ ███ ████ ██
101
+ ██ ██
102
+ ▀▀ ▀▀
103
+ `,
104
+ },
105
+ {
106
+ name: 'Cactus',
107
+ art: `
108
+ ▄██▄
109
+ ████
110
+ ▄██▄ ████
111
+ ████ ████ ▄██▄
112
+ ████ ████ ████
113
+ ▀███▄████▄███▀
114
+ ████
115
+ ████
116
+ ██████
117
+ ▄████████▄
118
+ ██████████
119
+ `,
120
+ },
121
+ {
122
+ name: 'Rocket',
123
+ art: `
124
+ ▄▄
125
+ ████
126
+ ██████
127
+ ██████
128
+ ████████
129
+ ██ ██ ██
130
+ ████████
131
+ ██████████
132
+ ▄██████████▄
133
+ ██▀██████▀██
134
+ ██████
135
+ ▐██████▌
136
+ ▄██▀ ▀██▄
137
+ ▀▀ ▀▀
138
+ `,
139
+ },
140
+ {
141
+ name: 'Skull',
142
+ art: `
143
+ ▄██████▄
144
+ ████████████
145
+ ██████████████
146
+ ███▀██████▀███
147
+ ██████████████
148
+ █████▄▄█████
149
+ ████████████
150
+ █▀█ ▀▀ █▀█
151
+ █ █ ██ █ █
152
+ ▀ ▀▀ ▀
153
+ `,
154
+ },
155
+ {
156
+ name: 'Penguin',
157
+ art: `
158
+ ▄████▄
159
+ ████████████
160
+ ██████████████
161
+ ████▀████▀████
162
+ █████▀▀▀▀█████
163
+ ██▌ ▄████▄ ▐██
164
+ ██▌ ██████ ▐██
165
+ ██▌ ██████ ▐██
166
+ ▀▌ ██████ ▐▀
167
+ ██████
168
+ ▄██ ██▄
169
+ ▀▀▀ ▀▀▀
170
+ `,
171
+ },
172
+ {
173
+ name: 'Mushroom',
174
+ art: `
175
+ ▄████▄
176
+ ▄████████▄
177
+ ██ ▀████▀ ██
178
+ ██████████████
179
+ ██ ▀████▀ ██
180
+ ██████████████
181
+ ▀██████████▀
182
+ ██████
183
+ ██████
184
+ ██████
185
+ ████████
186
+ ▀▀▀▀▀▀▀▀
187
+ `,
188
+ },
189
+ {
190
+ name: 'Alien',
191
+ art: `
192
+ ▄██████████▄
193
+ ██████████████
194
+ ██ ▄████████▄ ██
195
+ ██ █▀██████▀█ ██
196
+ ██ ██████████ ██
197
+ ████ ▀▀▀▀ ████
198
+ ▀██████████▀
199
+ ████████
200
+ ██████
201
+ ████
202
+ ████
203
+ `,
204
+ },
205
+ {
206
+ name: 'Coffee',
207
+ art: `
208
+ ▐█▌ ▐█▌ ▐█▌
209
+ ▐█▌ ▐█▌ ▐█▌
210
+ ▐█▌ ▐█▌ ▐█▌
211
+ ████████████████
212
+ ████████████████▄
213
+ █████████████████
214
+ █████████████████
215
+ ████████████████▀
216
+ ████████████████
217
+ ▀████████████▀
218
+ ████████████████
219
+ `,
220
+ },
221
+ {
222
+ name: 'Diamond',
223
+ art: `
224
+ ▄██▄
225
+ ██████
226
+ ████████
227
+ ██████████
228
+ ████████████
229
+ ██████████████
230
+ ████████████
231
+ ██████████
232
+ ████████
233
+ ██████
234
+ ▀██▀
235
+ `,
236
+ },
237
+ ];
238
+
239
+ export const SHAPE_NAMES = SHAPES.map((s) => s.name);
package/src/themes.js CHANGED
@@ -1,23 +1,68 @@
1
+ /**
2
+ * Font and gradient theme definitions for shell-logo.
3
+ *
4
+ * FONTS — Figlet font names available for text-mode logos.
5
+ * The render loop in index.js cycles through these with arrow keys.
6
+ * Indices 0–14 are the original set; new entries are appended so
7
+ * saved configs that store a font name keep working.
8
+ *
9
+ * THEMES — Named gradient color palettes (3 hex stops each).
10
+ * Used by gradient-string to colorize the rendered ASCII art.
11
+ */
12
+
13
+ // ── Fonts ──────────────────────────────────────────────────────────────────
14
+
1
15
  export const FONTS = [
16
+ // Classic / general-purpose
2
17
  'Standard', 'Big', 'ANSI Shadow', 'Slant', 'Small',
3
18
  'ANSI Regular', 'Bloody', 'DOS Rebel', 'Graffiti', 'Larry 3D',
4
19
  'Star Wars', 'Doh', 'Ghost', 'Fraktur', 'Fire Font-k',
20
+
21
+ // Block / pixel style — use filled Unicode characters (▄█▀) instead of
22
+ // typical ASCII slashes and pipes, giving a "painted pixels" look.
23
+ 'Block', 'Blocks', 'Shaded Blocky', 'Small Block',
24
+ 'Dot Matrix', 'Tiles', 'Rectangles',
25
+
26
+ // Popular classic figlet fonts
27
+ 'Doom', 'Banner3-D', 'Colossal', 'Epic',
28
+ 'Calvin S', 'Speed', '3D-ASCII', 'Isometric1',
5
29
  ];
6
30
 
31
+ // ── Gradient themes ────────────────────────────────────────────────────────
32
+
7
33
  export const THEMES = [
8
- { name: 'Sunset', colors: ['#ff6b6b', '#ff9f43', '#feca57'] },
9
- { name: 'Ocean', colors: ['#0abde3', '#48dbfb', '#54a0ff'] },
10
- { name: 'Forest', colors: ['#1dd1a1', '#2ed573', '#26de81'] },
11
- { name: 'Neon', colors: ['#c56cf0', '#fd79a8', '#ff6b6b'] },
12
- { name: 'Twilight', colors: ['#5f27cd', '#c56cf0', '#48dbfb'] },
13
- { name: 'Fire', colors: ['#ff4757', '#ff6348', '#ffdd59'] },
14
- { name: 'Pastel', colors: ['#fd79a8', '#feca57', '#48dbfb'] },
15
- { name: 'Monochrome', colors: ['#ffffff', '#54a0ff', '#5f27cd'] },
16
- { name: 'Aurora', colors: ['#00d2ff', '#3a7bd5', '#7b2ff7'] },
17
- { name: 'Cherry', colors: ['#eb3349', '#f45c43', '#ff8a80'] },
18
- { name: 'Cyberpunk', colors: ['#f72585', '#7209b7', '#4cc9f0'] },
19
- { name: 'Arctic', colors: ['#e0eafc', '#cfdef3', '#74b9ff'] },
20
- { name: 'Emerald', colors: ['#11998e', '#38ef7d', '#b8ff96'] },
21
- { name: 'Midnight', colors: ['#0f0c29', '#302b63', '#24c6dc'] },
22
- { name: 'Rose Gold', colors: ['#f4c4f3', '#fc5c7d', '#fda085'] },
34
+ // Warm
35
+ { name: 'Sunset', colors: ['#ff6b6b', '#ff9f43', '#feca57'] },
36
+ { name: 'Fire', colors: ['#ff4757', '#ff6348', '#ffdd59'] },
37
+ { name: 'Cherry', colors: ['#eb3349', '#f45c43', '#ff8a80'] },
38
+ { name: 'Rose Gold', colors: ['#f4c4f3', '#fc5c7d', '#fda085'] },
39
+ { name: 'Magma', colors: ['#f83600', '#f9d423', '#fe8c00'] },
40
+ { name: 'Sunrise', colors: ['#ff512f', '#f09819', '#ffed4a'] },
41
+ { name: 'Copper', colors: ['#b87333', '#da9855', '#f0c27f'] },
42
+
43
+ // Cool
44
+ { name: 'Ocean', colors: ['#0abde3', '#48dbfb', '#54a0ff'] },
45
+ { name: 'Arctic', colors: ['#e0eafc', '#cfdef3', '#74b9ff'] },
46
+ { name: 'Frost', colors: ['#e8f0ff', '#b8d4f0', '#88b8e0'] },
47
+ { name: 'Electric', colors: ['#00f2fe', '#4facfe', '#667eea'] },
48
+
49
+ // Green
50
+ { name: 'Forest', colors: ['#1dd1a1', '#2ed573', '#26de81'] },
51
+ { name: 'Emerald', colors: ['#11998e', '#38ef7d', '#b8ff96'] },
52
+ { name: 'Toxic', colors: ['#a8ff78', '#78ffd6', '#00e676'] },
53
+ { name: 'Matrix', colors: ['#00ff41', '#008f11', '#003b00'] },
54
+
55
+ // Purple / pink
56
+ { name: 'Neon', colors: ['#c56cf0', '#fd79a8', '#ff6b6b'] },
57
+ { name: 'Twilight', colors: ['#5f27cd', '#c56cf0', '#48dbfb'] },
58
+ { name: 'Cyberpunk', colors: ['#f72585', '#7209b7', '#4cc9f0'] },
59
+ { name: 'Grape', colors: ['#6a0572', '#ab83a1', '#e0aaff'] },
60
+ { name: 'Lavender Dream', colors: ['#a18cd1', '#fbc2eb', '#f6d5f7'] },
61
+ { name: 'Bubblegum', colors: ['#ff77ab', '#ff99cc', '#ffbbdd'] },
62
+
63
+ // Multi / neutral
64
+ { name: 'Pastel', colors: ['#fd79a8', '#feca57', '#48dbfb'] },
65
+ { name: 'Monochrome', colors: ['#ffffff', '#54a0ff', '#5f27cd'] },
66
+ { name: 'Aurora', colors: ['#00d2ff', '#3a7bd5', '#7b2ff7'] },
67
+ { name: 'Midnight', colors: ['#0f0c29', '#302b63', '#24c6dc'] },
23
68
  ];
package/src/ui.js CHANGED
@@ -1,6 +1,22 @@
1
+ /**
2
+ * Interactive CLI prompts for shell-logo.
3
+ *
4
+ * When a config already exists for the current folder, presents three options:
5
+ * - Run — display the logo, arrow key changes persist to disk
6
+ * - New session — display the logo, changes are in-memory only (temporary)
7
+ * - Generate — walk through the setup wizard to create a new config
8
+ *
9
+ * When no config exists, only the Generate option is shown.
10
+ *
11
+ * Returns { action, config, persistent } where `persistent` controls
12
+ * whether arrow key theme/font changes are saved back to disk.
13
+ */
14
+
1
15
  import * as p from '@clack/prompts';
2
16
  import chalk from 'chalk';
3
17
  import { tryLoadConfig } from './config.js';
18
+ import { SHAPES } from './shapes.js';
19
+ import { FONTS } from './themes.js';
4
20
 
5
21
  const COLOR_PALETTE = [
6
22
  { value: '#ff6b6b', label: `${chalk.bgHex('#ff6b6b')(' ')} Coral` },
@@ -21,24 +37,9 @@ const COLOR_PALETTE = [
21
37
  { value: '#ffffff', label: `${chalk.bgHex('#ffffff')(' ')} White` },
22
38
  ];
23
39
 
24
- const FONT_OPTIONS = [
25
- { value: 'Standard', label: 'Standard' },
26
- { value: 'Big', label: 'Big' },
27
- { value: 'ANSI Shadow', label: 'ANSI Shadow' },
28
- { value: 'Slant', label: 'Slant' },
29
- { value: 'Small', label: 'Small' },
30
- { value: 'ANSI Regular', label: 'ANSI Regular' },
31
- { value: 'Bloody', label: 'Bloody' },
32
- { value: 'DOS Rebel', label: 'DOS Rebel' },
33
- { value: 'Graffiti', label: 'Graffiti' },
34
- { value: 'Larry 3D', label: 'Larry 3D' },
35
- { value: 'Star Wars', label: 'Star Wars' },
36
- { value: 'Doh', label: 'Doh' },
37
- { value: 'Ghost', label: 'Ghost' },
38
- { value: 'Fraktur', label: 'Fraktur' },
39
- { value: 'Fire Font-k', label: 'Fire Font-k' },
40
- ];
40
+ const FONT_OPTIONS = FONTS.map(f => ({ value: f, label: f }));
41
41
 
42
+ /** Exit gracefully if the user presses Ctrl+C / Escape during a prompt. */
42
43
  function handleCancel(value) {
43
44
  if (p.isCancel(value)) {
44
45
  p.cancel('Cancelled.');
@@ -47,18 +48,8 @@ function handleCancel(value) {
47
48
  return value;
48
49
  }
49
50
 
50
- async function promptGenerate() {
51
- const text = handleCancel(
52
- await p.text({
53
- message: 'What text should the logo display?',
54
- placeholder: 'HELLO',
55
- validate: (val) => {
56
- if (!val || val.trim() === '') return 'Text is required.';
57
- },
58
- })
59
- );
60
-
61
- // Pick 3 random unique colors as defaults
51
+ /** Prompt for gradient colors (shared by text and shape modes). */
52
+ async function promptColors() {
62
53
  const shuffled = [...COLOR_PALETTE].sort(() => Math.random() - 0.5);
63
54
  const initialColors = shuffled.slice(0, 3).map(c => c.value);
64
55
 
@@ -76,6 +67,40 @@ async function promptGenerate() {
76
67
  if (colors.length >= 2) break;
77
68
  p.log.warning('Please select at least 2 colors.');
78
69
  }
70
+ return colors;
71
+ }
72
+
73
+ /** Walk the user through the Shape wizard: shape picker, colors. */
74
+ async function promptShapeGenerate() {
75
+ const shape = handleCancel(
76
+ await p.select({
77
+ message: 'Pick a mascot:',
78
+ options: SHAPES.map((s) => ({ value: s.name, label: s.name })),
79
+ })
80
+ );
81
+
82
+ const colors = await promptColors();
83
+
84
+ return {
85
+ action: 'generate',
86
+ config: { mode: 'shape', shape, colors },
87
+ persistent: true,
88
+ };
89
+ }
90
+
91
+ /** Walk the user through the Text wizard: text, colors, font. */
92
+ async function promptTextGenerate() {
93
+ const text = handleCancel(
94
+ await p.text({
95
+ message: 'What text should the logo display?',
96
+ placeholder: 'HELLO',
97
+ validate: (val) => {
98
+ if (!val || val.trim() === '') return 'Text is required.';
99
+ },
100
+ })
101
+ );
102
+
103
+ const colors = await promptColors();
79
104
 
80
105
  const font = handleCancel(
81
106
  await p.select({
@@ -86,22 +111,70 @@ async function promptGenerate() {
86
111
 
87
112
  return {
88
113
  action: 'generate',
89
- config: { text: text.trim(), colors, font },
114
+ config: { mode: 'text', text: text.trim(), colors, font },
115
+ persistent: true,
90
116
  };
91
117
  }
92
118
 
119
+ /** Walk the user through the Generate wizard: mode selection, then mode-specific prompts. */
120
+ async function promptGenerate() {
121
+ const mode = handleCancel(
122
+ await p.select({
123
+ message: 'What kind of logo?',
124
+ options: [
125
+ { value: 'text', label: 'Text', hint: 'type your own text' },
126
+ { value: 'shape', label: 'Shape', hint: 'pick a mascot/pet' },
127
+ ],
128
+ })
129
+ );
130
+
131
+ if (mode === 'shape') return promptShapeGenerate();
132
+ return promptTextGenerate();
133
+ }
134
+
135
+ /**
136
+ * Main entry point for the interactive CLI.
137
+ * Returns { action: 'generate'|'run', config, persistent }.
138
+ */
93
139
  export async function runInteractiveUI() {
94
140
  p.intro(chalk.bold('terminal-logo'));
95
141
 
96
- const hasConfig = !!tryLoadConfig();
142
+ const existingConfig = tryLoadConfig();
143
+
144
+ if (existingConfig) {
145
+ const action = handleCancel(
146
+ await p.select({
147
+ message: 'What would you like to do?',
148
+ initialValue: 'run',
149
+ options: [
150
+ { value: 'run', label: 'Run', hint: 'use saved config' },
151
+ { value: 'temp', label: 'New session', hint: "temporary, changes won't be saved" },
152
+ { value: 'generate', label: 'Generate', hint: 'create a new logo config' },
153
+ ],
154
+ })
155
+ );
156
+
157
+ if (action === 'generate') {
158
+ return promptGenerate();
159
+ }
160
+
161
+ if (action === 'temp') {
162
+ p.outro('Launching logo (temporary session)...');
163
+ return { action: 'run', config: existingConfig, persistent: false };
164
+ }
97
165
 
166
+ // action === 'run'
167
+ p.outro('Launching logo...');
168
+ return { action: 'run', config: existingConfig, persistent: true };
169
+ }
170
+
171
+ // No existing config — only offer Generate
98
172
  const action = handleCancel(
99
173
  await p.select({
100
174
  message: 'What would you like to do?',
101
- initialValue: hasConfig ? 'run' : 'generate',
175
+ initialValue: 'generate',
102
176
  options: [
103
- { value: 'generate', label: 'Generate', hint: 'create .shell-logo.json' },
104
- { value: 'run', label: 'Run', hint: 'display current logo' },
177
+ { value: 'generate', label: 'Generate', hint: 'create a new logo config' },
105
178
  ],
106
179
  })
107
180
  );
@@ -109,18 +182,4 @@ export async function runInteractiveUI() {
109
182
  if (action === 'generate') {
110
183
  return promptGenerate();
111
184
  }
112
-
113
- // action === 'run'
114
- const config = tryLoadConfig();
115
- if (!config) {
116
- p.log.error(
117
- 'No valid .shell-logo.json found in the current directory.\n' +
118
- ' Run again and choose "Generate" to create one.'
119
- );
120
- p.outro('Done');
121
- process.exit(1);
122
- }
123
-
124
- p.outro('Launching logo...');
125
- return { action: 'run', config };
126
185
  }