fantasy-claude 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/VERSION +1 -0
  4. package/cli.sh +7 -0
  5. package/config.json +135 -0
  6. package/configure.py +1922 -0
  7. package/configure.sh +5 -0
  8. package/hooks/notify-auth.sh +17 -0
  9. package/hooks/notify-elicitation.sh +17 -0
  10. package/hooks/notify-idle.sh +9 -0
  11. package/hooks/notify-permission.sh +17 -0
  12. package/hooks/notify.sh +38 -0
  13. package/hooks/sounds.sh +55 -0
  14. package/install.sh +67 -0
  15. package/package.json +25 -0
  16. package/sounds/error/.gitkeep +0 -0
  17. package/sounds/error/FFAAAAH.mp3 +0 -0
  18. package/sounds/error/lego-break.mp3 +0 -0
  19. package/sounds/notification/.gitkeep +0 -0
  20. package/statusline/elements/battery.sh +12 -0
  21. package/statusline/elements/burn-rate.sh +6 -0
  22. package/statusline/elements/context-pct.sh +40 -0
  23. package/statusline/elements/cwd.sh +21 -0
  24. package/statusline/elements/datetime.sh +2 -0
  25. package/statusline/elements/file-entropy.sh +31 -0
  26. package/statusline/elements/git-branch.sh +3 -0
  27. package/statusline/elements/github-repo.sh +6 -0
  28. package/statusline/elements/haiku.sh +77 -0
  29. package/statusline/elements/model.sh +33 -0
  30. package/statusline/elements/mood.sh +27 -0
  31. package/statusline/elements/moon-phase.sh +31 -0
  32. package/statusline/elements/pomodoro.sh +39 -0
  33. package/statusline/elements/reset-time.sh +15 -0
  34. package/statusline/elements/session-cost.sh +42 -0
  35. package/statusline/elements/session-duration.sh +43 -0
  36. package/statusline/elements/streak.sh +47 -0
  37. package/statusline/elements/usage-5h.sh +6 -0
  38. package/statusline/statusline.sh +223 -0
package/configure.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Thin wrapper — launches the interactive TUI configurator
3
+ set -euo pipefail
4
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec python3 "$REPO_DIR/configure.py" "$@"
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Notification hook for auth_success events
3
+ input=$(cat)
4
+ msg=$(echo "$input" | python3 -c "
5
+ import sys, json
6
+ try:
7
+ print(json.load(sys.stdin).get('message', 'Authentication succeeded'))
8
+ except:
9
+ print('Authentication succeeded')
10
+ " 2>/dev/null)
11
+ [ -z "$msg" ] && msg="Authentication succeeded"
12
+
13
+ if [[ "$OSTYPE" == "darwin"* ]]; then
14
+ printf '\e]9;\n\n✅ %s\a' "$msg" > /dev/tty 2>/dev/null
15
+ else
16
+ notify-send "Claude Code" "✅ $msg" 2>/dev/null
17
+ fi
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Notification hook for elicitation_dialog events
3
+ input=$(cat)
4
+ msg=$(echo "$input" | python3 -c "
5
+ import sys, json
6
+ try:
7
+ print(json.load(sys.stdin).get('message', 'Claude has a question'))
8
+ except:
9
+ print('Claude has a question')
10
+ " 2>/dev/null)
11
+ [ -z "$msg" ] && msg="Claude has a question"
12
+
13
+ if [[ "$OSTYPE" == "darwin"* ]]; then
14
+ printf '\e]9;\n\n❓ %s\a' "$msg" > /dev/tty 2>/dev/null
15
+ else
16
+ notify-send "Claude Code" "❓ $msg" 2>/dev/null
17
+ fi
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # Notification hook for idle_prompt events
3
+ input=$(cat)
4
+
5
+ if [[ "$OSTYPE" == "darwin"* ]]; then
6
+ printf '\e]9;\n\n💤 Ready for your input\a' > /dev/tty 2>/dev/null
7
+ else
8
+ notify-send "Claude Code" "💤 Ready for your input" 2>/dev/null
9
+ fi
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Notification hook for permission_prompt events
3
+ input=$(cat)
4
+ msg=$(echo "$input" | python3 -c "
5
+ import sys, json
6
+ try:
7
+ print(json.load(sys.stdin).get('message', 'Claude needs your permission'))
8
+ except:
9
+ print('Claude needs your permission')
10
+ " 2>/dev/null)
11
+ [ -z "$msg" ] && msg="Claude needs your permission"
12
+
13
+ if [[ "$OSTYPE" == "darwin"* ]]; then
14
+ printf '\e]9;\n\n🔐 %s\a' "$msg" > /dev/tty 2>/dev/null
15
+ else
16
+ notify-send "Claude Code" "🔐 $msg" 2>/dev/null
17
+ fi
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ # Hook script for notifications when Claude Code needs user input.
3
+ # Uses iTerm2 OSC 9 escape sequence on macOS, notify-send on Linux.
4
+
5
+ input=$(cat)
6
+
7
+ # Build notification message with emoji prefix based on type
8
+ message=$(echo "$input" | python3 -c "
9
+ import sys, json
10
+ try:
11
+ d = json.load(sys.stdin)
12
+ ntype = d.get('notification_type', '')
13
+ msg = d.get('message', '')
14
+
15
+ if ntype == 'permission_prompt':
16
+ print(f'🔐 {msg or \"Claude needs your permission\"}')
17
+ elif ntype == 'idle_prompt':
18
+ print('💤 Ready for your input')
19
+ elif ntype == 'elicitation_dialog':
20
+ print(f'❓ {msg or \"Claude has a question\"}')
21
+ elif ntype == 'auth_success':
22
+ print(f'✅ {msg or \"Authentication succeeded\"}')
23
+ else:
24
+ print(msg or 'Needs your attention')
25
+ except:
26
+ print('Needs your attention')
27
+ " 2>/dev/null)
28
+
29
+ [ -z "$message" ] && message="Needs your attention"
30
+
31
+ if [[ "$OSTYPE" == "darwin"* ]]; then
32
+ # iTerm2 OSC 9: write to /dev/tty to bypass stdout capture
33
+ printf '\e]9;\n\n%s\a' "$message" > /dev/tty 2>/dev/null
34
+ else
35
+ if command -v notify-send &>/dev/null; then
36
+ notify-send "Claude Code" "$message" 2>/dev/null
37
+ fi
38
+ fi
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # Hook script for sound events — reads config.json and plays the configured sound
3
+
4
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ CONFIG="$REPO_DIR/config.json"
6
+
7
+ input=$(cat)
8
+
9
+ # Determine event type and whether it's an error
10
+ hook_event=$(echo "$input" | python3 -c "
11
+ import sys, json
12
+ try:
13
+ d = json.load(sys.stdin)
14
+ event = d.get('hook_event_name', '')
15
+ is_error = d.get('tool_response', {}).get('is_error', False)
16
+ if event == 'PostToolUse' and is_error:
17
+ print('on_error')
18
+ elif event == 'Stop':
19
+ print('on_stop')
20
+ elif event == 'Notification':
21
+ print('on_notification')
22
+ except:
23
+ pass
24
+ " 2>/dev/null)
25
+
26
+ [ -z "$hook_event" ] && exit 0
27
+
28
+ sound_name=$(python3 -c "
29
+ import json
30
+ with open('$CONFIG') as f:
31
+ d = json.load(f)
32
+ name = d.get('sounds', {}).get('$hook_event')
33
+ if name:
34
+ print(name)
35
+ " 2>/dev/null)
36
+
37
+ [ -z "$sound_name" ] || [ "$sound_name" = "None" ] && exit 0
38
+
39
+ # Map event to sounds subdirectory
40
+ case "$hook_event" in
41
+ on_error) sound_dir="$REPO_DIR/sounds/error" ;;
42
+ on_stop) sound_dir="$REPO_DIR/sounds/notification" ;;
43
+ on_notification) sound_dir="$REPO_DIR/sounds/notification" ;;
44
+ *) exit 0 ;;
45
+ esac
46
+
47
+ sound_file="$sound_dir/$sound_name.mp3"
48
+ [ ! -f "$sound_file" ] && exit 0
49
+
50
+ if [[ "$OSTYPE" == "darwin"* ]]; then
51
+ afplay "$sound_file" &
52
+ else
53
+ command -v paplay &>/dev/null && paplay "$sound_file" & || \
54
+ command -v aplay &>/dev/null && aplay "$sound_file" &
55
+ fi
package/install.sh ADDED
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ CLAUDE_DIR="$HOME/.claude"
6
+ SETTINGS="$CLAUDE_DIR/settings.json"
7
+
8
+ VERSION=$(cat "$REPO_DIR/VERSION" 2>/dev/null || echo "unknown")
9
+ echo "Installing claude-hooks v$VERSION from $REPO_DIR"
10
+
11
+ # Make all scripts executable
12
+ chmod +x "$REPO_DIR/statusline/statusline.sh"
13
+ chmod +x "$REPO_DIR/statusline/elements/"*.sh
14
+ chmod +x "$REPO_DIR/hooks/sounds.sh"
15
+ chmod +x "$REPO_DIR/hooks/notify-permission.sh"
16
+ chmod +x "$REPO_DIR/hooks/notify-idle.sh"
17
+ chmod +x "$REPO_DIR/hooks/notify-elicitation.sh"
18
+ chmod +x "$REPO_DIR/hooks/notify-auth.sh"
19
+
20
+ # Create ~/.claude if needed
21
+ mkdir -p "$CLAUDE_DIR"
22
+
23
+ # Patch settings.json
24
+ if [ ! -f "$SETTINGS" ]; then
25
+ echo "{}" > "$SETTINGS"
26
+ fi
27
+
28
+ python3 - <<EOF
29
+ import json
30
+
31
+ settings_path = "$SETTINGS"
32
+ repo_dir = "$REPO_DIR"
33
+
34
+ with open(settings_path) as f:
35
+ settings = json.load(f)
36
+
37
+ settings["statusLine"] = {
38
+ "type": "command",
39
+ "command": f"bash {repo_dir}/statusline/statusline.sh"
40
+ }
41
+
42
+ hook_command = f"bash {repo_dir}/hooks/sounds.sh"
43
+
44
+ hooks = settings.setdefault("hooks", {})
45
+
46
+ for event in ["PostToolUse", "Stop", "Notification"]:
47
+ event_hooks = hooks.setdefault(event, [])
48
+ already = any(
49
+ any(h.get("command") == hook_command for h in entry.get("hooks", []))
50
+ for entry in event_hooks
51
+ )
52
+ if not already:
53
+ event_hooks.append({
54
+ "matcher": ".*" if event == "PostToolUse" else "",
55
+ "hooks": [{"type": "command", "command": hook_command}]
56
+ })
57
+
58
+ with open(settings_path, "w") as f:
59
+ json.dump(settings, f, indent=2)
60
+ f.write("\n")
61
+
62
+ print("settings.json updated.")
63
+ EOF
64
+
65
+ echo ""
66
+ echo "Done! Restart Claude Code to apply changes."
67
+ echo "Edit config.json to customize statusline elements and sounds."
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fantasy-claude",
3
+ "version": "1.0.3",
4
+ "description": "Statusline and sound hooks for Claude Code",
5
+ "bin": {
6
+ "fantasy-claude": "./cli.sh",
7
+ "fanclaude": "./cli.sh"
8
+ },
9
+ "files": [
10
+ "statusline/",
11
+ "hooks/",
12
+ "sounds/",
13
+ "config.json",
14
+ "configure.py",
15
+ "configure.sh",
16
+ "install.sh",
17
+ "cli.sh",
18
+ "VERSION"
19
+ ],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/itsAlfantasy/fantasy-claude"
24
+ }
25
+ }
File without changes
Binary file
Binary file
File without changes
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ if [[ "$OSTYPE" == "darwin"* ]]; then
3
+ percent=$(pmset -g batt 2>/dev/null | grep -o '[0-9]*%' | head -1)
4
+ charging=$(pmset -g batt 2>/dev/null | grep -q 'AC Power' && echo "+" || echo "")
5
+ [ -n "$percent" ] && echo "${percent}${charging}"
6
+ else
7
+ bat_path=$(ls /sys/class/power_supply/BAT*/capacity 2>/dev/null | head -1)
8
+ if [ -n "$bat_path" ]; then
9
+ percent=$(cat "$bat_path")
10
+ echo "${percent}%"
11
+ fi
12
+ fi
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ # Token burn rate, read from cache written by statusline-command.sh
3
+ CACHE="/tmp/claude_burn_cache_$(id -u)"
4
+ [ -f "$CACHE" ] || { echo "--"; exit 0; }
5
+ rate=$(cat "$CACHE")
6
+ [ -n "$rate" ] && echo "${rate}" || echo "--"
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # Context window usage % from the most recent Claude Code session JSONL
3
+ python3 - << 'PYEOF'
4
+ import glob, json, os, sys
5
+
6
+ CLAUDE_DIR = os.path.expanduser("~/.claude/projects")
7
+ CONTEXT_WINDOW = 200_000 # all current Claude models
8
+
9
+ # Find the most recently modified JSONL
10
+ files = glob.glob(f"{CLAUDE_DIR}/**/*.jsonl", recursive=True)
11
+ if not files:
12
+ print("--%")
13
+ sys.exit()
14
+
15
+ latest = max(files, key=os.path.getmtime)
16
+
17
+ last_usage = None
18
+ with open(latest, errors="replace") as f:
19
+ for line in f:
20
+ try:
21
+ d = json.loads(line)
22
+ if d.get("type") == "assistant":
23
+ u = d.get("message", {}).get("usage")
24
+ if u:
25
+ last_usage = u
26
+ except Exception:
27
+ pass
28
+
29
+ if not last_usage:
30
+ print("--%")
31
+ sys.exit()
32
+
33
+ total = (
34
+ last_usage.get("input_tokens", 0)
35
+ + last_usage.get("cache_creation_input_tokens", 0)
36
+ + last_usage.get("cache_read_input_tokens", 0)
37
+ )
38
+ pct = round(total / CONTEXT_WINDOW * 100)
39
+ print(f"{pct}%")
40
+ PYEOF
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
3
+ python3 - "$REPO_DIR/config.json" << 'PYEOF'
4
+ import os, json, sys
5
+
6
+ basename_only = False
7
+ try:
8
+ with open(sys.argv[1]) as f:
9
+ basename_only = json.load(f).get('statusline', {}).get('element_settings', {}).get('cwd', {}).get('basename_only', False)
10
+ except Exception:
11
+ pass
12
+
13
+ path = os.getcwd()
14
+ home = os.path.expanduser('~')
15
+ if basename_only:
16
+ print(os.path.basename(path))
17
+ else:
18
+ if path.startswith(home):
19
+ path = '~' + path[len(home):]
20
+ print(path)
21
+ PYEOF
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ echo "$(date '+%H:%M')"
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ python3 - << 'PYEOF'
3
+ import glob, os, json
4
+ claude_dir = os.path.expanduser('~/.claude/projects')
5
+ files = glob.glob(f'{claude_dir}/**/*.jsonl', recursive=True)
6
+ if not files:
7
+ print("0")
8
+ exit()
9
+ latest = max(files, key=os.path.getmtime)
10
+ touched = set()
11
+ try:
12
+ with open(latest, errors='replace') as f:
13
+ for line in f:
14
+ try:
15
+ d = json.loads(line)
16
+ if d.get('type') == 'assistant':
17
+ content = d.get('message', {}).get('content', [])
18
+ if isinstance(content, list):
19
+ for block in content:
20
+ if isinstance(block, dict) and block.get('type') == 'tool_use':
21
+ if block.get('name') in ('Edit', 'Write', 'Read', 'NotebookEdit'):
22
+ inp = block.get('input', {})
23
+ path = inp.get('file_path') or inp.get('path')
24
+ if path:
25
+ touched.add(path)
26
+ except Exception:
27
+ pass
28
+ except Exception:
29
+ pass
30
+ print(len(touched))
31
+ PYEOF
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ branch=$(git -C "${PWD}" rev-parse --abbrev-ref HEAD 2>/dev/null)
3
+ [ -n "$branch" ] && echo "$branch"
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ remote=$(git -C "${PWD}" remote get-url origin 2>/dev/null) || exit 0
3
+ [[ "$remote" == *github.com* ]] || exit 0
4
+ # Extract "user/repo" from SSH or HTTPS URL
5
+ repo=$(printf '%s' "$remote" | sed -E 's|.*github\.com[:/]||; s|\.git$||')
6
+ [ -n "$repo" ] && echo "$repo"
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ python3 - << 'PYEOF'
3
+ from datetime import date
4
+ import hashlib
5
+ LINE1 = [
6
+ "silent bytes flow past",
7
+ "code runs through the night",
8
+ "empty terminal",
9
+ "cursor blinks alone",
10
+ "git log shows the truth",
11
+ "null pointer drifts by",
12
+ "the merge conflict blooms",
13
+ "readme has no words",
14
+ "todo left undone",
15
+ "tests pass then they fail",
16
+ "branches everywhere",
17
+ "rebase brings despair",
18
+ "the build pipeline runs",
19
+ "cache miss haunts the night",
20
+ "function returns void",
21
+ "syntax error fades",
22
+ "the deadline draws near",
23
+ "infinite loop sleeps",
24
+ "latency is low",
25
+ "stack trace reveals all",
26
+ ]
27
+ LINE2 = [
28
+ "a thousand tokens consumed",
29
+ "the model thinks in silence",
30
+ "pull request awaits review",
31
+ "dependencies update",
32
+ "semicolons forgotten",
33
+ "undefined is not a type",
34
+ "the linter disagrees",
35
+ "context window fills slowly",
36
+ "another meeting skipped",
37
+ "the database is down",
38
+ "port eight thousand answers",
39
+ "environment not found here",
40
+ "the docker image builds",
41
+ "commit message says fix things",
42
+ "the production logs scroll by",
43
+ "authentication fails again",
44
+ "the schema migration runs",
45
+ "CI pipeline turns red",
46
+ "the feature flag is on",
47
+ "stack trace reveals the truth now",
48
+ ]
49
+ LINE3 = [
50
+ "ship it anyway",
51
+ "coffee grows cold fast",
52
+ "merge to main at last",
53
+ "rollback in the dark",
54
+ "it works on my box",
55
+ "close the ticket now",
56
+ "hotfix deployed live",
57
+ "the logs tell no lies",
58
+ "grep finds the answer",
59
+ "restart fixes all",
60
+ "refactor some day",
61
+ "tech debt compounds fast",
62
+ "version two will fix",
63
+ "backup exists not",
64
+ "the server breathes on",
65
+ "cron job runs at dawn",
66
+ "delete the old branch",
67
+ "the cursor still blinks",
68
+ "EOF awaits",
69
+ "chmod fixes all",
70
+ ]
71
+ d = date.today()
72
+ seed = int(hashlib.md5(d.isoformat().encode()).hexdigest(), 16)
73
+ l1 = LINE1[seed % len(LINE1)]
74
+ l2 = LINE2[(seed >> 8) % len(LINE2)]
75
+ l3 = LINE3[(seed >> 16) % len(LINE3)]
76
+ print(f"{l1} / {l2} / {l3}")
77
+ PYEOF
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # Current Claude model from the most recent session JSONL
3
+ python3 - << 'PYEOF'
4
+ import glob, json, os, re, sys
5
+
6
+ CLAUDE_DIR = os.path.expanduser("~/.claude/projects")
7
+ files = glob.glob(f"{CLAUDE_DIR}/**/*.jsonl", recursive=True)
8
+ if not files:
9
+ print("--")
10
+ sys.exit()
11
+
12
+ latest = max(files, key=os.path.getmtime)
13
+
14
+ model = None
15
+ with open(latest, errors="replace") as f:
16
+ for line in f:
17
+ try:
18
+ d = json.loads(line)
19
+ if d.get("type") == "assistant":
20
+ m = d.get("message", {}).get("model")
21
+ if m:
22
+ model = m
23
+ except Exception:
24
+ pass
25
+
26
+ if not model:
27
+ print("--")
28
+ sys.exit()
29
+
30
+ name = model.replace("claude-", "")
31
+ name = re.sub(r"-\d{8}$", "", name)
32
+ print(name)
33
+ PYEOF
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
3
+ python3 - "$REPO_DIR/config.json" << 'PYEOF'
4
+ import json, sys
5
+ from datetime import datetime
6
+
7
+ mood_set = 1
8
+ try:
9
+ with open(sys.argv[1]) as f:
10
+ mood_set = json.load(f).get('statusline', {}).get('element_settings', {}).get('mood', {}).get('mood_set', 1)
11
+ except Exception:
12
+ pass
13
+
14
+ SETS = {
15
+ 1: [(5, "😴"), (8, "☕"), (12, "🍕"), (14, "💻"), (18, "🌆"), (22, "🌙")],
16
+ 2: [(5, "🥱"), (8, "⚡"), (12, "🎯"), (14, "🔥"), (18, "🍺"), (22, "🌃")],
17
+ 3: [(5, "💤"), (8, "🌅"), (12, "☀️"), (14, "🌤️"), (18, "🌇"), (22, "🌌")],
18
+ }
19
+ hour = datetime.now().hour
20
+ emojis = SETS.get(mood_set, SETS[1])
21
+ result = emojis[-1][1]
22
+ for threshold, emoji in emojis:
23
+ if hour < threshold:
24
+ break
25
+ result = emoji
26
+ print(result)
27
+ PYEOF
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
3
+ python3 - "$REPO_DIR/config.json" << 'PYEOF'
4
+ import json, sys
5
+ from datetime import date
6
+
7
+ show_name = False
8
+ try:
9
+ with open(sys.argv[1]) as f:
10
+ show_name = json.load(f).get('statusline', {}).get('element_settings', {}).get('moon-phase', {}).get('show_name', False)
11
+ except Exception:
12
+ pass
13
+
14
+ d = date.today()
15
+ known_new = date(2000, 1, 6)
16
+ delta = (d - known_new).days
17
+ phase = (delta % 29.53058867) / 29.53058867
18
+ phases = [
19
+ ('🌑', 'New Moon'),
20
+ ('🌒', 'Waxing Crescent'),
21
+ ('🌓', 'First Quarter'),
22
+ ('🌔', 'Waxing Gibbous'),
23
+ ('🌕', 'Full Moon'),
24
+ ('🌖', 'Waning Gibbous'),
25
+ ('🌗', 'Last Quarter'),
26
+ ('🌘', 'Waning Crescent'),
27
+ ]
28
+ idx = round(phase * 8) % 8
29
+ emoji, name = phases[idx]
30
+ print(f"{emoji} {name}" if show_name else emoji)
31
+ PYEOF
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
3
+ POMO_FILE="/tmp/pomodoro_$(id -u)"
4
+
5
+ _get_duration() {
6
+ python3 - "$REPO_DIR/config.json" << 'PYEOF'
7
+ import json, sys
8
+ try:
9
+ with open(sys.argv[1]) as f:
10
+ d = json.load(f).get('statusline', {}).get('element_settings', {}).get('pomodoro', {}).get('duration', 25)
11
+ print(int(d) * 60)
12
+ except Exception:
13
+ print(1500)
14
+ PYEOF
15
+ }
16
+
17
+ if [ ! -f "$POMO_FILE" ]; then
18
+ dur_secs=$(_get_duration)
19
+ printf '%d:00\n' "$(( dur_secs / 60 ))"
20
+ exit 0
21
+ fi
22
+
23
+ IFS='|' read -r start_ts duration state < "$POMO_FILE"
24
+ now=$(date +%s)
25
+ elapsed=$(( now - start_ts ))
26
+ remaining=$(( duration - elapsed ))
27
+
28
+ if [ "$state" = "running" ]; then
29
+ if [ "$remaining" -le 0 ]; then
30
+ echo "done!"
31
+ else
32
+ printf '%d:%02d\n' "$(( remaining / 60 ))" "$(( remaining % 60 ))"
33
+ fi
34
+ elif [ "$state" = "paused" ]; then
35
+ printf '⏸%d:%02d\n' "$(( remaining / 60 ))" "$(( remaining % 60 ))"
36
+ else
37
+ dur_secs=$(_get_duration)
38
+ printf '%d:00\n' "$(( dur_secs / 60 ))"
39
+ fi
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # 5-hour window reset time, parsed from cache written by statusline-command.sh
3
+ CACHE="/tmp/claude_usage_cache_$(id -u)"
4
+ [ -f "$CACHE" ] || { echo "--"; exit 0; }
5
+ read -r _ resets_at < "$CACHE"
6
+ if [ -z "$resets_at" ] || [ "$resets_at" = "--" ]; then
7
+ echo "--"
8
+ exit 0
9
+ fi
10
+ # Strip sub-seconds and offset, convert to local time
11
+ TS_UTC="${resets_at%%.*}"
12
+ TS_UTC="${TS_UTC%+*}"
13
+ local_time=$(date -j -u -f "%Y-%m-%dT%H:%M:%S" "$TS_UTC" +"%H:%M" 2>/dev/null \
14
+ || date -d "$resets_at" +"%H:%M" 2>/dev/null)
15
+ echo "${local_time:---}"