fedora-csb-system-manager
1#!/usr/bin/env bash
2# journelly-manager - CLI tool for Journelly journal file manipulation via Emacs batch mode
3# Copyright (C) 2025 Vincent Demeester
4# Part of Claude Code Journal skill
5
6set -euo pipefail
7
8# Configuration
9SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10BATCH_FUNCTIONS="${BATCH_FUNCTIONS:-$SCRIPT_DIR/journelly-batch-functions.el}"
11EMACS="${EMACS:-emacs}"
12
13# Debug mode
14DEBUG="${DEBUG:-0}"
15
16# Colors for output (if not outputting JSON)
17if [[ -t 1 ]] && [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
18 RED='\033[0;31m'
19 GREEN='\033[0;32m'
20 YELLOW='\033[1;33m'
21 BLUE='\033[0;34m'
22 NC='\033[0m' # No Color
23else
24 RED=''
25 GREEN=''
26 YELLOW=''
27 BLUE=''
28 NC=''
29fi
30
31# Error handling
32error() {
33 echo -e "${RED}Error: $*${NC}" >&2
34 exit 1
35}
36
37debug() {
38 if [[ "$DEBUG" == "1" ]]; then
39 echo -e "${YELLOW}Debug: $*${NC}" >&2
40 fi
41}
42
43info() {
44 if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
45 echo -e "${BLUE}$*${NC}" >&2
46 fi
47}
48
49success() {
50 if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
51 echo -e "${GREEN}$*${NC}" >&2
52 fi
53}
54
55# Check dependencies
56check_deps() {
57 if ! command -v "$EMACS" &> /dev/null; then
58 error "Emacs not found. Set EMACS environment variable or install emacs."
59 fi
60
61 if [[ ! -f "$BATCH_FUNCTIONS" ]]; then
62 error "journelly-batch-functions.el not found at: $BATCH_FUNCTIONS"
63 fi
64}
65
66# Run Emacs batch command
67run_batch() {
68 local function_call="$1"
69 debug "Running: $EMACS --batch --load \"$BATCH_FUNCTIONS\" --eval \"$function_call\""
70
71 "$EMACS" --batch \
72 --load "$BATCH_FUNCTIONS" \
73 --eval "$function_call" 2>&1
74}
75
76# Usage information
77usage() {
78 cat <<EOF
79journelly-manager - CLI tool for managing Journelly journal files
80
81USAGE:
82 journelly-manager <command> [arguments]
83
84COMMANDS:
85 create FILE LOCATION CONTENT [options]
86 Create new journal entry
87
88 Options:
89 --latitude=LAT GPS latitude
90 --longitude=LON GPS longitude
91 --temperature=TEMP Temperature (e.g., "15,2°C")
92 --condition=COND Weather condition (e.g., "Cloudy")
93 --symbol=SYM Weather symbol (e.g., "cloud")
94 --content-file=PATH Read content from file instead
95
96 Examples:
97 journelly-manager create ~/desktop/org/Journelly.org "Home" "Today was great"
98
99 journelly-manager create ~/desktop/org/Journelly.org "Kyushu" \\
100 "Work session notes" \\
101 --latitude=48.8672 --longitude=2.1851 \\
102 --temperature="15,2°C" --condition="Partly Cloudy" --symbol="cloud.sun"
103
104 append FILE CONTENT [--content-file=PATH]
105 Append to today's journal entry
106
107 Examples:
108 journelly-manager append ~/desktop/org/Journelly.org "Additional thoughts"
109 journelly-manager append ~/desktop/org/Journelly.org --content-file=/tmp/notes.txt
110
111 list FILE [--limit=N]
112 List recent journal entries (default: 10)
113
114 Examples:
115 journelly-manager list ~/desktop/org/Journelly.org
116 journelly-manager list ~/desktop/org/Journelly.org --limit=20
117
118 search FILE QUERY
119 Search journal entries for text
120
121 Examples:
122 journelly-manager search ~/desktop/org/Journelly.org "claude"
123 journelly-manager search ~/desktop/org/Journelly.org "homelab"
124
125 get FILE DATE [TIME]
126 Get specific entry by date and optional time
127
128 Examples:
129 journelly-manager get ~/desktop/org/Journelly.org "2025-12-08"
130 journelly-manager get ~/desktop/org/Journelly.org "2025-12-08" "15:30"
131
132GLOBAL OPTIONS:
133 --help, -h Show this help message
134 --version Show version information
135 --debug Enable debug output
136
137ENVIRONMENT VARIABLES:
138 EMACS Path to Emacs binary (default: emacs)
139 BATCH_FUNCTIONS Path to journelly-batch-functions.el
140 DEBUG Enable debug mode (1 or 0)
141 JSON_OUTPUT Output JSON only (1 or 0)
142
143EXAMPLES:
144 # Create simple entry
145 journelly-manager create ~/desktop/org/Journelly.org "Home" \\
146 "Productive day working on Claude skills."
147
148 # Create entry with weather data
149 journelly-manager create ~/desktop/org/Journelly.org "Rue Jean Bourguignon" \\
150 "Evening reflection" \\
151 --latitude=48.86721 --longitude=2.18509 \\
152 --temperature="8,5°C" --condition="Clear" --symbol="moon.stars"
153
154 # Create entry with content from file
155 echo "My journal thoughts..." > /tmp/entry.txt
156 journelly-manager create ~/desktop/org/Journelly.org "Kyushu" \\
157 --content-file=/tmp/entry.txt
158
159 # Append to today's entry
160 journelly-manager append ~/desktop/org/Journelly.org \\
161 "Update: Made more progress on the project!"
162
163 # List recent entries
164 journelly-manager list ~/desktop/org/Journelly.org --limit=5
165
166 # Search for entries about work
167 journelly-manager search ~/desktop/org/Journelly.org "work"
168
169 # Get specific entry
170 journelly-manager get ~/desktop/org/Journelly.org "2025-12-08" "15:30"
171
172VERSION:
173 1.0.0
174
175AUTHOR:
176 Vincent Demeester <vincent@demeester.fr>
177
178SEE ALSO:
179 Journelly iOS App: https://journelly.com
180 Org-mode: https://orgmode.org
181EOF
182}
183
184# Parse command line arguments
185parse_args() {
186 local cmd="${1:-}"
187 shift || true
188
189 case "$cmd" in
190 create)
191 cmd_create "$@"
192 ;;
193 append)
194 cmd_append "$@"
195 ;;
196 list)
197 cmd_list "$@"
198 ;;
199 search)
200 cmd_search "$@"
201 ;;
202 get)
203 cmd_get "$@"
204 ;;
205 --help|-h|help)
206 usage
207 exit 0
208 ;;
209 --version)
210 echo "journelly-manager 1.0.0"
211 exit 0
212 ;;
213 "")
214 error "No command specified. Use --help for usage."
215 ;;
216 *)
217 error "Unknown command: $cmd. Use --help for usage."
218 ;;
219 esac
220}
221
222# Command: create
223cmd_create() {
224 local file="${1:-}"
225 local location="${2:-}"
226 local content="${3:-}"
227 shift 3 || error "create requires FILE LOCATION CONTENT arguments"
228
229 [[ -z "$file" ]] && error "FILE argument required"
230 [[ -z "$location" ]] && error "LOCATION argument required"
231 [[ ! -f "$file" ]] && error "File not found: $file"
232
233 # Parse optional arguments
234 local latitude=""
235 local longitude=""
236 local temperature=""
237 local condition=""
238 local symbol=""
239 local content_file=""
240
241 while [[ $# -gt 0 ]]; do
242 case "$1" in
243 --latitude=*)
244 latitude="${1#*=}"
245 ;;
246 --longitude=*)
247 longitude="${1#*=}"
248 ;;
249 --temperature=*)
250 temperature="${1#*=}"
251 ;;
252 --condition=*)
253 condition="${1#*=}"
254 ;;
255 --symbol=*)
256 symbol="${1#*=}"
257 ;;
258 --content-file=*)
259 content_file="${1#*=}"
260 [[ ! -f "$content_file" ]] && error "Content file not found: $content_file"
261 ;;
262 *)
263 error "Unknown option: $1"
264 ;;
265 esac
266 shift
267 done
268
269 # Build Emacs Lisp call
270 local elisp_call="(journelly-batch-create-entry \"$file\" \"$location\" \"$content\""
271
272 [[ -n "$latitude" ]] && elisp_call="$elisp_call \"$latitude\"" || elisp_call="$elisp_call nil"
273 [[ -n "$longitude" ]] && elisp_call="$elisp_call \"$longitude\"" || elisp_call="$elisp_call nil"
274 [[ -n "$temperature" ]] && elisp_call="$elisp_call \"$temperature\"" || elisp_call="$elisp_call nil"
275 [[ -n "$condition" ]] && elisp_call="$elisp_call \"$condition\"" || elisp_call="$elisp_call nil"
276 [[ -n "$symbol" ]] && elisp_call="$elisp_call \"$symbol\"" || elisp_call="$elisp_call nil"
277 [[ -n "$content_file" ]] && elisp_call="$elisp_call \"$content_file\"" || elisp_call="$elisp_call nil"
278
279 elisp_call="$elisp_call)"
280
281 info "Creating journal entry..."
282 local result
283 result=$(run_batch "$elisp_call")
284 echo "$result"
285
286 if echo "$result" | grep -q '"success":true'; then
287 success "Journal entry created successfully"
288 fi
289}
290
291# Command: append
292cmd_append() {
293 local file="${1:-}"
294 local content="${2:-}"
295 shift 2 || error "append requires FILE CONTENT arguments"
296
297 [[ -z "$file" ]] && error "FILE argument required"
298 [[ ! -f "$file" ]] && error "File not found: $file"
299
300 local content_file=""
301
302 while [[ $# -gt 0 ]]; do
303 case "$1" in
304 --content-file=*)
305 content_file="${1#*=}"
306 [[ ! -f "$content_file" ]] && error "Content file not found: $content_file"
307 ;;
308 *)
309 error "Unknown option: $1"
310 ;;
311 esac
312 shift
313 done
314
315 local elisp_call="(journelly-batch-append-to-today \"$file\" \"$content\""
316 [[ -n "$content_file" ]] && elisp_call="$elisp_call \"$content_file\"" || elisp_call="$elisp_call nil"
317 elisp_call="$elisp_call)"
318
319 info "Appending to today's entry..."
320 local result
321 result=$(run_batch "$elisp_call")
322 echo "$result"
323
324 if echo "$result" | grep -q '"success":true'; then
325 success "Content appended successfully"
326 fi
327}
328
329# Command: list
330cmd_list() {
331 local file="${1:-}"
332 shift || error "list requires FILE argument"
333
334 [[ -z "$file" ]] && error "FILE argument required"
335 [[ ! -f "$file" ]] && error "File not found: $file"
336
337 local limit=""
338
339 while [[ $# -gt 0 ]]; do
340 case "$1" in
341 --limit=*)
342 limit="${1#*=}"
343 ;;
344 *)
345 error "Unknown option: $1"
346 ;;
347 esac
348 shift
349 done
350
351 local elisp_call="(journelly-batch-list-entries \"$file\""
352 [[ -n "$limit" ]] && elisp_call="$elisp_call \"$limit\"" || elisp_call="$elisp_call nil"
353 elisp_call="$elisp_call)"
354
355 info "Listing recent entries..."
356 run_batch "$elisp_call"
357}
358
359# Command: search
360cmd_search() {
361 local file="${1:-}"
362 local query="${2:-}"
363 shift 2 || error "search requires FILE QUERY arguments"
364
365 [[ -z "$file" ]] && error "FILE argument required"
366 [[ -z "$query" ]] && error "QUERY argument required"
367 [[ ! -f "$file" ]] && error "File not found: $file"
368
369 local elisp_call="(journelly-batch-search \"$file\" \"$query\")"
370
371 info "Searching for: $query"
372 run_batch "$elisp_call"
373}
374
375# Command: get
376cmd_get() {
377 local file="${1:-}"
378 local date="${2:-}"
379 local time="${3:-}"
380 shift 2 || error "get requires FILE DATE arguments"
381
382 [[ -z "$file" ]] && error "FILE argument required"
383 [[ -z "$date" ]] && error "DATE argument required"
384 [[ ! -f "$file" ]] && error "File not found: $file"
385
386 local elisp_call="(journelly-batch-get-entry \"$file\" \"$date\""
387 [[ -n "$time" ]] && elisp_call="$elisp_call \"$time\"" || elisp_call="$elisp_call nil"
388 elisp_call="$elisp_call)"
389
390 info "Getting entry for: $date${time:+ at $time}"
391 run_batch "$elisp_call"
392}
393
394# Main
395main() {
396 check_deps
397 parse_args "$@"
398}
399
400main "$@"