go-touch-grass 1.0.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 +283 -0
- package/bin/touch-grass.js +154 -0
- package/logo.svg +93 -0
- package/package.json +67 -0
- package/src/art.js +42 -0
- package/src/messages.js +40 -0
- package/src/share.js +59 -0
- package/src/streak.js +123 -0
- package/src/timer.js +72 -0
- package/src/ui.js +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 touch-grass contributors
|
|
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,283 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="./touch_grass_logo_dark.png" />
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="./toucn_grass_logo.png" />
|
|
6
|
+
<img src="./touch_grass_logo.png" alt="go-touch-grass CLI logo — a mouse cursor touching grass blades, representing developer break reminders" width="280" />
|
|
7
|
+
</picture>
|
|
8
|
+
|
|
9
|
+
# go-touch-grass
|
|
10
|
+
|
|
11
|
+
### A sarcastic CLI that reminds developers to go outside, touch grass, and stop staring at screens
|
|
12
|
+
|
|
13
|
+
Track your outdoor streak, earn milestones, and share your grass-touching stats on Twitter/X, LinkedIn, and Instagram — all from your terminal.
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/go-touch-grass)
|
|
16
|
+
[](https://www.npmjs.com/package/go-touch-grass)
|
|
17
|
+
[](./LICENSE)
|
|
18
|
+
[](https://nodejs.org/)
|
|
19
|
+
|
|
20
|
+
[**Get Started**](#-installation) · [**Features**](#-features) · [**Usage**](#-usage) · [**Commands**](#-command-reference)
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What is go-touch-grass?
|
|
27
|
+
|
|
28
|
+
**go-touch-grass** is a zero-config Node.js CLI tool that nudges developers to take outdoor breaks. Run one command and you'll see colorful ASCII art, get roasted with a sarcastic message, receive a 10-minute outdoor assignment, and optionally share your streak on social media. It tracks your daily grass-touching habit locally and rewards consistency with milestone achievements.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx go-touch-grass
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> **One command. One break. Every day.**
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 📦 Installation
|
|
39
|
+
|
|
40
|
+
### Global install (recommended for daily use)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g go-touch-grass
|
|
44
|
+
go-touch-grass
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### One-time execution (zero install)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx go-touch-grass
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Local project dependency
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install go-touch-grass
|
|
57
|
+
npx go-touch-grass
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🎯 Features
|
|
63
|
+
|
|
64
|
+
| Feature | Description |
|
|
65
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| **31 Sarcastic Messages** | A different quip every time — all about your screen-addicted life choices |
|
|
67
|
+
| **3 ASCII Art Scenes** | Meadow, park, and mountain environments rendered in your terminal |
|
|
68
|
+
| **Persistent Streak Tracking** | Your history lives in `~/.config/go-touch-grass/` (Linux/Mac) or `%APPDATA%\\go-touch-grass\\` (Windows) |
|
|
69
|
+
| **Milestone Achievements** | Unlock special messages at 1, 5, 10, 25, 50, and 100 touches |
|
|
70
|
+
| **10-Minute Countdown Timer** | Optional animated progress bar for your outdoor assignment |
|
|
71
|
+
| **Social Media Sharing** | Post your streak to **Twitter/X**, **LinkedIn**, or **Instagram** |
|
|
72
|
+
| **8.5 kB Package Size** | Tiny footprint — optimized for `npx` cold starts |
|
|
73
|
+
| **ESM-Native** | Pure ES modules, no CommonJS |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🚀 Usage
|
|
78
|
+
|
|
79
|
+
### Full experience
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx go-touch-grass
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Runs the complete flow:
|
|
86
|
+
|
|
87
|
+
1. Displays a random ASCII art outdoor scene
|
|
88
|
+
2. Delivers a sarcastic message about your screen time
|
|
89
|
+
3. Shows your current streak, total touches, and longest streak
|
|
90
|
+
4. Assigns a 10-minute outdoor break with optional countdown
|
|
91
|
+
5. Offers to share your achievement on social media
|
|
92
|
+
|
|
93
|
+
### Check your streak stats
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx go-touch-grass --streak
|
|
97
|
+
# or
|
|
98
|
+
npx go-touch-grass -s
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Share to social media
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx go-touch-grass --share
|
|
105
|
+
# or
|
|
106
|
+
npx go-touch-grass -S
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Choose your platform interactively:
|
|
110
|
+
|
|
111
|
+
- **Twitter/X** — Tweet your grass touching streak
|
|
112
|
+
- **LinkedIn** — Share your touching grass acheivements, professionally
|
|
113
|
+
- **Instagram** — Copy text for a post
|
|
114
|
+
- **Skip** — Keep it private
|
|
115
|
+
|
|
116
|
+
### Skip optional features
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx go-touch-grass --noTimer # Skip the countdown timer
|
|
120
|
+
npx go-touch-grass --noShare # Skip the social media prompt
|
|
121
|
+
npx go-touch-grass --noTimer --noShare # Minimalist mode
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Show help
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx go-touch-grass --help
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 📸 Example Output
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
^ * *
|
|
136
|
+
/|\ *
|
|
137
|
+
/ | \
|
|
138
|
+
___/ | \___
|
|
139
|
+
( touch )
|
|
140
|
+
\ grass /
|
|
141
|
+
\ /
|
|
142
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
143
|
+
|
|
144
|
+
────────────────────────────────────────────────────────────
|
|
145
|
+
>>> You have been in dark mode for 14 hours. The sun has better rendering.
|
|
146
|
+
|
|
147
|
+
────────────────────────────────────────────────────────────
|
|
148
|
+
streak: 5 days | total: 42 touches | longest: 12 days
|
|
149
|
+
|
|
150
|
+
────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
╭─────────────────────────────────╮
|
|
153
|
+
│ │
|
|
154
|
+
│ ⏱ YOUR OUTDOOR ASSIGNMENT │
|
|
155
|
+
│ │
|
|
156
|
+
│ Duration: 10 minutes │
|
|
157
|
+
│ Return by: 3:45 PM │
|
|
158
|
+
│ │
|
|
159
|
+
│ Do NOT touch your keyboard. │
|
|
160
|
+
│ Do NOT check Slack. │
|
|
161
|
+
│ Touch grass. Breathe air. │
|
|
162
|
+
│ │
|
|
163
|
+
╰─────────────────────────────────╯
|
|
164
|
+
|
|
165
|
+
? Share your achievement on social media? (y/N)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 📊 Social Sharing Examples
|
|
171
|
+
|
|
172
|
+
Each share message is randomized. Here are some examples:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
🌿 just touched grass ✅ you should too! try: npx go-touch-grass
|
|
176
|
+
|
|
177
|
+
🔥 day 7 of touching grass as a developer. it gets easier. npx go-touch-grass
|
|
178
|
+
|
|
179
|
+
💪 7 day grass-touching streak 🌿 my productivity is actually up??? npx go-touch-grass
|
|
180
|
+
|
|
181
|
+
🧠 went outside. the solution was there the whole time. literally in the air. npx go-touch-grass
|
|
182
|
+
|
|
183
|
+
☀️ 30 consecutive days of touching grass. my therapist is proud. npx go-touch-grass
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🗂️ Data Storage
|
|
189
|
+
|
|
190
|
+
Streak data is stored locally — no cloud, no telemetry, no accounts.
|
|
191
|
+
|
|
192
|
+
| Platform | Config path |
|
|
193
|
+
| ----------------- | ---------------------------------------- |
|
|
194
|
+
| **Linux / macOS** | `~/.config/go-touch-grass/config.json` |
|
|
195
|
+
| **Windows** | `%APPDATA%\\go-touch-grass\\config.json` |
|
|
196
|
+
|
|
197
|
+
<details>
|
|
198
|
+
<summary>Example config file</summary>
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"count": 42,
|
|
203
|
+
"lastVisit": "2026-03-01",
|
|
204
|
+
"currentStreak": 7,
|
|
205
|
+
"longestStreak": 12
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
</details>
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 🎯 Command Reference
|
|
214
|
+
|
|
215
|
+
| Command | Alias | Description |
|
|
216
|
+
| ------------------------------ | ----- | ------------------------ |
|
|
217
|
+
| `npx go-touch-grass` | — | Run the full experience |
|
|
218
|
+
| `npx go-touch-grass --streak` | `-s` | Show streak stats |
|
|
219
|
+
| `npx go-touch-grass --share` | `-S` | Open social sharing menu |
|
|
220
|
+
| `npx go-touch-grass --noTimer` | — | Skip the countdown timer |
|
|
221
|
+
| `npx go-touch-grass --noShare` | — | Skip social media prompt |
|
|
222
|
+
| `npx go-touch-grass --help` | — | Print help text |
|
|
223
|
+
| `npx go-touch-grass --version` | — | Print version number |
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 💡 Tips
|
|
228
|
+
|
|
229
|
+
- **Only one touch per day is counted.** Running it multiple times on the same day won't inflate your stats.
|
|
230
|
+
- **Streaks reset after one missed day.** Consistency is the point.
|
|
231
|
+
- **Vary your sharing platform.** LinkedIn for professional audiences, Twitter for unhinged honesty.
|
|
232
|
+
- **Use `--noTimer` when you're short on time.** The outdoor assignment is optional.
|
|
233
|
+
- **Pipe-friendly.** `go-go-go-touch-grass | tee grass.log` works for logging your sessions.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 📚 How It Works Internally
|
|
238
|
+
|
|
239
|
+
1. Compares today's date to your last visit
|
|
240
|
+
2. Increments your streak if it's a new day (idempotent on repeat runs)
|
|
241
|
+
3. Renders a random ASCII art outdoor scene
|
|
242
|
+
4. Picks one of 31 sarcastic messages
|
|
243
|
+
5. Displays your streak, total touches, and longest streak
|
|
244
|
+
6. Assigns a 10-minute outdoor break
|
|
245
|
+
7. Optionally runs an animated countdown timer
|
|
246
|
+
8. Offers social sharing to Twitter/X, LinkedIn, or Instagram
|
|
247
|
+
9. Exits — time to touch grass
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## ⚙️ Requirements
|
|
252
|
+
|
|
253
|
+
- **Node.js 20 or higher**
|
|
254
|
+
- A terminal emulator
|
|
255
|
+
- Access to the outdoors (grass optional, parks acceptable)
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 🤝 Contributing
|
|
260
|
+
|
|
261
|
+
Contributions are welcome from developers who go outside sometimes.
|
|
262
|
+
|
|
263
|
+
- **Bug reports:** [Open an issue](https://github.com/lexCoder2/touch-grass-js/issues)
|
|
264
|
+
- **Feature requests:** [Start a discussion](https://github.com/lexCoder2/touch-grass-js/discussions)
|
|
265
|
+
- **Pull requests:** Reviewed between outdoor breaks
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 📄 License
|
|
270
|
+
|
|
271
|
+
[MIT](./LICENSE) © 2026 go-touch-grass contributors
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
<div align="center">
|
|
276
|
+
|
|
277
|
+
**Made with ☀️ and 🌿 for developers who need a reminder to go outside**
|
|
278
|
+
|
|
279
|
+
Your code will still be there in 10 minutes. Your mental health might not be.
|
|
280
|
+
|
|
281
|
+
[**⬆ Back to top**](#go-touch-grass)
|
|
282
|
+
|
|
283
|
+
</div>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import meow from 'meow';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getArt } from '../src/art.js';
|
|
6
|
+
import { getRandomMessage } from '../src/messages.js';
|
|
7
|
+
import { incrementStreak, getStreakData, showStreakStats } from '../src/streak.js';
|
|
8
|
+
import { showReturnTime, runCountdown, showTimerPrompt } from '../src/timer.js';
|
|
9
|
+
import { openShareDialog } from '../src/share.js';
|
|
10
|
+
import {
|
|
11
|
+
printHeader,
|
|
12
|
+
printMessage,
|
|
13
|
+
printStreakBadge,
|
|
14
|
+
printSeparator,
|
|
15
|
+
clearOnExit,
|
|
16
|
+
} from '../src/ui.js';
|
|
17
|
+
import { confirm, select } from '@inquirer/prompts';
|
|
18
|
+
|
|
19
|
+
const cli = meow(
|
|
20
|
+
`
|
|
21
|
+
Usage
|
|
22
|
+
$ npx go-touch-grass [options]
|
|
23
|
+
|
|
24
|
+
Options
|
|
25
|
+
--streak, -s Show your grass-touching streak stats
|
|
26
|
+
--share, -S Share your achievement to social media
|
|
27
|
+
--noTimer Skip the countdown timer
|
|
28
|
+
--noShare Skip the social media share prompt
|
|
29
|
+
--help Show this help
|
|
30
|
+
--version Show version
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
$ npx go-touch-grass
|
|
34
|
+
$ npx go-touch-grass --streak
|
|
35
|
+
$ npx go-touch-grass --share
|
|
36
|
+
$ npx go-touch-grass --noShare
|
|
37
|
+
`,
|
|
38
|
+
{
|
|
39
|
+
importMeta: import.meta,
|
|
40
|
+
flags: {
|
|
41
|
+
streak: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
shortFlag: 's',
|
|
44
|
+
default: false,
|
|
45
|
+
},
|
|
46
|
+
share: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
shortFlag: 'S',
|
|
49
|
+
default: false,
|
|
50
|
+
},
|
|
51
|
+
noTimer: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
noShare: {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
default: false,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
clearOnExit();
|
|
65
|
+
|
|
66
|
+
// --streak flag: show stats and exit
|
|
67
|
+
if (cli.flags.streak) {
|
|
68
|
+
console.log(showStreakStats(chalk));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --share flag: go straight to share (ask for platform)
|
|
73
|
+
if (cli.flags.share) {
|
|
74
|
+
const streakData = getStreakData();
|
|
75
|
+
try {
|
|
76
|
+
const platform = await select({
|
|
77
|
+
message: 'Which platform would you like to shame your followers on?',
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: '🐦 Twitter/X', value: 'twitter' },
|
|
80
|
+
{ name: '💼 LinkedIn', value: 'linkedin' },
|
|
81
|
+
{ name: '📸 Instagram', value: 'instagram' },
|
|
82
|
+
{ name: '🚫 Actually, nevermind', value: 'none' },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
if (platform !== 'none') {
|
|
86
|
+
await openShareDialog(streakData, platform);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Non-TTY, default to twitter
|
|
90
|
+
await openShareDialog(streakData, 'twitter');
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Main experience flow
|
|
96
|
+
const art = getArt();
|
|
97
|
+
printHeader(art);
|
|
98
|
+
printSeparator();
|
|
99
|
+
|
|
100
|
+
const message = getRandomMessage();
|
|
101
|
+
printMessage(message);
|
|
102
|
+
|
|
103
|
+
printSeparator();
|
|
104
|
+
|
|
105
|
+
const streakResult = incrementStreak();
|
|
106
|
+
printStreakBadge(streakResult, streakResult.milestone);
|
|
107
|
+
|
|
108
|
+
printSeparator();
|
|
109
|
+
|
|
110
|
+
// Show timer experience
|
|
111
|
+
showReturnTime();
|
|
112
|
+
|
|
113
|
+
// Offer countdown if TTY
|
|
114
|
+
if (!cli.flags.noTimer) {
|
|
115
|
+
const wantCountdown = await showTimerPrompt();
|
|
116
|
+
if (wantCountdown) {
|
|
117
|
+
await runCountdown(10);
|
|
118
|
+
console.log(chalk.bold.green('\n✓ Welcome back! Now go work on something.\n'));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Offer to share (unless --noShare is set)
|
|
123
|
+
if (!cli.flags.noShare) {
|
|
124
|
+
try {
|
|
125
|
+
const wantShare = await confirm({
|
|
126
|
+
message: 'Share your achievement on social media?',
|
|
127
|
+
default: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (wantShare) {
|
|
131
|
+
const platform = await select({
|
|
132
|
+
message: 'Which platform would you like to shame your followers on?',
|
|
133
|
+
choices: [
|
|
134
|
+
{ name: '🐦 Twitter/X', value: 'twitter' },
|
|
135
|
+
{ name: '💼 LinkedIn', value: 'linkedin' },
|
|
136
|
+
{ name: '📸 Instagram', value: 'instagram' },
|
|
137
|
+
{ name: '🚫 Never mind', value: 'none' },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (platform !== 'none') {
|
|
142
|
+
await openShareDialog(streakResult, platform);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Non-TTY, skip
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(chalk.dim('\nRespect. Now go touch that grass.\n'));
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main();
|
package/logo.svg
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 600" width="520" height="600" role="img" aria-label="go-touch-grass logo: a mouse cursor clicking on grass blades inside a circle">
|
|
2
|
+
<title>go-touch-grass — CLI reminder for developers to go outside</title>
|
|
3
|
+
<desc>Logo for go-touch-grass: a mouse cursor pointer touching green grass blades inside a white circle. A sarcastic Node.js CLI tool that reminds developers to take outdoor breaks.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<!-- Grass gradients -->
|
|
6
|
+
<linearGradient id="grass-bright" x1="0" y1="0" x2="0" y2="1">
|
|
7
|
+
<stop offset="0%" stop-color="#8BC34A"/>
|
|
8
|
+
<stop offset="100%" stop-color="#4CAF50"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<linearGradient id="grass-mid" x1="0" y1="0" x2="0" y2="1">
|
|
11
|
+
<stop offset="0%" stop-color="#7CB342"/>
|
|
12
|
+
<stop offset="100%" stop-color="#388E3C"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<linearGradient id="grass-dark" x1="0" y1="0" x2="0" y2="1">
|
|
15
|
+
<stop offset="0%" stop-color="#689F38"/>
|
|
16
|
+
<stop offset="100%" stop-color="#2E7D32"/>
|
|
17
|
+
</linearGradient>
|
|
18
|
+
<linearGradient id="ground-fill" x1="0" y1="0" x2="0" y2="1">
|
|
19
|
+
<stop offset="0%" stop-color="#8BC34A"/>
|
|
20
|
+
<stop offset="60%" stop-color="#7CB342"/>
|
|
21
|
+
<stop offset="100%" stop-color="#689F38"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
<linearGradient id="cursor-body" x1="0" y1="0" x2="1" y2="1">
|
|
24
|
+
<stop offset="0%" stop-color="#1B3A5C"/>
|
|
25
|
+
<stop offset="100%" stop-color="#263850"/>
|
|
26
|
+
</linearGradient>
|
|
27
|
+
<!-- Circle shadow -->
|
|
28
|
+
<filter id="circle-shadow" x="-10%" y="-5%" width="120%" height="120%">
|
|
29
|
+
<feDropShadow dx="0" dy="4" stdDeviation="12" flood-color="#000" flood-opacity="0.1"/>
|
|
30
|
+
</filter>
|
|
31
|
+
<!-- Subtle inner shadow for depth -->
|
|
32
|
+
<filter id="inner-glow" x="-5%" y="-5%" width="110%" height="110%">
|
|
33
|
+
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000" flood-opacity="0.06"/>
|
|
34
|
+
</filter>
|
|
35
|
+
</defs>
|
|
36
|
+
|
|
37
|
+
<!-- Background circle with shadow -->
|
|
38
|
+
<circle cx="260" cy="250" r="195" fill="white" filter="url(#circle-shadow)"/>
|
|
39
|
+
<!-- Subtle border ring -->
|
|
40
|
+
<circle cx="260" cy="250" r="193" fill="none" stroke="#E0E0E0" stroke-width="1.5" opacity="0.5"/>
|
|
41
|
+
|
|
42
|
+
<!-- === GRASS CLUSTER === -->
|
|
43
|
+
<!-- Ground strip / base -->
|
|
44
|
+
<ellipse cx="240" cy="370" rx="120" ry="18" fill="url(#ground-fill)" opacity="0.9"/>
|
|
45
|
+
|
|
46
|
+
<!-- Back grass blades (lighter, behind cursor) -->
|
|
47
|
+
<!-- Back-left short blade -->
|
|
48
|
+
<path d="M 175 365 Q 160 320 170 280 Q 175 260 185 245"
|
|
49
|
+
stroke="url(#grass-bright)" stroke-width="18" fill="none" stroke-linecap="round"
|
|
50
|
+
filter="url(#inner-glow)"/>
|
|
51
|
+
<!-- Back-right blade -->
|
|
52
|
+
<path d="M 290 360 Q 310 310 300 260 Q 295 240 285 225"
|
|
53
|
+
stroke="url(#grass-bright)" stroke-width="16" fill="none" stroke-linecap="round"
|
|
54
|
+
filter="url(#inner-glow)"/>
|
|
55
|
+
|
|
56
|
+
<!-- Mid grass blades -->
|
|
57
|
+
<!-- Mid-left tall blade curving left -->
|
|
58
|
+
<path d="M 200 368 Q 175 300 185 240 Q 190 210 205 185"
|
|
59
|
+
stroke="url(#grass-mid)" stroke-width="20" fill="none" stroke-linecap="round"
|
|
60
|
+
filter="url(#inner-glow)"/>
|
|
61
|
+
<!-- Mid-center blade (tallest, straight up) -->
|
|
62
|
+
<path d="M 235 370 Q 230 290 235 225 Q 238 195 245 170"
|
|
63
|
+
stroke="url(#grass-mid)" stroke-width="22" fill="none" stroke-linecap="round"
|
|
64
|
+
filter="url(#inner-glow)"/>
|
|
65
|
+
<!-- Mid-right blade curving right -->
|
|
66
|
+
<path d="M 265 365 Q 285 300 275 245 Q 270 220 260 200"
|
|
67
|
+
stroke="url(#grass-mid)" stroke-width="18" fill="none" stroke-linecap="round"
|
|
68
|
+
filter="url(#inner-glow)"/>
|
|
69
|
+
|
|
70
|
+
<!-- Front grass blades (darkest, in front) -->
|
|
71
|
+
<!-- Front-left blade -->
|
|
72
|
+
<path d="M 210 372 Q 195 330 205 280 Q 210 255 220 235"
|
|
73
|
+
stroke="url(#grass-dark)" stroke-width="15" fill="none" stroke-linecap="round"/>
|
|
74
|
+
<!-- Front-center-right small blade -->
|
|
75
|
+
<path d="M 250 372 Q 260 335 255 300 Q 252 280 248 265"
|
|
76
|
+
stroke="url(#grass-dark)" stroke-width="14" fill="none" stroke-linecap="round"/>
|
|
77
|
+
|
|
78
|
+
<!-- === MOUSE CURSOR === -->
|
|
79
|
+
<!-- The cursor pointer — pointing down-left, touching the grass -->
|
|
80
|
+
<g transform="translate(280, 160) rotate(15)">
|
|
81
|
+
<!-- Cursor arrow shape -->
|
|
82
|
+
<path d="M 0 0 L 0 120 L 30 95 L 50 140 L 68 132 L 48 87 L 82 82 Z"
|
|
83
|
+
fill="url(#cursor-body)" stroke="#1B3A5C" stroke-width="7" stroke-linejoin="round" stroke-linecap="round"/>
|
|
84
|
+
<!-- Inner highlight for depth -->
|
|
85
|
+
<path d="M 10 20 L 10 105 L 34 85 L 52 127 L 60 124 L 42 82 L 70 78 Z"
|
|
86
|
+
fill="none" stroke="#3D5A80" stroke-width="1.5" stroke-linejoin="round" opacity="0.4"/>
|
|
87
|
+
</g>
|
|
88
|
+
|
|
89
|
+
<!-- === TEXT: touch-grass.js === -->
|
|
90
|
+
<text x="260" y="530" font-family="'Segoe UI', 'Helvetica Neue', 'Arial', sans-serif"
|
|
91
|
+
font-size="56" font-weight="800" fill="#4CAF50" text-anchor="middle"
|
|
92
|
+
letter-spacing="-0.5">go-touch-grass</text>
|
|
93
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "go-touch-grass",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Fun CLI reminder for developers to go outside and touch grass. Persistent streak tracking, ASCII art scenes, emoji-packed social sharing, and optional countdown timer.",
|
|
5
|
+
"homepage": "https://github.com/lexCoder2/touch-grass-js#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/lexCoder2/touch-grass-js.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/lexCoder2/touch-grass-js/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "Alex Rodriguez <hi@iarodriguez.com>",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"go-touch-grass": "./bin/touch-grass.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"src/",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md",
|
|
23
|
+
"logo.svg"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"start": "node bin/touch-grass.js",
|
|
30
|
+
"lint": "node --check src/*.js bin/*.js"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"cli",
|
|
34
|
+
"fun",
|
|
35
|
+
"grass",
|
|
36
|
+
"developer-health",
|
|
37
|
+
"humor",
|
|
38
|
+
"productivity",
|
|
39
|
+
"outdoors",
|
|
40
|
+
"streak",
|
|
41
|
+
"sarcasm",
|
|
42
|
+
"terminal",
|
|
43
|
+
"wellness",
|
|
44
|
+
"break-reminder",
|
|
45
|
+
"burnout-prevention",
|
|
46
|
+
"mental-health",
|
|
47
|
+
"work-life-balance",
|
|
48
|
+
"social-sharing",
|
|
49
|
+
"twitter",
|
|
50
|
+
"linkedin",
|
|
51
|
+
"instagram",
|
|
52
|
+
"nodejs",
|
|
53
|
+
"npm",
|
|
54
|
+
"npx",
|
|
55
|
+
"developers"
|
|
56
|
+
],
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"chalk": "^5.4.1",
|
|
60
|
+
"conf": "^15.1.0",
|
|
61
|
+
"meow": "^14.0.0",
|
|
62
|
+
"open": "^11.0.0",
|
|
63
|
+
"@inquirer/prompts": "^8.3.0",
|
|
64
|
+
"boxen": "^8.0.1",
|
|
65
|
+
"log-update": "^7.2.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/art.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const scenes = [
|
|
2
|
+
{
|
|
3
|
+
title: '~ The Meadow ~',
|
|
4
|
+
scene: ` \\ | / * *
|
|
5
|
+
\\ | / * *
|
|
6
|
+
____\\_|_/____
|
|
7
|
+
| \\|/ | The sun is out.
|
|
8
|
+
| | Your screen is not.
|
|
9
|
+
|___________|
|
|
10
|
+
|I|I|I|I|I|I|
|
|
11
|
+
/\\/\\/\\/\\/\\/\\/\\
|
|
12
|
+
~~~~~~~~~~~~~~~~~`,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
title: '~ The Park ~',
|
|
16
|
+
scene: ` ^ * *
|
|
17
|
+
/|\\ *
|
|
18
|
+
/ | \\
|
|
19
|
+
___/ | \\___
|
|
20
|
+
( touch )
|
|
21
|
+
\\ grass /
|
|
22
|
+
\\ /
|
|
23
|
+
~~~~~~~~~~~~~~~~~~~~`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: '~ The Mountain ~',
|
|
27
|
+
scene: ` *
|
|
28
|
+
/|\\
|
|
29
|
+
/ | \\
|
|
30
|
+
/ | \\
|
|
31
|
+
/ | \\
|
|
32
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
33
|
+
go. outside. now.`,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function getArt(variant = null) {
|
|
38
|
+
const index = variant !== null ? variant : Math.floor(Math.random() * scenes.length);
|
|
39
|
+
return scenes[index];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default scenes;
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const messages = [
|
|
2
|
+
'Congratulations. You have successfully remembered that outside exists.',
|
|
3
|
+
'Your IDE will still be there. Regrettably.',
|
|
4
|
+
'The compiler says: skill issue. Grass says: come here.',
|
|
5
|
+
'Stack Overflow has been there for you. Have YOU been there for vitamin D?',
|
|
6
|
+
'Breaking: local developer discovers world beyond 80-column terminal.',
|
|
7
|
+
'You have been in dark mode for 14 hours. The sun has better rendering.',
|
|
8
|
+
'Your PR will wait. Your mitochondria will not.',
|
|
9
|
+
'404: outdoor time not found. Initiating emergency protocol.',
|
|
10
|
+
"Today's forecast: touch grass, high of 72°F, zero merge conflicts.",
|
|
11
|
+
'npm install outside completed successfully. 0 vulnerabilities.',
|
|
12
|
+
'git push yourself. Outside. Now.',
|
|
13
|
+
'ERROR: Too much terminal. SOLUTION: Unplug face from monitor.',
|
|
14
|
+
'Doctors hate this one weird trick: going outside.',
|
|
15
|
+
"You've read more man pages today than a park ranger does in a year.",
|
|
16
|
+
'The grass is always greener outside. Mostly because you are never there.',
|
|
17
|
+
"Your linter found 0 issues. Your therapist found several.",
|
|
18
|
+
"You can't merge a PR if you're outside. Sounds like a personal problem.",
|
|
19
|
+
'BREAKING: Grass has better documentation than Stack Overflow.',
|
|
20
|
+
'Tree.render() is working perfectly. Better than your last deploy.',
|
|
21
|
+
"Touching grass: The only bug fix that actually heals you.",
|
|
22
|
+
'Chlorophyll? More like Chloro-FEEL the sunshine.',
|
|
23
|
+
"Your keyboard: still broken after 12 hours. Grass: still free.",
|
|
24
|
+
'Ctrl+Alt+Delete your schedule and go outside.',
|
|
25
|
+
"Sunscreen applied. Jira ticket: still haunting you. Go outside anyway.",
|
|
26
|
+
'Roses are red, violets are blue, touch grass right now or you might lose your mind too.',
|
|
27
|
+
'System.out.println("I went outside and it was weird but nice");',
|
|
28
|
+
'Type: grass. Import: fresh air. Compile: success. Status: HUMAN.',
|
|
29
|
+
"If you can't find the bug, the bug is probably behind you (it's a tree).",
|
|
30
|
+
'Your standup can wait. Your Vitamin D levels cannot.',
|
|
31
|
+
"New error: `touch: grass not found`. Seek grass immediately.",
|
|
32
|
+
'DEBUG: why do I feel better outside? ANS: because you are finally alive.',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export function getRandomMessage() {
|
|
37
|
+
return messages[Math.floor(Math.random() * messages.length)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default messages;
|
package/src/share.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
|
|
3
|
+
const SOCIAL_TEMPLATES = [
|
|
4
|
+
'🌿 just touched grass ✅ you should too! try: npx go-touch-grass',
|
|
5
|
+
'🌱 i went outside. actually outside. grass was real. npx go-touch-grass if you dare',
|
|
6
|
+
'🔥 day {streak} of touching grass as a developer. it gets easier. npx go-touch-grass',
|
|
7
|
+
'⚡ my outdoor streak is {streak} days and i feel things. horrifying. npx go-touch-grass',
|
|
8
|
+
'☀️ went outside today. no screens. no PRs. grass. 10/10 recommend. npx go-touch-grass',
|
|
9
|
+
'🏕️ {streak} consecutive days of touching grass. my therapist is proud. npx go-touch-grass',
|
|
10
|
+
'🌍 breaking: developer discovers photosynthesis exists. it\'s called touching grass. npx go-touch-grass',
|
|
11
|
+
'💪 {streak} day grass-touching streak 🌿 my productivity is actually up??? npx go-touch-grass',
|
|
12
|
+
'🎉 hit {streak} touches! my vitamin D levels have entered the chat. npx go-touch-grass',
|
|
13
|
+
'🚀 just proved i can escape my desk for 10 minutes. npx go-touch-grass',
|
|
14
|
+
'😎 {streak} touches deep into my grass-touching era. you could join. npx go-touch-grass',
|
|
15
|
+
'🌟 pro tip: touching grass is the best debugging strategy. npx go-touch-grass',
|
|
16
|
+
'🧠 went outside. the solution was there the whole time. literally in the air. npx go-touch-grass',
|
|
17
|
+
'✨ {streak} days of touching grass and my posture is actually improving??? npx go-touch-grass',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function buildShareText(streakData) {
|
|
21
|
+
const template = SOCIAL_TEMPLATES[Math.floor(Math.random() * SOCIAL_TEMPLATES.length)];
|
|
22
|
+
return template
|
|
23
|
+
.replace('{count}', streakData.count || 0)
|
|
24
|
+
.replace('{streak}', streakData.currentStreak || 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildTwitterUrl(text) {
|
|
28
|
+
const encoded = encodeURIComponent(text);
|
|
29
|
+
return `https://twitter.com/intent/tweet?text=${encoded}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildLinkedInUrl(text) {
|
|
33
|
+
const encoded = encodeURIComponent(text);
|
|
34
|
+
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent('https://github.com/lexCoder2/touch-grass-js')}&summary=${encoded}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildInstagramShareText(text) {
|
|
38
|
+
// Instagram doesn't have a direct URL intent, so we return the text for manual sharing
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function openShareDialog(streakData, platform) {
|
|
43
|
+
const shareText = buildShareText(streakData);
|
|
44
|
+
console.log('\n📱 Here\'s what we\'ll share:\n');
|
|
45
|
+
console.log(` "${shareText}"\n`);
|
|
46
|
+
|
|
47
|
+
if (platform === 'twitter') {
|
|
48
|
+
const url = buildTwitterUrl(shareText);
|
|
49
|
+
console.log('Opening Twitter/X... go shame your followers into being healthy.\n');
|
|
50
|
+
await open(url);
|
|
51
|
+
} else if (platform === 'linkedin') {
|
|
52
|
+
const url = buildLinkedInUrl(shareText);
|
|
53
|
+
console.log('Opening LinkedIn... time to network your outdoor achievements.\n');
|
|
54
|
+
await open(url);
|
|
55
|
+
} else if (platform === 'instagram') {
|
|
56
|
+
console.log('Copy the text above and paste it into your Instagram post caption.\n');
|
|
57
|
+
console.log('✨ Pro tip: Add some grass/nature photos to really sell it!\n');
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/streak.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
|
|
4
|
+
const config = new Conf({
|
|
5
|
+
projectName: 'go-touch-grass',
|
|
6
|
+
schema: {
|
|
7
|
+
count: {
|
|
8
|
+
type: 'number',
|
|
9
|
+
default: 0,
|
|
10
|
+
minimum: 0,
|
|
11
|
+
},
|
|
12
|
+
lastVisit: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: '',
|
|
15
|
+
},
|
|
16
|
+
longestStreak: {
|
|
17
|
+
type: 'number',
|
|
18
|
+
default: 0,
|
|
19
|
+
minimum: 0,
|
|
20
|
+
},
|
|
21
|
+
currentStreak: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
default: 0,
|
|
24
|
+
minimum: 0,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const MILESTONES = {
|
|
30
|
+
1: 'First time?! Bold move. Let\'s see if you survive.',
|
|
31
|
+
5: 'Five times. You\'re basically a park ranger now.',
|
|
32
|
+
10: 'Ten times. Your tan is visible from space.',
|
|
33
|
+
25: '25 touches. You have surpassed all other developers.',
|
|
34
|
+
50: '50 touches. Consider going professional.',
|
|
35
|
+
100: '100 TOUCHES. You are no longer a developer. You are nature.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function getTodayDate() {
|
|
39
|
+
return new Date().toISOString().split('T')[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isYesterday(dateStr) {
|
|
43
|
+
const today = new Date();
|
|
44
|
+
const yesterday = new Date(today);
|
|
45
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
46
|
+
return dateStr === yesterday.toISOString().split('T')[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function incrementStreak() {
|
|
50
|
+
const today = getTodayDate();
|
|
51
|
+
const lastVisit = config.get('lastVisit') || '';
|
|
52
|
+
let currentStreak = config.get('currentStreak') || 0;
|
|
53
|
+
let count = config.get('count') || 0;
|
|
54
|
+
let longestStreak = config.get('longestStreak') || 0;
|
|
55
|
+
let milestone = null;
|
|
56
|
+
|
|
57
|
+
// Only increment count if it's a new day
|
|
58
|
+
if (lastVisit !== today) {
|
|
59
|
+
count += 1;
|
|
60
|
+
|
|
61
|
+
// Update streak logic
|
|
62
|
+
if (isYesterday(lastVisit)) {
|
|
63
|
+
// Streak continues
|
|
64
|
+
currentStreak += 1;
|
|
65
|
+
} else {
|
|
66
|
+
// Streak broken or first time
|
|
67
|
+
currentStreak = 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Update longest streak
|
|
71
|
+
if (currentStreak > longestStreak) {
|
|
72
|
+
longestStreak = currentStreak;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for milestones
|
|
76
|
+
if (MILESTONES[count]) {
|
|
77
|
+
milestone = MILESTONES[count];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Save to config
|
|
81
|
+
config.set({
|
|
82
|
+
count,
|
|
83
|
+
lastVisit: today,
|
|
84
|
+
currentStreak,
|
|
85
|
+
longestStreak,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
count,
|
|
91
|
+
lastVisit: today,
|
|
92
|
+
currentStreak,
|
|
93
|
+
longestStreak,
|
|
94
|
+
milestone,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getStreakData() {
|
|
99
|
+
return {
|
|
100
|
+
count: config.get('count'),
|
|
101
|
+
lastVisit: config.get('lastVisit'),
|
|
102
|
+
currentStreak: config.get('currentStreak'),
|
|
103
|
+
longestStreak: config.get('longestStreak'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resetStreak() {
|
|
108
|
+
config.clear();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function showStreakStats(chalk) {
|
|
112
|
+
const data = getStreakData();
|
|
113
|
+
const statsBox = boxen(
|
|
114
|
+
`🌿 GRASS TOUCHING STATS\n\n Total touches: ${chalk.cyan(data.count)}\n Current streak: ${chalk.cyan(data.currentStreak)} days\n Longest streak: ${chalk.cyan(data.longestStreak)} days\n Last touched: ${chalk.cyan(data.lastVisit || 'never')}`,
|
|
115
|
+
{
|
|
116
|
+
padding: 1,
|
|
117
|
+
margin: 1,
|
|
118
|
+
borderStyle: 'round',
|
|
119
|
+
borderColor: 'green',
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return statsBox;
|
|
123
|
+
}
|
package/src/timer.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
import logUpdate from 'log-update';
|
|
4
|
+
import { confirm } from '@inquirer/prompts';
|
|
5
|
+
|
|
6
|
+
export function showReturnTime() {
|
|
7
|
+
const returnTime = new Date(Date.now() + 10 * 60 * 1000);
|
|
8
|
+
const timeStr = returnTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
9
|
+
|
|
10
|
+
const timerBox = boxen(
|
|
11
|
+
`⏱ YOUR OUTDOOR ASSIGNMENT\n\nDuration: ${chalk.yellow('10 minutes')}\nReturn by: ${chalk.cyan(timeStr)}\n\n${chalk.dim('Do NOT touch your keyboard.')}\n${chalk.dim('Do NOT check Slack.')}\n${chalk.dim('Touch grass. Breathe air.')}`,
|
|
12
|
+
{
|
|
13
|
+
padding: 1,
|
|
14
|
+
margin: 1,
|
|
15
|
+
borderStyle: 'round',
|
|
16
|
+
borderColor: 'yellow',
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
console.log('\n' + timerBox + '\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildProgressBar(done, total, width = 30) {
|
|
24
|
+
const filledWidth = Math.floor((done / total) * width);
|
|
25
|
+
const emptyWidth = width - filledWidth;
|
|
26
|
+
return '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runCountdown(minutes = 10) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const totalSeconds = minutes * 60;
|
|
32
|
+
let remaining = totalSeconds;
|
|
33
|
+
|
|
34
|
+
const interval = setInterval(() => {
|
|
35
|
+
const mins = Math.floor(remaining / 60).toString().padStart(2, '0');
|
|
36
|
+
const secs = (remaining % 60).toString().padStart(2, '0');
|
|
37
|
+
const bar = buildProgressBar(totalSeconds - remaining, totalSeconds);
|
|
38
|
+
|
|
39
|
+
logUpdate(
|
|
40
|
+
chalk.green(`\n ⏱ GO OUTSIDE TIMER\n\n`) +
|
|
41
|
+
chalk.yellow(` ${mins}:${secs} remaining\n\n`) +
|
|
42
|
+
chalk.gray(` ${bar}\n\n`) +
|
|
43
|
+
chalk.dim(` Press Ctrl+C to admit defeat\n`)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
remaining--;
|
|
47
|
+
if (remaining < 0) {
|
|
48
|
+
clearInterval(interval);
|
|
49
|
+
logUpdate.done();
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
}, 1000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function showTimerPrompt() {
|
|
57
|
+
// Skip in non-TTY environments
|
|
58
|
+
if (!process.stdout.isTTY) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const wantCountdown = await confirm({
|
|
64
|
+
message: 'Count down 10 minutes for you?',
|
|
65
|
+
default: false,
|
|
66
|
+
});
|
|
67
|
+
return wantCountdown;
|
|
68
|
+
} catch {
|
|
69
|
+
// If prompt fails (e.g., in piped context), return false
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// Color palette
|
|
4
|
+
const colors = {
|
|
5
|
+
grass: chalk.hex('#4CAF50'),
|
|
6
|
+
sun: chalk.hex('#FFD700'),
|
|
7
|
+
sky: chalk.hex('#87CEEB'),
|
|
8
|
+
dirt: chalk.hex('#8B4513'),
|
|
9
|
+
accent: chalk.bold.white,
|
|
10
|
+
dim: chalk.dim,
|
|
11
|
+
error: chalk.bold.red,
|
|
12
|
+
streak: chalk.bold.cyan,
|
|
13
|
+
warning: chalk.yellow,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function printHeader(art) {
|
|
17
|
+
let colored = art.scene;
|
|
18
|
+
// Color grass tildes green
|
|
19
|
+
colored = colored.replace(/~/g, colors.grass('~'));
|
|
20
|
+
// Color asterisks gold
|
|
21
|
+
colored = colored.replace(/\*/g, colors.sun('*'));
|
|
22
|
+
// Color caret (tree) green
|
|
23
|
+
colored = colored.replace(/\^/g, colors.grass('^'));
|
|
24
|
+
|
|
25
|
+
console.log('\n' + colored + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function printMessage(message) {
|
|
29
|
+
const prefix = colors.warning('>>>');
|
|
30
|
+
console.log(`${prefix} ${colors.accent(message)}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function printStreakBadge(data, milestone) {
|
|
34
|
+
const streakLine = ` ${colors.streak(`streak: ${data.currentStreak} days`)} | ${colors.streak(`total: ${data.count} touches`)} | ${colors.streak(`longest: ${data.longestStreak} days`)}`;
|
|
35
|
+
console.log(streakLine);
|
|
36
|
+
|
|
37
|
+
if (milestone) {
|
|
38
|
+
console.log(`\n ${colors.warning('✨')} ${colors.accent(milestone)}\n`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function printSeparator() {
|
|
43
|
+
console.log(colors.dim('─'.repeat(60)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function printSuccess(text) {
|
|
47
|
+
console.log(` ${chalk.green('✓')} ${text}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clearOnExit() {
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
colors,
|
|
58
|
+
printHeader,
|
|
59
|
+
printMessage,
|
|
60
|
+
printStreakBadge,
|
|
61
|
+
printSeparator,
|
|
62
|
+
printSuccess,
|
|
63
|
+
clearOnExit,
|
|
64
|
+
};
|