mtosity 0.0.4 → 0.0.5
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/README.md +114 -24
- package/dist/cli.js +118 -0
- package/dist/commands/clock.js +303 -0
- package/dist/commands/game.js +30 -0
- package/dist/commands/harmonica.js +70 -0
- package/dist/commands/help.js +46 -0
- package/dist/commands/me.js +189 -0
- package/dist/commands/spicetify.js +153 -0
- package/dist/commands/system.js +195 -0
- package/dist/commands/whisky.js +158 -0
- package/dist/commands/youtube.js +73 -0
- package/dist/games/invaders/entities.js +47 -0
- package/dist/games/invaders/index.js +223 -0
- package/dist/games/invaders/renderer.js +105 -0
- package/dist/games/invaders/wave.js +44 -0
- package/dist/games/terminal.js +62 -0
- package/dist/games/tetris/bastard.js +56 -0
- package/dist/games/tetris/board.js +142 -0
- package/dist/games/tetris/index.js +146 -0
- package/dist/games/tetris/pieces.js +149 -0
- package/dist/games/tetris/renderer.js +188 -0
- package/dist/index.js +2 -169
- package/dist/utils/ffmpeg.js +15 -0
- package/dist/utils/network.js +29 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,56 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MTosity
|
|
2
2
|
|
|
3
|
-
My personal
|
|
3
|
+
My personal CLI, whatever I need in one place. You can use it too! You may know something about me while using it.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
__ __ _
|
|
7
|
-
| \/ | |
|
|
8
|
-
| |\/| |
|
|
9
|
-
| | | |
|
|
10
|
-
|_| |_|
|
|
11
|
-
|
|
6
|
+
__ __ _____ _ _
|
|
7
|
+
| \/ | |_ _| ___ ___ (_) | |_ _ _
|
|
8
|
+
| |\/| | | | / _ \ / __| | | | __| | | | |
|
|
9
|
+
| | | | | | | (_) | \__ \ | | | |_ | |_| |
|
|
10
|
+
|_| |_| |_| \___/ |___/ |_| \__| \__, |
|
|
11
|
+
|___/
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Quick Start
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
bunx mtosity
|
|
18
|
+
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
or
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
|
-
|
|
23
|
+
npx mtosity
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
##
|
|
26
|
+
## Commands
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
### About
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
| Command | Description |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `me` | Animated terminal resume / about me |
|
|
31
33
|
|
|
32
|
-
###
|
|
34
|
+
### System
|
|
35
|
+
|
|
36
|
+
| Command | Description |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `system` | Display system info (OS, CPU, GPU, memory, IP addresses) |
|
|
39
|
+
|
|
40
|
+
### Apps
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `spotify status` | Show current Spicetify config |
|
|
45
|
+
| `spotify theme <name>` | Switch theme (mocha, macchiato, frappe, latte, dark, dracula, nord, gruvbox, rosepine, default) |
|
|
46
|
+
| `spotify apply` | Apply Spicetify config |
|
|
47
|
+
| `spotify restart` | Restart Spotify |
|
|
48
|
+
| `spotify fix` | Backup & apply (fixes after Spotify updates) |
|
|
49
|
+
| `spotify restore` | Restore original Spotify |
|
|
50
|
+
| `whisky status` | Check Whisky installation |
|
|
51
|
+
| `whisky run <file.exe>` | Run a Windows .exe via Whisky |
|
|
52
|
+
| `whisky open` | Open Whisky.app GUI |
|
|
53
|
+
| `whisky install` | Install Whisky via Homebrew |
|
|
54
|
+
|
|
55
|
+
### Media
|
|
33
56
|
|
|
34
57
|
| Command | Description |
|
|
35
58
|
|---|---|
|
|
36
59
|
| `yt <url> [start] [end]` | Download YouTube video (MP4) |
|
|
37
60
|
| `yt-mp3 <url> [start] [end]` | Download YouTube audio (MP3) |
|
|
38
|
-
| `
|
|
61
|
+
| `harmonica <file> [preset]` | Enhance harmonica recording (presets: echo, echo-light, echo-heavy, bass) |
|
|
62
|
+
|
|
63
|
+
### Games
|
|
64
|
+
|
|
65
|
+
| Command | Description |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `game` | List available games |
|
|
68
|
+
| `game tetris` | Bastard Tetris — always gives you the worst piece |
|
|
69
|
+
| `game invaders` | nInvaders — Space Invaders clone in the terminal |
|
|
70
|
+
|
|
71
|
+
### Utility
|
|
72
|
+
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `clock` | World clock showing times across cities |
|
|
76
|
+
| `clock -p <city>` | Add specific cities to the clock (e.g. `clock -p berlin -p "san francisco"`) |
|
|
77
|
+
|
|
78
|
+
### General
|
|
79
|
+
|
|
80
|
+
| Command | Description |
|
|
81
|
+
|---|---|
|
|
39
82
|
| `help` | Show available commands |
|
|
40
83
|
| `clear` | Clear the terminal |
|
|
41
84
|
| `exit` | Exit the CLI |
|
|
42
85
|
|
|
43
|
-
|
|
86
|
+
> **Tip**: Press ↑/↓ arrows to navigate command history.
|
|
87
|
+
|
|
88
|
+
## Examples
|
|
44
89
|
|
|
45
90
|
```bash
|
|
91
|
+
# System info
|
|
92
|
+
system
|
|
93
|
+
|
|
94
|
+
# Switch Spotify theme
|
|
95
|
+
spotify theme mocha
|
|
96
|
+
|
|
46
97
|
# Download a video
|
|
47
98
|
yt https://youtu.be/dQw4w9WgXcQ
|
|
48
99
|
|
|
49
|
-
# Download audio
|
|
50
|
-
yt-mp3 https://youtu.be/dQw4w9WgXcQ
|
|
100
|
+
# Download and trim audio (start at 0:30, duration 1:00)
|
|
101
|
+
yt-mp3 https://youtu.be/dQw4w9WgXcQ 00:00:30 00:01:00
|
|
102
|
+
|
|
103
|
+
# Run a Windows exe
|
|
104
|
+
whisky run ~/Downloads/setup.exe
|
|
105
|
+
|
|
106
|
+
# Enhance a harmonica recording with heavy echo
|
|
107
|
+
harmonica recording.mp4 echo-heavy
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Project Structure
|
|
51
111
|
|
|
52
|
-
|
|
53
|
-
|
|
112
|
+
```
|
|
113
|
+
src/
|
|
114
|
+
├── index.ts Entry point
|
|
115
|
+
├── cli.ts REPL loop & command dispatch
|
|
116
|
+
├── commands/
|
|
117
|
+
│ ├── help.ts Help display
|
|
118
|
+
│ ├── system.ts System info (neofetch-style)
|
|
119
|
+
│ ├── spicetify.ts Spotify/Spicetify management
|
|
120
|
+
│ ├── whisky.ts Windows app runner via Whisky
|
|
121
|
+
│ ├── youtube.ts YouTube downloader
|
|
122
|
+
│ ├── harmonica.ts Audio enhancement
|
|
123
|
+
│ ├── clock.ts World clock display
|
|
124
|
+
│ └── game.ts Game launcher (tetris, invaders)
|
|
125
|
+
├── games/
|
|
126
|
+
│ ├── terminal.ts Shared game infrastructure (raw mode, keys, ANSI)
|
|
127
|
+
│ ├── tetris/
|
|
128
|
+
│ │ ├── index.ts Tetris game loop & state machine
|
|
129
|
+
│ │ ├── pieces.ts Tetromino definitions & rotations
|
|
130
|
+
│ │ ├── board.ts Board state, collision, line clearing
|
|
131
|
+
│ │ ├── bastard.ts Worst-piece selection algorithm
|
|
132
|
+
│ │ └── renderer.ts Board + HUD rendering
|
|
133
|
+
│ └── invaders/
|
|
134
|
+
│ ├── index.ts Invaders game loop & state machine
|
|
135
|
+
│ ├── entities.ts Types, sprites, colors
|
|
136
|
+
│ ├── wave.ts Wave creation, speed scaling
|
|
137
|
+
│ └── renderer.ts Scene rendering
|
|
138
|
+
└── utils/
|
|
139
|
+
├── ffmpeg.ts FFmpeg & yt-dlp setup
|
|
140
|
+
└── network.ts IP address utilities
|
|
54
141
|
```
|
|
55
142
|
|
|
56
143
|
## Development
|
|
@@ -62,6 +149,9 @@ bun install
|
|
|
62
149
|
# Run in dev mode
|
|
63
150
|
bun run dev
|
|
64
151
|
|
|
152
|
+
# Run tests
|
|
153
|
+
bun test
|
|
154
|
+
|
|
65
155
|
# Build
|
|
66
156
|
bun run build
|
|
67
157
|
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.main = main;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const figlet_1 = __importDefault(require("figlet"));
|
|
9
|
+
const readline_1 = __importDefault(require("readline"));
|
|
10
|
+
const system_1 = require("./commands/system");
|
|
11
|
+
const youtube_1 = require("./commands/youtube");
|
|
12
|
+
const help_1 = require("./commands/help");
|
|
13
|
+
const harmonica_1 = require("./commands/harmonica");
|
|
14
|
+
const spicetify_1 = require("./commands/spicetify");
|
|
15
|
+
const whisky_1 = require("./commands/whisky");
|
|
16
|
+
const me_1 = require("./commands/me");
|
|
17
|
+
const clock_1 = require("./commands/clock");
|
|
18
|
+
const game_1 = require("./commands/game");
|
|
19
|
+
function prompt(rl) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(chalk_1.default.green("mtosity > "), (answer) => {
|
|
22
|
+
resolve(answer);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
console.clear();
|
|
28
|
+
console.log(chalk_1.default.green(figlet_1.default.textSync("MTosity", { horizontalLayout: "full" })));
|
|
29
|
+
console.log(chalk_1.default.dim(`Welcome! Type '${chalk_1.default.white("me")}' to learn about me, or '${chalk_1.default.white("help")}' for all commands.\n`));
|
|
30
|
+
const rl = readline_1.default.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
historySize: 100,
|
|
34
|
+
terminal: true,
|
|
35
|
+
});
|
|
36
|
+
// Handle Ctrl+C gracefully
|
|
37
|
+
rl.on("close", () => {
|
|
38
|
+
const confirmRl = readline_1.default.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stdout,
|
|
41
|
+
terminal: true,
|
|
42
|
+
});
|
|
43
|
+
console.log("");
|
|
44
|
+
confirmRl.question(chalk_1.default.yellow("Are you sure you want to quit? (y/N) "), (answer) => {
|
|
45
|
+
confirmRl.close();
|
|
46
|
+
if (answer.trim().toLowerCase() === "y") {
|
|
47
|
+
console.log(chalk_1.default.green("Goodbye!"));
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
main().catch(console.error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// Prevent default SIGINT from killing the process
|
|
56
|
+
process.on("SIGINT", () => { });
|
|
57
|
+
while (true) {
|
|
58
|
+
const input = await prompt(rl);
|
|
59
|
+
const parts = input.trim().split(" ");
|
|
60
|
+
const cmd = parts[0];
|
|
61
|
+
const args = parts.slice(1);
|
|
62
|
+
switch (cmd) {
|
|
63
|
+
case "system":
|
|
64
|
+
await (0, system_1.runNeofetch)();
|
|
65
|
+
break;
|
|
66
|
+
case "yt":
|
|
67
|
+
if (!args[0]) {
|
|
68
|
+
console.log(chalk_1.default.dim("Usage: yt <url> [start] [end]"));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await (0, youtube_1.downloadYouTube)(args[0], "video", args[1], args[2]);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case "yt-mp3":
|
|
75
|
+
if (!args[0]) {
|
|
76
|
+
console.log(chalk_1.default.dim("Usage: yt-mp3 <url> [start] [end]"));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
await (0, youtube_1.downloadYouTube)(args[0], "audio", args[1], args[2]);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case "spotify":
|
|
83
|
+
await (0, spicetify_1.runSpicetify)(args[0], args[1]);
|
|
84
|
+
break;
|
|
85
|
+
case "whisky":
|
|
86
|
+
await (0, whisky_1.runWhisky)(args[0], args[1]);
|
|
87
|
+
break;
|
|
88
|
+
case "harmonica":
|
|
89
|
+
await (0, harmonica_1.enhanceHarmonica)(args[0], args[1]);
|
|
90
|
+
break;
|
|
91
|
+
case "me":
|
|
92
|
+
await (0, me_1.showResume)();
|
|
93
|
+
break;
|
|
94
|
+
case "clock":
|
|
95
|
+
const extraPlaces = (0, clock_1.parsePlaces)(args);
|
|
96
|
+
(0, clock_1.showClock)(extraPlaces);
|
|
97
|
+
break;
|
|
98
|
+
case "game":
|
|
99
|
+
await (0, game_1.runGame)(rl, args[0]);
|
|
100
|
+
break;
|
|
101
|
+
case "clear":
|
|
102
|
+
console.clear();
|
|
103
|
+
break;
|
|
104
|
+
case "exit":
|
|
105
|
+
console.log(chalk_1.default.green("Goodbye!"));
|
|
106
|
+
rl.close();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
case "":
|
|
109
|
+
break;
|
|
110
|
+
case "help":
|
|
111
|
+
(0, help_1.showHelp)();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
console.log(chalk_1.default.red(`Unknown command: ${cmd}`));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parsePlaces = parsePlaces;
|
|
7
|
+
exports.showClock = showClock;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const CITIES = [
|
|
10
|
+
{ city: "Sydney", timezone: "Australia/Sydney", flag: "🇦🇺" },
|
|
11
|
+
{ city: "Tokyo", timezone: "Asia/Tokyo", flag: "🇯🇵" },
|
|
12
|
+
{ city: "Singapore", timezone: "Asia/Singapore", flag: "🇸🇬" },
|
|
13
|
+
{ city: "Ho Chi Minh", timezone: "Asia/Ho_Chi_Minh", flag: "🇻🇳" },
|
|
14
|
+
{ city: "Dubai", timezone: "Asia/Dubai", flag: "🇦🇪" },
|
|
15
|
+
{ city: "UTC", timezone: "UTC", flag: "🌐" },
|
|
16
|
+
{ city: "London", timezone: "Europe/London", flag: "🇬🇧" },
|
|
17
|
+
{ city: "Paris", timezone: "Europe/Paris", flag: "🇫🇷" },
|
|
18
|
+
{ city: "New York", timezone: "America/New_York", flag: "🇺🇸" },
|
|
19
|
+
{ city: "San Francisco", timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
20
|
+
{ city: "Honolulu", timezone: "Pacific/Honolulu", flag: "🇺🇸" },
|
|
21
|
+
];
|
|
22
|
+
// Lookup table for resolving city names to IANA timezones
|
|
23
|
+
const CITY_LOOKUP = {
|
|
24
|
+
// Americas
|
|
25
|
+
"new york": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
26
|
+
"los angeles": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
27
|
+
"san francisco": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
28
|
+
"chicago": { timezone: "America/Chicago", flag: "🇺🇸" },
|
|
29
|
+
"denver": { timezone: "America/Denver", flag: "🇺🇸" },
|
|
30
|
+
"houston": { timezone: "America/Chicago", flag: "🇺🇸" },
|
|
31
|
+
"dallas": { timezone: "America/Chicago", flag: "🇺🇸" },
|
|
32
|
+
"seattle": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
33
|
+
"miami": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
34
|
+
"boston": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
35
|
+
"atlanta": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
36
|
+
"raleigh": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
37
|
+
"washington": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
38
|
+
"washington dc": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
39
|
+
"dc": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
40
|
+
"las vegas": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
41
|
+
"vegas": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
42
|
+
"phoenix": { timezone: "America/Phoenix", flag: "🇺🇸" },
|
|
43
|
+
"philadelphia": { timezone: "America/New_York", flag: "🇺🇸" },
|
|
44
|
+
"detroit": { timezone: "America/Detroit", flag: "🇺🇸" },
|
|
45
|
+
"san diego": { timezone: "America/Los_Angeles", flag: "🇺🇸" },
|
|
46
|
+
"austin": { timezone: "America/Chicago", flag: "🇺🇸" },
|
|
47
|
+
"honolulu": { timezone: "Pacific/Honolulu", flag: "🇺🇸" },
|
|
48
|
+
"anchorage": { timezone: "America/Anchorage", flag: "🇺🇸" },
|
|
49
|
+
"toronto": { timezone: "America/Toronto", flag: "🇨🇦" },
|
|
50
|
+
"montreal": { timezone: "America/Toronto", flag: "🇨🇦" },
|
|
51
|
+
"vancouver": { timezone: "America/Vancouver", flag: "🇨🇦" },
|
|
52
|
+
"mexico city": { timezone: "America/Mexico_City", flag: "🇲🇽" },
|
|
53
|
+
"sao paulo": { timezone: "America/Sao_Paulo", flag: "🇧🇷" },
|
|
54
|
+
"rio": { timezone: "America/Sao_Paulo", flag: "🇧🇷" },
|
|
55
|
+
"buenos aires": { timezone: "America/Argentina/Buenos_Aires", flag: "🇦🇷" },
|
|
56
|
+
"santiago": { timezone: "America/Santiago", flag: "🇨🇱" },
|
|
57
|
+
"lima": { timezone: "America/Lima", flag: "🇵🇪" },
|
|
58
|
+
"bogota": { timezone: "America/Bogota", flag: "🇨🇴" },
|
|
59
|
+
// Europe
|
|
60
|
+
"london": { timezone: "Europe/London", flag: "🇬🇧" },
|
|
61
|
+
"dublin": { timezone: "Europe/Dublin", flag: "🇮🇪" },
|
|
62
|
+
"paris": { timezone: "Europe/Paris", flag: "🇫🇷" },
|
|
63
|
+
"berlin": { timezone: "Europe/Berlin", flag: "🇩🇪" },
|
|
64
|
+
"frankfurt": { timezone: "Europe/Berlin", flag: "🇩🇪" },
|
|
65
|
+
"munich": { timezone: "Europe/Berlin", flag: "🇩🇪" },
|
|
66
|
+
"amsterdam": { timezone: "Europe/Amsterdam", flag: "🇳🇱" },
|
|
67
|
+
"brussels": { timezone: "Europe/Brussels", flag: "🇧🇪" },
|
|
68
|
+
"madrid": { timezone: "Europe/Madrid", flag: "🇪🇸" },
|
|
69
|
+
"barcelona": { timezone: "Europe/Madrid", flag: "🇪🇸" },
|
|
70
|
+
"rome": { timezone: "Europe/Rome", flag: "🇮🇹" },
|
|
71
|
+
"milan": { timezone: "Europe/Rome", flag: "🇮🇹" },
|
|
72
|
+
"vienne": { timezone: "Europe/Vienna", flag: "🇦🇹" },
|
|
73
|
+
"vienna": { timezone: "Europe/Vienna", flag: "🇦🇹" },
|
|
74
|
+
"zurich": { timezone: "Europe/Zurich", flag: "🇨H" },
|
|
75
|
+
"warsaw": { timezone: "Europe/Warsaw", flag: "🇵🇱" },
|
|
76
|
+
"stockholm": { timezone: "Europe/Stockholm", flag: "🇸🇪" },
|
|
77
|
+
"oslo": { timezone: "Europe/Oslo", flag: "🇳🇴" },
|
|
78
|
+
"copenhagen": { timezone: "Europe/Copenhagen", flag: "🇩🇰" },
|
|
79
|
+
"helsinki": { timezone: "Europe/Helsinki", flag: "🇫🇮" },
|
|
80
|
+
"athens": { timezone: "Europe/Athens", flag: "🇬🇷" },
|
|
81
|
+
"istanbul": { timezone: "Europe/Istanbul", flag: "🇹🇷" },
|
|
82
|
+
"moscow": { timezone: "Europe/Moscow", flag: "🇷🇺" },
|
|
83
|
+
"kyiv": { timezone: "Europe/Kyiv", flag: "🇺🇦" },
|
|
84
|
+
"kiev": { timezone: "Europe/Kyiv", flag: "🇺🇦" },
|
|
85
|
+
// Asia & Oceania
|
|
86
|
+
"tokyo": { timezone: "Asia/Tokyo", flag: "🇯🇵" },
|
|
87
|
+
"osaka": { timezone: "Asia/Tokyo", flag: "🇯🇵" },
|
|
88
|
+
"kyoto": { timezone: "Asia/Tokyo", flag: "🇯🇵" },
|
|
89
|
+
"singapore": { timezone: "Asia/Singapore", flag: "🇸🇬" },
|
|
90
|
+
"ho chi minh": { timezone: "Asia/Ho_Chi_Minh", flag: "🇻🇳" },
|
|
91
|
+
"saigon": { timezone: "Asia/Ho_Chi_Minh", flag: "🇻🇳" },
|
|
92
|
+
"hanoi": { timezone: "Asia/Ho_Chi_Minh", flag: "🇻🇳" },
|
|
93
|
+
"ha noi": { timezone: "Asia/Ho_Chi_Minh", flag: "🇻🇳" },
|
|
94
|
+
"bangkok": { timezone: "Asia/Bangkok", flag: "🇹🇭" },
|
|
95
|
+
"jakarta": { timezone: "Asia/Jakarta", flag: "🇮🇩" },
|
|
96
|
+
"bali": { timezone: "Asia/Makassar", flag: "🇮🇩" },
|
|
97
|
+
"manila": { timezone: "Asia/Manila", flag: "🇵🇭" },
|
|
98
|
+
"kuala lumpur": { timezone: "Asia/Kuala_Lumpur", flag: "🇲🇾" },
|
|
99
|
+
"dubai": { timezone: "Asia/Dubai", flag: "🇦🇪" },
|
|
100
|
+
"abu dhabi": { timezone: "Asia/Dubai", flag: "🇦🇪" },
|
|
101
|
+
"doha": { timezone: "Asia/Qatar", flag: "🇶🇦" },
|
|
102
|
+
"riyadh": { timezone: "Asia/Riyadh", flag: "🇸🇦" },
|
|
103
|
+
"tel aviv": { timezone: "Asia/Jerusalem", flag: "🇮🇱" },
|
|
104
|
+
"jerusalem": { timezone: "Asia/Jerusalem", flag: "🇮🇱" },
|
|
105
|
+
"mumbai": { timezone: "Asia/Kolkata", flag: "🇮🇳" },
|
|
106
|
+
"delhi": { timezone: "Asia/Kolkata", flag: "🇮🇳" },
|
|
107
|
+
"bangalore": { timezone: "Asia/Kolkata", flag: "🇮🇳" },
|
|
108
|
+
"beijing": { timezone: "Asia/Shanghai", flag: "🇨🇳" },
|
|
109
|
+
"shanghai": { timezone: "Asia/Shanghai", flag: "🇨🇳" },
|
|
110
|
+
"shenzhen": { timezone: "Asia/Shanghai", flag: "🇨🇳" },
|
|
111
|
+
"hong kong": { timezone: "Asia/Hong_Kong", flag: "🇭🇰" },
|
|
112
|
+
"seoul": { timezone: "Asia/Seoul", flag: "🇰🇷" },
|
|
113
|
+
"taipei": { timezone: "Asia/Taipei", flag: "🇹🇼" },
|
|
114
|
+
"sydney": { timezone: "Australia/Sydney", flag: "🇦🇺" },
|
|
115
|
+
"melbourne": { timezone: "Australia/Melbourne", flag: "🇦🇺" },
|
|
116
|
+
"brisbane": { timezone: "Australia/Brisbane", flag: "🇦🇺" },
|
|
117
|
+
"perth": { timezone: "Australia/Perth", flag: "🇦🇺" },
|
|
118
|
+
"adelaide": { timezone: "Australia/Adelaide", flag: "🇦🇺" },
|
|
119
|
+
"auckland": { timezone: "Pacific/Auckland", flag: "🇳🇿" },
|
|
120
|
+
"wellington": { timezone: "Pacific/Auckland", flag: "🇳🇿" },
|
|
121
|
+
// Africa & Middle East
|
|
122
|
+
"cairo": { timezone: "Africa/Cairo", flag: "🇪🇬" },
|
|
123
|
+
"johannesburg": { timezone: "Africa/Johannesburg", flag: "🇿🇦" },
|
|
124
|
+
"nairobi": { timezone: "Africa/Nairobi", flag: "🇰🇪" },
|
|
125
|
+
// Special
|
|
126
|
+
"utc": { timezone: "UTC", flag: "🌐" },
|
|
127
|
+
};
|
|
128
|
+
function parsePlaces(args) {
|
|
129
|
+
const places = [];
|
|
130
|
+
let i = 0;
|
|
131
|
+
while (i < args.length) {
|
|
132
|
+
if (args[i] === "-p" && i + 1 < args.length) {
|
|
133
|
+
i++;
|
|
134
|
+
// Collect words until the next -p flag or end
|
|
135
|
+
const words = [];
|
|
136
|
+
while (i < args.length && args[i] !== "-p") {
|
|
137
|
+
words.push(args[i]);
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
if (words.length > 0) {
|
|
141
|
+
places.push(words.join(" "));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return places;
|
|
149
|
+
}
|
|
150
|
+
function resolvePlace(name) {
|
|
151
|
+
const key = name.toLowerCase().trim();
|
|
152
|
+
const entry = CITY_LOOKUP[key];
|
|
153
|
+
if (!entry)
|
|
154
|
+
return null;
|
|
155
|
+
// Capitalize city name nicely
|
|
156
|
+
const displayName = name.trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
157
|
+
return { city: displayName, timezone: entry.timezone, flag: entry.flag };
|
|
158
|
+
}
|
|
159
|
+
function getTimeInZone(tz, now) {
|
|
160
|
+
const timeFmt = new Intl.DateTimeFormat("en-US", {
|
|
161
|
+
timeZone: tz,
|
|
162
|
+
hour: "2-digit",
|
|
163
|
+
minute: "2-digit",
|
|
164
|
+
hour12: true,
|
|
165
|
+
});
|
|
166
|
+
const dateFmt = new Intl.DateTimeFormat("en-US", {
|
|
167
|
+
timeZone: tz,
|
|
168
|
+
weekday: "short",
|
|
169
|
+
month: "short",
|
|
170
|
+
day: "numeric",
|
|
171
|
+
});
|
|
172
|
+
const hourFmt = new Intl.DateTimeFormat("en-US", {
|
|
173
|
+
timeZone: tz,
|
|
174
|
+
hour: "numeric",
|
|
175
|
+
hour12: false,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
time: timeFmt.format(now),
|
|
179
|
+
date: dateFmt.format(now),
|
|
180
|
+
hour: parseInt(hourFmt.format(now), 10),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function getUtcOffset(tz, now) {
|
|
184
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
185
|
+
timeZone: tz,
|
|
186
|
+
timeZoneName: "shortOffset",
|
|
187
|
+
});
|
|
188
|
+
const parts = formatter.formatToParts(now);
|
|
189
|
+
const tzPart = parts.find((p) => p.type === "timeZoneName");
|
|
190
|
+
if (!tzPart)
|
|
191
|
+
return 0;
|
|
192
|
+
// Parse "GMT+7", "GMT-5", "GMT+5:30", "GMT" etc.
|
|
193
|
+
const match = tzPart.value.match(/GMT([+-]?)(\d+)?(?::(\d+))?/);
|
|
194
|
+
if (!match)
|
|
195
|
+
return 0;
|
|
196
|
+
const sign = match[1] === "-" ? -1 : 1;
|
|
197
|
+
const hours = parseInt(match[2] || "0", 10);
|
|
198
|
+
const minutes = parseInt(match[3] || "0", 10);
|
|
199
|
+
return sign * (hours * 60 + minutes);
|
|
200
|
+
}
|
|
201
|
+
function getDayNightIcon(hour) {
|
|
202
|
+
if (hour >= 6 && hour < 18)
|
|
203
|
+
return "☀️";
|
|
204
|
+
if (hour >= 18 && hour < 21)
|
|
205
|
+
return "🌅";
|
|
206
|
+
return "🌙";
|
|
207
|
+
}
|
|
208
|
+
function formatOffset(diffMinutes) {
|
|
209
|
+
const sign = diffMinutes >= 0 ? "+" : "";
|
|
210
|
+
const hours = Math.floor(Math.abs(diffMinutes) / 60);
|
|
211
|
+
const mins = Math.abs(diffMinutes) % 60;
|
|
212
|
+
if (mins === 0)
|
|
213
|
+
return `${sign}${diffMinutes >= 0 ? hours : -hours}h`;
|
|
214
|
+
return `${sign}${diffMinutes >= 0 ? "" : "-"}${hours}h${mins}m`;
|
|
215
|
+
}
|
|
216
|
+
function showClock(extraPlaces = []) {
|
|
217
|
+
const now = new Date();
|
|
218
|
+
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
219
|
+
const localOffset = getUtcOffset(localTz, now);
|
|
220
|
+
const localInfo = getTimeInZone(localTz, now);
|
|
221
|
+
// Resolve extra places
|
|
222
|
+
const extraCities = [];
|
|
223
|
+
const unknownPlaces = [];
|
|
224
|
+
for (const place of extraPlaces) {
|
|
225
|
+
const resolved = resolvePlace(place);
|
|
226
|
+
if (resolved) {
|
|
227
|
+
extraCities.push({ ...resolved, isRequested: true });
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
unknownPlaces.push(place);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Merge: default cities + extras (dedup by timezone+city)
|
|
234
|
+
// Use map to create mutable copies and track requested status
|
|
235
|
+
const allCities = CITIES.map((c) => ({ ...c, isRequested: false }));
|
|
236
|
+
for (const ec of extraCities) {
|
|
237
|
+
const existingIndex = allCities.findIndex((c) => c.timezone === ec.timezone && c.city.toLowerCase() === ec.city.toLowerCase());
|
|
238
|
+
if (existingIndex !== -1) {
|
|
239
|
+
allCities[existingIndex].isRequested = true;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
allCities.push(ec);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Compute data for each city
|
|
246
|
+
const entries = allCities.map((c) => {
|
|
247
|
+
const info = getTimeInZone(c.timezone, now);
|
|
248
|
+
const offset = getUtcOffset(c.timezone, now);
|
|
249
|
+
const diff = offset - localOffset;
|
|
250
|
+
return { ...c, ...info, offset, diff };
|
|
251
|
+
});
|
|
252
|
+
// Sort by UTC offset (west → east)
|
|
253
|
+
entries.sort((a, b) => a.offset - b.offset);
|
|
254
|
+
// Group
|
|
255
|
+
const ahead = entries.filter((e) => e.diff > 0);
|
|
256
|
+
const same = entries.filter((e) => e.diff === 0);
|
|
257
|
+
const behind = entries.filter((e) => e.diff < 0);
|
|
258
|
+
const W = 56;
|
|
259
|
+
const dim = chalk_1.default.dim;
|
|
260
|
+
const divider = dim(" " + "─".repeat(W));
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log(chalk_1.default.cyan.bold(" 🕐 World Clock"));
|
|
263
|
+
console.log(divider);
|
|
264
|
+
// Local time header
|
|
265
|
+
const localCity = localTz.split("/").pop()?.replace(/_/g, " ") || localTz;
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(` 📍 ${chalk_1.default.white.bold("You")} ${chalk_1.default.dim("·")} ${chalk_1.default.white.bold(localCity)} ${chalk_1.default.dim("·")} ${chalk_1.default.green.bold(localInfo.time)} ${chalk_1.default.dim(localInfo.date)} ${getDayNightIcon(localInfo.hour)}`);
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(divider);
|
|
270
|
+
const printGroup = (label, labelColor, items) => {
|
|
271
|
+
if (items.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
console.log("");
|
|
274
|
+
console.log(` ${labelColor(label)}`);
|
|
275
|
+
console.log("");
|
|
276
|
+
for (const e of items) {
|
|
277
|
+
const offsetStr = formatOffset(e.diff);
|
|
278
|
+
const offsetColored = e.diff > 0
|
|
279
|
+
? chalk_1.default.green(offsetStr.padStart(6))
|
|
280
|
+
: e.diff < 0
|
|
281
|
+
? chalk_1.default.red(offsetStr.padStart(6))
|
|
282
|
+
: chalk_1.default.yellow(offsetStr.padStart(6));
|
|
283
|
+
const icon = getDayNightIcon(e.hour);
|
|
284
|
+
// Highlight requested cities
|
|
285
|
+
const rawCity = (e.flag + " " + e.city).padEnd(22);
|
|
286
|
+
const cityDisplay = e.isRequested ? chalk_1.default.cyan.bold(rawCity) : rawCity;
|
|
287
|
+
console.log(` ${cityDisplay} ${chalk_1.default.white.bold(e.time.padEnd(9))} ${dim(e.date.padEnd(14))} ${icon} ${offsetColored}`);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
printGroup("▲ Ahead of you", chalk_1.default.green.bold, ahead);
|
|
291
|
+
printGroup("● Same time", chalk_1.default.yellow.bold, same);
|
|
292
|
+
printGroup("▼ Behind you", chalk_1.default.red.bold, behind);
|
|
293
|
+
console.log("");
|
|
294
|
+
console.log(divider);
|
|
295
|
+
console.log(dim(` Timezone: ${localTz}`));
|
|
296
|
+
if (unknownPlaces.length > 0) {
|
|
297
|
+
console.log("");
|
|
298
|
+
for (const p of unknownPlaces) {
|
|
299
|
+
console.log(chalk_1.default.yellow(` ⚠ Unknown place: "${p}". Try a major city name.`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
console.log("");
|
|
303
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runGame = runGame;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const tetris_1 = require("../games/tetris");
|
|
9
|
+
const invaders_1 = require("../games/invaders");
|
|
10
|
+
const GAMES = {
|
|
11
|
+
tetris: { name: "Bastard Tetris", desc: "Tetris that always gives you the worst piece", play: tetris_1.playTetris },
|
|
12
|
+
invaders: { name: "nInvaders", desc: "Space Invaders clone in the terminal", play: invaders_1.playInvaders },
|
|
13
|
+
};
|
|
14
|
+
function showGameList() {
|
|
15
|
+
const cmd = chalk_1.default.cyan.bold;
|
|
16
|
+
const dim = chalk_1.default.dim;
|
|
17
|
+
console.log("");
|
|
18
|
+
console.log(chalk_1.default.white.bold(" Available Games:"));
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log(` ${cmd("game tetris")} Bastard Tetris ${dim("— always the worst piece")}`);
|
|
21
|
+
console.log(` ${cmd("game invaders")} nInvaders ${dim("— Space Invaders clone")}`);
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
async function runGame(rl, gameName) {
|
|
25
|
+
if (!gameName || !GAMES[gameName]) {
|
|
26
|
+
showGameList();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await GAMES[gameName].play(rl);
|
|
30
|
+
}
|