shell-logo 0.1.0
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/LICENSE +21 -0
- package/README.md +70 -0
- package/examples/.shell-logo.json +6 -0
- package/package.json +43 -0
- package/src/config.js +91 -0
- package/src/generate.js +8 -0
- package/src/index.js +150 -0
- package/src/renderer.js +24 -0
- package/src/scaling.js +39 -0
- package/src/terminal.js +42 -0
- package/src/themes.js +12 -0
- package/src/ui.js +116 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafael Chiti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# shell-logo
|
|
2
|
+
|
|
3
|
+
Now that we are all coding with 1232 terminals open, is kind of hard to quickly find the one you want at a glance.
|
|
4
|
+
Adding a small configurable logo to quickly spot each project.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i -g shell-logo
|
|
12
|
+
# or
|
|
13
|
+
pnpm add -g shell-logo
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
shell-logo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Launches an interactive menu where you can:
|
|
23
|
+
|
|
24
|
+
1. **Generate** - create a new `.shell-logo.json` config by picking text, colors, and a font
|
|
25
|
+
2. **Run** - display the logo from an existing `.shell-logo.json` in the current directory
|
|
26
|
+
|
|
27
|
+
Once the logo is displayed, use arrow keys to cycle through themes and `q` to quit.
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
The generated `.shell-logo.json` looks like this:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"text": "My Project",
|
|
36
|
+
"font": "Standard",
|
|
37
|
+
"colors": ["#ff6b6b", "#feca57"],
|
|
38
|
+
"padding": 1
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Field | Type | Default | Description |
|
|
43
|
+
| --------- | ---------- | ----------------------------------- | --------------------------------- |
|
|
44
|
+
| `text` | `string` | _(required)_ | The text to render as ASCII art |
|
|
45
|
+
| `font` | `string` | `"Standard"` | Figlet font name |
|
|
46
|
+
| `colors` | `string[]` | `["#ff6b6b", "#feca57", "#48dbfb"]` | Hex colors for the gradient (>=2) |
|
|
47
|
+
| `padding` | `number` | `1` | Vertical padding above the logo |
|
|
48
|
+
|
|
49
|
+
See `examples/` for more config samples.
|
|
50
|
+
|
|
51
|
+
## Publishing to npm
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 1. Make sure you're logged in
|
|
55
|
+
npm login
|
|
56
|
+
|
|
57
|
+
# 2. Do a dry run to verify what gets packed
|
|
58
|
+
npm pack --dry-run
|
|
59
|
+
|
|
60
|
+
# 3. Publish
|
|
61
|
+
npm publish
|
|
62
|
+
|
|
63
|
+
# For subsequent releases, bump the version first
|
|
64
|
+
npm version patch # or minor / major
|
|
65
|
+
npm publish
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shell-logo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Render colorful ASCII art logos in the terminal from a config file",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Rafael Chiti",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rafaelchiti/shell-logo.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rafaelchiti/shell-logo",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rafaelchiti/shell-logo/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"shell-logo": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"terminal",
|
|
22
|
+
"ascii-art",
|
|
23
|
+
"logo",
|
|
24
|
+
"figlet",
|
|
25
|
+
"gradient"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/",
|
|
32
|
+
"examples/"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^5.4.1",
|
|
36
|
+
"figlet": "^1.8.0",
|
|
37
|
+
"@clack/prompts": "^0.11.0",
|
|
38
|
+
"gradient-string": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"start": "node src/index.js"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const DEFAULTS = {
|
|
6
|
+
colors: ['#ff6b6b', '#feca57', '#48dbfb'],
|
|
7
|
+
font: 'Standard',
|
|
8
|
+
};
|
|
9
|
+
|
|
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
|
+
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (config.colors !== undefined) {
|
|
32
|
+
if (!Array.isArray(config.colors) || config.colors.length < 2) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
text: config.text.trim(),
|
|
39
|
+
colors: config.colors ?? DEFAULTS.colors,
|
|
40
|
+
font: config.font ?? DEFAULTS.font,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadConfig() {
|
|
45
|
+
const configPath = join(process.cwd(), '.shell-logo.json');
|
|
46
|
+
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = readFileSync(configPath, 'utf-8');
|
|
50
|
+
} 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);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let config;
|
|
63
|
+
try {
|
|
64
|
+
config = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
console.error(chalk.red('Error: ') + '.shell-logo.json contains invalid JSON.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
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.'
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
text: config.text.trim(),
|
|
88
|
+
colors: config.colors ?? DEFAULTS.colors,
|
|
89
|
+
font: config.font ?? DEFAULTS.font,
|
|
90
|
+
};
|
|
91
|
+
}
|
package/src/generate.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
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
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runInteractiveUI } from './ui.js';
|
|
6
|
+
import { writeConfig } from './generate.js';
|
|
7
|
+
import { render } from './renderer.js';
|
|
8
|
+
import { getTerminalSize, clearScreen, hideCursor, showCursor, centerContent } from './terminal.js';
|
|
9
|
+
import { THEMES, FONTS } from './themes.js';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import * as p from '@clack/prompts';
|
|
12
|
+
|
|
13
|
+
const { action, config } = await runInteractiveUI();
|
|
14
|
+
|
|
15
|
+
if (action === 'generate') {
|
|
16
|
+
const s = p.spinner();
|
|
17
|
+
s.start('Writing .shell-logo.json...');
|
|
18
|
+
writeConfig(config);
|
|
19
|
+
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
|
+
}
|
|
59
|
+
|
|
60
|
+
startRenderLoop(config);
|
|
61
|
+
|
|
62
|
+
function startRenderLoop(config) {
|
|
63
|
+
let resizeTimer;
|
|
64
|
+
|
|
65
|
+
// Find the current theme index by matching colors
|
|
66
|
+
let themeIndex = THEMES.findIndex(
|
|
67
|
+
(t) => JSON.stringify(t.colors) === JSON.stringify(config.colors)
|
|
68
|
+
);
|
|
69
|
+
if (themeIndex === -1) themeIndex = 0;
|
|
70
|
+
|
|
71
|
+
let fontIndex = FONTS.indexOf(config.font);
|
|
72
|
+
if (fontIndex === -1) fontIndex = 0;
|
|
73
|
+
|
|
74
|
+
let showStatus = false;
|
|
75
|
+
let statusTimer;
|
|
76
|
+
|
|
77
|
+
function renderLoop() {
|
|
78
|
+
const { columns, rows } = getTerminalSize();
|
|
79
|
+
const art = render(config, columns, rows);
|
|
80
|
+
clearScreen();
|
|
81
|
+
process.stdout.write(centerContent(art, columns, rows));
|
|
82
|
+
if (showStatus) {
|
|
83
|
+
const status = ` ${THEMES[themeIndex].name} · ${FONTS[fontIndex]} · ↑↓ theme ←→ font q quit`;
|
|
84
|
+
process.stdout.write(`\x1B[${rows};1H` + chalk.dim(status));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function flashStatus() {
|
|
89
|
+
showStatus = true;
|
|
90
|
+
clearTimeout(statusTimer);
|
|
91
|
+
statusTimer = setTimeout(() => {
|
|
92
|
+
showStatus = false;
|
|
93
|
+
renderLoop();
|
|
94
|
+
}, 2000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function debouncedRender() {
|
|
98
|
+
clearTimeout(resizeTimer);
|
|
99
|
+
resizeTimer = setTimeout(renderLoop, 50);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanup() {
|
|
103
|
+
showCursor();
|
|
104
|
+
clearScreen();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Raw mode AFTER prompts are done
|
|
109
|
+
process.stdin.setRawMode(true);
|
|
110
|
+
process.stdin.resume();
|
|
111
|
+
hideCursor();
|
|
112
|
+
renderLoop();
|
|
113
|
+
|
|
114
|
+
// Show controls help on startup for 4 seconds
|
|
115
|
+
showStatus = true;
|
|
116
|
+
statusTimer = setTimeout(() => {
|
|
117
|
+
showStatus = false;
|
|
118
|
+
renderLoop();
|
|
119
|
+
}, 4000);
|
|
120
|
+
|
|
121
|
+
process.stdout.on('resize', debouncedRender);
|
|
122
|
+
|
|
123
|
+
process.stdin.on('data', (key) => {
|
|
124
|
+
if (key[0] === 3 || key[0] === 113) cleanup(); // Ctrl+C or q
|
|
125
|
+
|
|
126
|
+
// Arrow up/down are 3-byte escape sequences
|
|
127
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
128
|
+
if (key[2] === 65) { // Arrow Up — prev theme
|
|
129
|
+
themeIndex = (themeIndex - 1 + THEMES.length) % THEMES.length;
|
|
130
|
+
config.colors = THEMES[themeIndex].colors;
|
|
131
|
+
} else if (key[2] === 66) { // Arrow Down — next theme
|
|
132
|
+
themeIndex = (themeIndex + 1) % THEMES.length;
|
|
133
|
+
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];
|
|
140
|
+
} else {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
writeConfig(config);
|
|
144
|
+
flashStatus();
|
|
145
|
+
renderLoop();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
});
|
|
149
|
+
process.on('SIGTERM', cleanup);
|
|
150
|
+
}
|
package/src/renderer.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import { scaleArt } from './scaling.js';
|
|
4
|
+
|
|
5
|
+
function figletSync(text, font) {
|
|
6
|
+
try {
|
|
7
|
+
return figlet.textSync(text, { font });
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function render(config, columns, rows) {
|
|
14
|
+
const art = figletSync(config.text, config.font);
|
|
15
|
+
if (!art) {
|
|
16
|
+
const grad = gradient(config.colors);
|
|
17
|
+
return grad(config.text.slice(0, columns));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const trimmed = art.trimEnd();
|
|
21
|
+
const scaled = scaleArt(trimmed, columns, rows - 2);
|
|
22
|
+
const grad = gradient(config.colors);
|
|
23
|
+
return grad.multiline(scaled);
|
|
24
|
+
}
|
package/src/scaling.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function measureArt(art) {
|
|
2
|
+
const lines = art.split('\n');
|
|
3
|
+
const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
4
|
+
return { width, height: lines.length };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function scaleArt(art, targetWidth, targetHeight) {
|
|
8
|
+
const lines = art.split('\n');
|
|
9
|
+
const { width: artWidth, height: artHeight } = measureArt(art);
|
|
10
|
+
|
|
11
|
+
if (artWidth <= targetWidth && artHeight <= targetHeight) return art;
|
|
12
|
+
|
|
13
|
+
const scaleFactor = Math.min(
|
|
14
|
+
targetWidth / artWidth,
|
|
15
|
+
targetHeight / artHeight,
|
|
16
|
+
1.0
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const newWidth = Math.floor(artWidth * scaleFactor);
|
|
20
|
+
const newHeight = Math.floor(artHeight * scaleFactor);
|
|
21
|
+
|
|
22
|
+
const result = [];
|
|
23
|
+
for (let j = 0; j < newHeight; j++) {
|
|
24
|
+
const srcRow = Math.floor(j * artHeight / newHeight);
|
|
25
|
+
const line = lines[srcRow] || '';
|
|
26
|
+
if (line.length === 0) {
|
|
27
|
+
result.push('');
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const row = [];
|
|
31
|
+
for (let i = 0; i < newWidth; i++) {
|
|
32
|
+
const srcCol = Math.floor(i * artWidth / newWidth);
|
|
33
|
+
row.push(srcCol < line.length ? line[srcCol] : ' ');
|
|
34
|
+
}
|
|
35
|
+
result.push(row.join(''));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result.join('\n');
|
|
39
|
+
}
|
package/src/terminal.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Strip ANSI escape codes when calculating visual width
|
|
2
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
3
|
+
|
|
4
|
+
export function getTerminalSize() {
|
|
5
|
+
return {
|
|
6
|
+
columns: process.stdout.columns || 80,
|
|
7
|
+
rows: process.stdout.rows || 24,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clearScreen() {
|
|
12
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function hideCursor() {
|
|
16
|
+
process.stdout.write('\x1B[?25l');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function showCursor() {
|
|
20
|
+
process.stdout.write('\x1B[?25h');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function centerContent(text, columns, rows) {
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
|
|
26
|
+
// Calculate the max visual width (strip ANSI codes)
|
|
27
|
+
const maxWidth = lines.reduce((max, line) => {
|
|
28
|
+
const visual = line.replace(ANSI_RE, '').length;
|
|
29
|
+
return visual > max ? visual : max;
|
|
30
|
+
}, 0);
|
|
31
|
+
|
|
32
|
+
// Horizontal padding for each line
|
|
33
|
+
const leftPad = Math.max(0, Math.floor((columns - maxWidth) / 2));
|
|
34
|
+
const padStr = ' '.repeat(leftPad);
|
|
35
|
+
|
|
36
|
+
const paddedLines = lines.map((line) => padStr + line);
|
|
37
|
+
|
|
38
|
+
const contentHeight = paddedLines.length;
|
|
39
|
+
const topPad = Math.max(0, Math.floor((rows - contentHeight) / 2));
|
|
40
|
+
|
|
41
|
+
return '\n'.repeat(topPad) + paddedLines.join('\n');
|
|
42
|
+
}
|
package/src/themes.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const FONTS = ['Standard', 'Big', 'ANSI Shadow', 'Slant', 'Small'];
|
|
2
|
+
|
|
3
|
+
export const THEMES = [
|
|
4
|
+
{ name: 'Sunset', colors: ['#ff6b6b', '#ff9f43', '#feca57'] },
|
|
5
|
+
{ name: 'Ocean', colors: ['#0abde3', '#48dbfb', '#54a0ff'] },
|
|
6
|
+
{ name: 'Forest', colors: ['#1dd1a1', '#2ed573', '#26de81'] },
|
|
7
|
+
{ name: 'Neon', colors: ['#c56cf0', '#fd79a8', '#ff6b6b'] },
|
|
8
|
+
{ name: 'Twilight', colors: ['#5f27cd', '#c56cf0', '#48dbfb'] },
|
|
9
|
+
{ name: 'Fire', colors: ['#ff4757', '#ff6348', '#ffdd59'] },
|
|
10
|
+
{ name: 'Pastel', colors: ['#fd79a8', '#feca57', '#48dbfb'] },
|
|
11
|
+
{ name: 'Monochrome', colors: ['#ffffff', '#54a0ff', '#5f27cd'] },
|
|
12
|
+
];
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { tryLoadConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
const COLOR_PALETTE = [
|
|
6
|
+
{ value: '#ff6b6b', label: `${chalk.bgHex('#ff6b6b')(' ')} Coral` },
|
|
7
|
+
{ value: '#ff4757', label: `${chalk.bgHex('#ff4757')(' ')} Vibrant Red` },
|
|
8
|
+
{ value: '#ff6348', label: `${chalk.bgHex('#ff6348')(' ')} Tomato` },
|
|
9
|
+
{ value: '#feca57', label: `${chalk.bgHex('#feca57')(' ')} Golden` },
|
|
10
|
+
{ value: '#ff9f43', label: `${chalk.bgHex('#ff9f43')(' ')} Orange` },
|
|
11
|
+
{ value: '#ffdd59', label: `${chalk.bgHex('#ffdd59')(' ')} Lemon` },
|
|
12
|
+
{ value: '#1dd1a1', label: `${chalk.bgHex('#1dd1a1')(' ')} Mint` },
|
|
13
|
+
{ value: '#2ed573', label: `${chalk.bgHex('#2ed573')(' ')} Neon Green` },
|
|
14
|
+
{ value: '#26de81', label: `${chalk.bgHex('#26de81')(' ')} Emerald` },
|
|
15
|
+
{ value: '#48dbfb', label: `${chalk.bgHex('#48dbfb')(' ')} Sky` },
|
|
16
|
+
{ value: '#0abde3', label: `${chalk.bgHex('#0abde3')(' ')} Cerulean` },
|
|
17
|
+
{ value: '#54a0ff', label: `${chalk.bgHex('#54a0ff')(' ')} Cornflower` },
|
|
18
|
+
{ value: '#5f27cd', label: `${chalk.bgHex('#5f27cd')(' ')} Deep Purple` },
|
|
19
|
+
{ value: '#c56cf0', label: `${chalk.bgHex('#c56cf0')(' ')} Lavender` },
|
|
20
|
+
{ value: '#fd79a8', label: `${chalk.bgHex('#fd79a8')(' ')} Pink` },
|
|
21
|
+
{ value: '#ffffff', label: `${chalk.bgHex('#ffffff')(' ')} White` },
|
|
22
|
+
];
|
|
23
|
+
|
|
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
|
+
];
|
|
31
|
+
|
|
32
|
+
function handleCancel(value) {
|
|
33
|
+
if (p.isCancel(value)) {
|
|
34
|
+
p.cancel('Cancelled.');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function promptGenerate() {
|
|
41
|
+
const text = handleCancel(
|
|
42
|
+
await p.text({
|
|
43
|
+
message: 'What text should the logo display?',
|
|
44
|
+
placeholder: 'HELLO',
|
|
45
|
+
validate: (val) => {
|
|
46
|
+
if (!val || val.trim() === '') return 'Text is required.';
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Pick 3 random unique colors as defaults
|
|
52
|
+
const shuffled = [...COLOR_PALETTE].sort(() => Math.random() - 0.5);
|
|
53
|
+
const initialColors = shuffled.slice(0, 3).map(c => c.value);
|
|
54
|
+
|
|
55
|
+
let colors;
|
|
56
|
+
while (true) {
|
|
57
|
+
colors = handleCancel(
|
|
58
|
+
await p.multiselect({
|
|
59
|
+
message: 'Pick 2 or more colors for the gradient:',
|
|
60
|
+
options: COLOR_PALETTE,
|
|
61
|
+
initialValues: initialColors,
|
|
62
|
+
required: true,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (colors.length >= 2) break;
|
|
67
|
+
p.log.warning('Please select at least 2 colors.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const font = handleCancel(
|
|
71
|
+
await p.select({
|
|
72
|
+
message: 'Pick a font:',
|
|
73
|
+
options: FONT_OPTIONS,
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
action: 'generate',
|
|
79
|
+
config: { text: text.trim(), colors, font },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runInteractiveUI() {
|
|
84
|
+
p.intro(chalk.bold('terminal-logo'));
|
|
85
|
+
|
|
86
|
+
const hasConfig = !!tryLoadConfig();
|
|
87
|
+
|
|
88
|
+
const action = handleCancel(
|
|
89
|
+
await p.select({
|
|
90
|
+
message: 'What would you like to do?',
|
|
91
|
+
initialValue: hasConfig ? 'run' : 'generate',
|
|
92
|
+
options: [
|
|
93
|
+
{ value: 'generate', label: 'Generate', hint: 'create .shell-logo.json' },
|
|
94
|
+
{ value: 'run', label: 'Run', hint: 'display current logo' },
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (action === 'generate') {
|
|
100
|
+
return promptGenerate();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// action === 'run'
|
|
104
|
+
const config = tryLoadConfig();
|
|
105
|
+
if (!config) {
|
|
106
|
+
p.log.error(
|
|
107
|
+
'No valid .shell-logo.json found in the current directory.\n' +
|
|
108
|
+
' Run again and choose "Generate" to create one.'
|
|
109
|
+
);
|
|
110
|
+
p.outro('Done');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
p.outro('Launching logo...');
|
|
115
|
+
return { action: 'run', config };
|
|
116
|
+
}
|