Skip to content

Commit fbd377d

Browse files
committed
feat: enhance installation and task management with cross-platform support
- Refactored `install.sh` to streamline wizard invocation with dynamic arguments. - Updated installation documentation to include Linux systemd service instructions. - Enhanced task management CLI to support postponing alarms with user input for future tasks. - Added cross-platform file opening functionality for generated reports and visualizations. - Introduced default notification sounds based on the operating system. - Improved internationalization for new task features and prompts.
1 parent 9fa03fc commit fbd377d

12 files changed

Lines changed: 407 additions & 54 deletions

File tree

documentation/docs/cli/task-management.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ rmd add "Biology homework" 9 1 1
3636
```
3737

3838
### Interactive Mode
39-
If you run `rmd add` directly without parameters in an interactive terminal, or if you omit some parameters, the system will display a friendly **Task Creator Wizard** panel, guiding you through a couple of quick questions one by one with emojis to complete the task description, priority level, and task type.
39+
If you run `rmd add` directly without parameters in an interactive terminal, or if you omit some parameters, the system will display a friendly **Task Creator Wizard** panel, guiding you through a couple of quick questions one by one with emojis to complete the task description, priority level, task type, and the optional **postpone days** (to make it a future task whose daily urgent alarm starts later). Press Enter at the postpone prompt to skip it and start the alarm today.
4040

4141
If you are running in a non-interactive environment (such as a script), omitting the required parameters will print an error message and exit with an error code. Any omitted optional parameters (like task type) will fall back to their default values (e.g. task type 1).
4242

documentation/docs/installation.md

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,18 @@ It installs `rmd` as the primary CLI and keeps `reminder` as a compatibility ali
4848
* Register the background service (on macOS and Linux).
4949

5050
2. **Finalize Setup**:
51-
Follow the on-screen instructions to load the service. Typically, this involves:
52-
```bash
53-
launchctl load ~/Library/LaunchAgents/com.sergiudm.schedule.management.reminder.plist
54-
```
51+
Follow the on-screen instructions to start the background service.
52+
- **macOS** (launchd):
53+
```bash
54+
launchctl load ~/Library/LaunchAgents/com.sergiudm.schedule.management.reminder.plist
55+
```
56+
- **Linux** (systemd user service):
57+
```bash
58+
systemctl --user start schedule-management.service
59+
systemctl --user enable schedule-management.service # auto-start at login
60+
```
61+
> Make sure lingering is enabled so the user service runs without an
62+
> active login session: `loginctl enable-linger "$USER"`.
5563

5664
### Method 2: Manual Installation
5765

@@ -148,11 +156,17 @@ Confirm that everything is working correctly.
148156
```
149157
*Expected output: A list of available commands.*
150158
151-
2. **Check Service Status** (macOS):
152-
```bash
153-
launchctl list | grep schedule
154-
```
155-
*Expected output: A process ID and status code (usually 0).*
159+
2. **Check Service Status**:
160+
- **macOS**:
161+
```bash
162+
launchctl list | grep schedule
163+
```
164+
*Expected output: A process ID and status code (usually 0).*
165+
- **Linux**:
166+
```bash
167+
systemctl --user status schedule-management.service
168+
```
169+
*Expected output: `active (running)` with recent log lines.*
156170
157171
3. **View Schedule**:
158172
```bash
@@ -165,9 +179,15 @@ Confirm that everything is working correctly.
165179
To completely remove the application:
166180
167181
1. **Unload the Service**:
168-
```bash
169-
launchctl unload ~/Library/LaunchAgents/com.sergiudm.schedule.management.reminder.plist
170-
```
182+
- **macOS**:
183+
```bash
184+
launchctl unload ~/Library/LaunchAgents/com.sergiudm.schedule.management.reminder.plist
185+
```
186+
- **Linux**:
187+
```bash
188+
systemctl --user stop schedule-management.service
189+
systemctl --user disable schedule-management.service
190+
```
171191
172192
2. **Remove Configuration & Data**:
173193
```bash

documentation/docs/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Selected sources:
6161
* **Time Points**: Instant, one-off reminders (e.g., "Hydrate", "Bedtime").
6262
* **Common Routines**: Define daily habits once, apply them everywhere.
6363
* **CLI Power**: A comprehensive command-line interface for managing tasks, visualizing schedules, and controlling the daemon.
64-
* **System Integration**: Runs as a native background service (via `launchd` on macOS), ensuring reliability across reboots.
64+
* **System Integration**: Runs as a native background service (via `launchd` on macOS or a `systemd` user service on Linux), ensuring reliability across reboots.
6565

6666
## Why TOML?
6767

install.sh

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,20 @@ configure_configs() {
400400
extra_args+=("--yes")
401401
fi
402402

403+
local wizard_invocation=(
404+
"$INSTALL_DIR/.venv/bin/python" "$wizard_script"
405+
--config-dir "$target_config_dir"
406+
)
403407
if [[ -d "$template_dir" ]]; then
404-
"$INSTALL_DIR/.venv/bin/python" "$wizard_script" \
405-
--config-dir "$target_config_dir" \
406-
--template-dir "$template_dir" \
407-
"${extra_args[@]:-}"
408+
wizard_invocation+=(--template-dir "$template_dir")
408409
else
409410
log_warning "Template directory not found at $template_dir; using config directory as template source."
410-
"$INSTALL_DIR/.venv/bin/python" "$wizard_script" \
411-
--config-dir "$target_config_dir" \
412-
"${extra_args[@]:-}"
413411
fi
412+
if [[ ${#extra_args[@]} -gt 0 ]]; then
413+
wizard_invocation+=("${extra_args[@]}")
414+
fi
415+
416+
"${wizard_invocation[@]}"
414417

415418
log_info "Validated active config directory: $target_config_dir"
416419
log_success "Configuration checks complete"

src/schedule_management/commands/service.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
HABIT_PATH,
3232
)
3333
from schedule_management.i18n import _t
34+
from schedule_management.platform import open_file
3435
from schedule_management.config_layout import (
3536
list_config_ids,
3637
preview_active_config_dir,
@@ -398,12 +399,9 @@ def report_command(args) -> int:
398399
if report_path:
399400
print("\n" + _t("✅ Report generated: {path}").format(path=report_path))
400401

401-
# Try to open on macOS
402-
if sys.platform == "darwin":
403-
try:
404-
subprocess.run(["open", str(report_path)], check=False)
405-
except Exception:
406-
pass # Silent fail for opening
402+
# Try to open the generated report in the default viewer (cross-platform)
403+
if not open_file(report_path):
404+
pass # No opener available; the report path was already printed above
407405

408406
return 0
409407
else:

src/schedule_management/commands/status.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
$ rmd view # Generate schedule PDF
1515
"""
1616

17-
import subprocess
1817
import sys
1918
from datetime import date, datetime, timedelta
2019
from pathlib import Path
@@ -32,6 +31,7 @@
3231

3332
from schedule_management import SETTINGS_PATH, ODD_PATH, EVEN_PATH
3433
from schedule_management.i18n import _t
34+
from schedule_management.platform import open_file
3535
from schedule_management.config import ScheduleConfig, WeeklySchedule
3636
from schedule_management.synced_schedule import (
3737
apply_synced_schedule,
@@ -410,24 +410,14 @@ def view_command(args) -> int:
410410

411411
print("\n" + _t("📁 Visualization file generated:"))
412412

413-
# Open PDF on macOS
414-
if sys.platform == "darwin":
415-
print("\n" + _t("🖼️ Opening visualization..."))
416-
try:
417-
import platform
418-
419-
if platform.system() == "Windows":
420-
desktop_path = Path.home() / "Desktop"
421-
else:
422-
desktop_path = Path.home() / "Desktop"
423-
424-
pdf_path = desktop_path / "schedule_visualization.pdf"
425-
subprocess.run(
426-
["open", str(pdf_path)],
427-
check=False,
428-
)
429-
except Exception as e:
430-
print(_t("⚠️ Could not open file: {e}").format(e=e))
413+
# Open the generated PDF in the default viewer (cross-platform)
414+
print("\n" + _t("🖼️ Opening visualization..."))
415+
try:
416+
pdf_path = Path.home() / "Desktop" / "schedule_visualization.pdf"
417+
if not open_file(pdf_path):
418+
print(_t("⚠️ Could not open file automatically. File saved to: {path}").format(path=pdf_path))
419+
except Exception as e:
420+
print(_t("⚠️ Could not open file: {e}").format(e=e))
431421

432422
return 0
433423

src/schedule_management/commands/tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,42 @@ def add_task(args) -> int:
177177
else:
178178
console.print("[bold yellow]" + _t("⚠️ Oops! Invalid selection. Please choose a valid number from the list.") + "[/bold yellow]")
179179

180+
# Optional: postpone the daily urgent alarm to make this a future task.
181+
if postpone is None:
182+
console.print(
183+
"\n[bold cyan]"
184+
+ _t("🗓️ Postpone the daily urgent alarm? Enter days from now (0 or empty = start today): ")
185+
+ "[/bold cyan]"
186+
)
187+
while True:
188+
try:
189+
postpone_input = console.input(
190+
"[bold cyan]" + _t("Postpone days (default 0): ") + "[/bold cyan]"
191+
).strip()
192+
except (EOFError, KeyboardInterrupt):
193+
print("\n" + _t("👋 Operation cancelled. Have a great day! ✨"))
194+
return 1
195+
if postpone_input == "":
196+
postpone = 0
197+
break
198+
try:
199+
postpone_val = int(postpone_input)
200+
if postpone_val < 0:
201+
console.print(
202+
"[bold yellow]"
203+
+ _t("⚠️ Oops! Postpone days must be a non-negative integer. Let's try that again! 🌟")
204+
+ "[/bold yellow]"
205+
)
206+
continue
207+
postpone = postpone_val
208+
break
209+
except ValueError:
210+
console.print(
211+
"[bold yellow]"
212+
+ _t("⚠️ Oops! Postpone days needs to be a valid number. Please enter a non-negative integer! 🔢")
213+
+ "[/bold yellow]"
214+
)
215+
180216
# Validate priority
181217
if priority <= 0:
182218
print(_t("❌ Error: Priority must be a positive integer"))

src/schedule_management/config.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@
2828
import tomllib
2929

3030

31+
# Default notification sounds per platform. These are only used when a config
32+
# does not set ``sound_file`` explicitly; if the file is absent at runtime the
33+
# alarm still fires (the dialog appears, the sound is simply skipped).
34+
_DEFAULT_SOUND_MACOS = "/System/Library/Sounds/Ping.aiff"
35+
# Ships with sound-theme-freedesktop on most GNOME/Ubuntu/Fedora installs.
36+
_DEFAULT_SOUND_LINUX = "/usr/share/sounds/freedesktop/stereo/complete.oga"
37+
38+
39+
def _default_sound_file() -> str:
40+
"""Return the platform-appropriate default notification sound path."""
41+
from schedule_management.platform import get_platform
42+
43+
if get_platform() == "macos":
44+
return _DEFAULT_SOUND_MACOS
45+
return _DEFAULT_SOUND_LINUX
46+
47+
3148
# =============================================================================
3249
# TOML FILE LOADING
3350
# =============================================================================
@@ -106,10 +123,13 @@ def sound_file(self) -> str:
106123
"""
107124
Path to the notification sound file.
108125
109-
Returns:
110-
Sound file path (default: macOS Ping sound)
126+
Returns the configured ``sound_file`` if set; otherwise falls back to a
127+
platform-appropriate system sound (macOS ``Ping.aiff`` or the
128+
freedesktop ``complete.oga`` on Linux). On platforms where the default
129+
file is missing, ``play_sound`` degrades gracefully to no audio while
130+
the alarm dialog still appears.
111131
"""
112-
return self.settings.get("sound_file", "/System/Library/Sounds/Ping.aiff")
132+
return self.settings.get("sound_file", _default_sound_file())
113133

114134
@property
115135
def alarm_interval(self) -> int:

src/schedule_management/i18n.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def get_language() -> str:
7676
"Enter Task Type Number: ": "请输入任务类型编号:",
7777
"⚠️ Oops! Invalid selection. Please choose a valid number from the list.": "⚠️ 哎呀!选择无效。请从列表中选择一个有效的编号。",
7878
"❌ Error: Invalid task type. Choose from: {choices}": "❌ 错误:无效的任务类型。可选范围:{choices}",
79+
"🗓️ Postpone the daily urgent alarm? Enter days from now (0 or empty = start today): ": "🗓️ 要推迟每日紧急提醒吗?请输入从今天起的天数(0 或留空 = 今天开始):",
80+
"Postpone days (default 0): ": "推迟天数(默认 0):",
81+
"⚠️ Oops! Postpone days must be a non-negative integer. Let's try that again! 🌟": "⚠️ 哎呀!推迟天数必须是非负整数。让我们再试一次!🌟",
82+
"⚠️ Oops! Postpone days needs to be a valid number. Please enter a non-negative integer! 🔢": "⚠️ 哎呀!推迟天数必须是一个有效的数字。请输入非负整数!🔢",
7983
"Legend:": "图例:",
8084
"👋 Operation cancelled. Have a great day! ✨": "👋 操作已取消。祝您度过美好的一天!✨",
8185
"❌ Error: Priority must be a positive integer": "❌ 错误:优先级必须是正整数",

src/schedule_management/platform.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
>>> show_dialog('Reminder: Time for a break!')
2020
"""
2121

22+
import shutil
2223
import subprocess
2324
import sys
25+
from pathlib import Path
2426

2527

2628
# =============================================================================
@@ -52,6 +54,43 @@ def get_platform() -> str:
5254
return "unknown"
5355

5456

57+
def open_file(path: str | Path) -> bool:
58+
"""
59+
Open a file or directory using the platform's default application.
60+
61+
Picks the native "open" command for the current platform and falls back
62+
silently if no suitable opener is found.
63+
64+
Args:
65+
path: File or directory path to open.
66+
67+
Returns:
68+
True if an opener command was available and launched, False otherwise.
69+
Note: a True result does not guarantee the application actually opened
70+
the file; the opener process is spawned non-blocking.
71+
72+
Example:
73+
>>> open_file('/path/to/report.pdf')
74+
"""
75+
platform_name = get_platform()
76+
if platform_name == "macos":
77+
openers = ["open"]
78+
elif platform_name == "linux":
79+
openers = ["xdg-open"]
80+
else:
81+
openers = ["xdg-open", "open"]
82+
83+
for opener in openers:
84+
if shutil.which(opener) is None:
85+
continue
86+
try:
87+
subprocess.Popen([opener, str(path)])
88+
return True
89+
except (FileNotFoundError, OSError):
90+
continue
91+
return False
92+
93+
5594
# =============================================================================
5695
# SOUND PLAYBACK
5796
# =============================================================================

0 commit comments

Comments
 (0)