mtosity 0.0.2 → 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 CHANGED
@@ -1,56 +1,143 @@
1
- # mtosity
1
+ # MTosity
2
2
 
3
- My personal terminal, whatevery I need in one place. You can use it too! You may know something about me while using it.
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
- ## Prerequisites
14
+ ## Quick Start
15
15
 
16
- - [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed and available in your PATH
16
+ ```bash
17
+ bunx mtosity
18
+ ```
17
19
 
18
- ## Install
20
+ or
19
21
 
20
22
  ```bash
21
- npm install -g mtosity
23
+ npx mtosity
22
24
  ```
23
25
 
24
- ## Usage
26
+ ## Commands
25
27
 
26
- Run the CLI:
28
+ ### About
27
29
 
28
- ```bash
29
- mtosity
30
- ```
30
+ | Command | Description |
31
+ |---|---|
32
+ | `me` | Animated terminal resume / about me |
31
33
 
32
- ### Commands
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
- | `neofetch` | Display system information |
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
- ### Examples
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 only
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
- # Download and trim (start at 0:30, duration 1:00)
53
- yt https://youtu.be/dQw4w9WgXcQ 00:00:30 00:01:00
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
+ }