Skip to content

Commit bd40d2a

Browse files
committed
Add persistent vars, status aliases, and tray timer UX/audio updates
1 parent a61a67d commit bd40d2a

14 files changed

Lines changed: 423 additions & 13 deletions

File tree

commands/set.py

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,146 @@
11
import sys
2+
import os
3+
import yaml
24
from modules.item_manager import read_item_data, write_item_data
35
from modules import variables as Variables
6+
from modules import status_utils
7+
from modules.scheduler import status_current_path
48

59
# --- Global Variables for Scripting ---
610
# This dictionary stores variables set by the 'set var' command.
711
# In a more robust system, this would be managed by the Console or a dedicated scripting engine
812
GLOBAL_VARS = {}
13+
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
14+
PROFILE_PATH = os.path.join(ROOT_DIR, "user", "profile", "profile.yml")
15+
TIMER_SETTINGS_PATH = os.path.join(ROOT_DIR, "user", "settings", "timer_settings.yml")
16+
TIMER_PROFILES_PATH = os.path.join(ROOT_DIR, "user", "settings", "timer_profiles.yml")
17+
18+
def _sync_status_var_to_yaml(var_name: str, var_value: str):
19+
"""
20+
Persist `status_*` variable assignments to current_status.yml and keep
21+
runtime mirrored vars in sync.
22+
"""
23+
raw_name = str(var_name or "").strip()
24+
if not raw_name.lower().startswith("status_"):
25+
return None, None
26+
27+
indicator = status_utils.status_slug(raw_name[len("status_"):])
28+
if not indicator:
29+
return "Invalid status variable name.", None
30+
normalized_value, err = status_utils.canonicalize_status_value(indicator, var_value)
31+
if err:
32+
return err, None
33+
34+
path = status_current_path()
35+
try:
36+
if os.path.exists(path):
37+
with open(path, "r", encoding="utf-8") as f:
38+
current = yaml.safe_load(f) or {}
39+
else:
40+
current = {}
41+
if not isinstance(current, dict):
42+
current = {}
43+
except Exception:
44+
current = {}
45+
46+
current[indicator] = normalized_value
47+
try:
48+
with open(path, "w", encoding="utf-8") as f:
49+
yaml.dump(current, f, default_flow_style=False)
50+
except Exception as e:
51+
return f"Failed to write status file: {e}", None
52+
53+
try:
54+
Variables.sync_status_vars(current)
55+
except Exception:
56+
pass
57+
return None, str(normalized_value)
58+
59+
60+
def _sync_nickname_var_to_profile(var_name: str, var_value: str):
61+
"""
62+
Persist nickname variable assignment to profile.yml and keep runtime var
63+
aligned with profile source-of-truth.
64+
"""
65+
raw_name = str(var_name or "").strip().lower()
66+
if raw_name != "nickname":
67+
return None, None
68+
69+
nickname = str(var_value or "").strip()
70+
if not nickname:
71+
return "Nickname cannot be empty.", None
72+
73+
profile = {}
74+
try:
75+
if os.path.exists(PROFILE_PATH):
76+
with open(PROFILE_PATH, "r", encoding="utf-8") as f:
77+
profile = yaml.safe_load(f) or {}
78+
if not isinstance(profile, dict):
79+
profile = {}
80+
except Exception:
81+
profile = {}
82+
83+
profile["nickname"] = nickname
84+
try:
85+
os.makedirs(os.path.dirname(PROFILE_PATH), exist_ok=True)
86+
with open(PROFILE_PATH, "w", encoding="utf-8") as f:
87+
yaml.dump(profile, f, default_flow_style=False, sort_keys=False)
88+
except Exception as e:
89+
return f"Failed to write profile nickname: {e}", None
90+
91+
return None, nickname
92+
93+
94+
def _sync_timer_profile_var_to_settings(var_name: str, var_value: str):
95+
"""
96+
Persist timer_profile variable assignment to timer_settings.yml.
97+
Validates profile exists in timer_profiles.yml when available.
98+
"""
99+
raw_name = str(var_name or "").strip().lower()
100+
if raw_name != "timer_profile":
101+
return None, None
102+
103+
profile_name = str(var_value or "").strip()
104+
if not profile_name:
105+
return "Timer profile cannot be empty.", None
106+
107+
profiles = {}
108+
try:
109+
if os.path.exists(TIMER_PROFILES_PATH):
110+
with open(TIMER_PROFILES_PATH, "r", encoding="utf-8") as f:
111+
profiles = yaml.safe_load(f) or {}
112+
if not isinstance(profiles, dict):
113+
profiles = {}
114+
except Exception:
115+
profiles = {}
116+
117+
if profiles and profile_name not in profiles:
118+
# Case-insensitive convenience for profile selection.
119+
lower_map = {str(k).lower(): str(k) for k in profiles.keys()}
120+
match = lower_map.get(profile_name.lower())
121+
if not match:
122+
return f"Unknown timer profile '{profile_name}'.", None
123+
profile_name = match
124+
125+
settings = {}
126+
try:
127+
if os.path.exists(TIMER_SETTINGS_PATH):
128+
with open(TIMER_SETTINGS_PATH, "r", encoding="utf-8") as f:
129+
settings = yaml.safe_load(f) or {}
130+
if not isinstance(settings, dict):
131+
settings = {}
132+
except Exception:
133+
settings = {}
134+
135+
settings["default_profile"] = profile_name
136+
try:
137+
os.makedirs(os.path.dirname(TIMER_SETTINGS_PATH), exist_ok=True)
138+
with open(TIMER_SETTINGS_PATH, "w", encoding="utf-8") as f:
139+
yaml.dump(settings, f, default_flow_style=False, sort_keys=False)
140+
except Exception as e:
141+
return f"Failed to write timer settings: {e}", None
142+
143+
return None, profile_name
9144

10145

11146
def run(args, properties):
@@ -41,9 +176,44 @@ def run(args, properties):
41176

42177
var_assignment = args[1]
43178
if ':' in var_assignment:
44-
var_name, var_value = var_assignment.split(':', 1)
45-
Variables.set_var(var_name, var_value)
46-
print(f"✅. Variable '{var_name}' set to '{var_value}'.")
179+
raw_var_name, var_value = var_assignment.split(':', 1)
180+
var_name = Variables.canonical_var_name(raw_var_name)
181+
err, normalized_status_value = _sync_status_var_to_yaml(var_name, var_value)
182+
if err:
183+
print(f"❌ {err}")
184+
return
185+
normalized_nickname_value = None
186+
normalized_timer_profile_value = None
187+
if normalized_status_value is None:
188+
nick_err, normalized_nickname_value = _sync_nickname_var_to_profile(var_name, var_value)
189+
if nick_err:
190+
print(f"❌ {nick_err}")
191+
return
192+
if normalized_status_value is None and normalized_nickname_value is None:
193+
timer_err, normalized_timer_profile_value = _sync_timer_profile_var_to_settings(var_name, var_value)
194+
if timer_err:
195+
print(f"❌ {timer_err}")
196+
return
197+
final_value = (
198+
normalized_status_value
199+
if normalized_status_value is not None
200+
else normalized_nickname_value
201+
if normalized_nickname_value is not None
202+
else normalized_timer_profile_value
203+
if normalized_timer_profile_value is not None
204+
else var_value
205+
)
206+
Variables.set_var(var_name, final_value)
207+
if str(raw_var_name).strip() != str(var_name).strip():
208+
print(f"✅. Variable '{raw_var_name}' (alias of '{var_name}') set to '{final_value}'.")
209+
else:
210+
print(f"✅. Variable '{var_name}' set to '{final_value}'.")
211+
if str(var_name).strip().lower().startswith("status_"):
212+
print("↳ Synced to current_status.yml")
213+
elif str(var_name).strip().lower() == "nickname":
214+
print("↳ Synced to profile.yml")
215+
elif str(var_name).strip().lower() == "timer_profile":
216+
print("↳ Synced to timer_settings.yml")
47217
else:
48218
print(f"❌ Invalid variable assignment: {var_assignment}. Expected format: <variable_name>:<value>")
49219
return
@@ -148,6 +318,10 @@ def get_help_message():
148318
Description: Sets properties of an item or defines a script variable.
149319
Example: set note MyMeetingNotes priority:high category:work
150320
Example: set var my_variable:some_value
321+
Example: set var status_energy:high # updates var and current_status.yml
322+
Example: set var location:home # alias of status_place; updates current_status.yml
323+
Example: set var nickname:Alice # updates var and user/profile/profile.yml
324+
Example: set var timer_profile:classic_pomodoro # updates var and user/settings/timer_settings.yml
151325
152326
Special (goals):
153327
set goal "<name>" template:true # mark goal as a template

commands/status.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from modules.scheduler import status_current_path, status_history_path_for_date
55
from modules import status_utils
6+
from modules import variables as Variables
67

78
def run(args, properties):
89
status_file_path = status_current_path()
@@ -18,6 +19,10 @@ def run(args, properties):
1819
with open(status_file_path, 'r') as f:
1920
current_status = yaml.safe_load(f)
2021
if current_status:
22+
try:
23+
Variables.sync_status_vars(current_status)
24+
except Exception:
25+
pass
2126
print("Current Status:")
2227
for key, value in current_status.items():
2328
print(f" {key}: {value}")
@@ -64,6 +69,10 @@ def run(args, properties):
6469
# Write updated status
6570
with open(status_file_path, 'w') as f:
6671
yaml.dump(current_status, f, default_flow_style=False)
72+
try:
73+
Variables.sync_status_vars(current_status)
74+
except Exception:
75+
pass
6776

6877
# Append to dated status history
6978
history_path = status_history_path_for_date(datetime.now())
@@ -99,7 +108,17 @@ def run(args, properties):
99108
print(f"❌ An unexpected error occurred: {e}")
100109

101110
def get_help_message():
102-
return "Views or sets user status variables.\n\nUsage: status <indicator>:<value>\n status (to view current status)\n\nExamples:\n status emotion:happy\n status energy:low"
111+
return (
112+
"Views or sets user status values.\n\n"
113+
"Usage: status <indicator>:<value>\n"
114+
" status (to view current status)\n\n"
115+
"Runtime variable mirror:\n"
116+
" Current status values are also exposed as @status_<indicator>\n"
117+
" Example: @status_energy, @status_focus\n\n"
118+
"Examples:\n"
119+
" status emotion:happy\n"
120+
" status energy:low"
121+
)
103122

104123
if __name__ == '__main__':
105124
# This block is for testing the command directly

commands/vars.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def run(args, properties):
1010
name = properties.get('name')
1111

1212
if name:
13-
val = vars_map.get(name)
13+
val = Variables.get_var(name)
1414
if val is None:
1515
print(f"No variable named '{name}'.")
1616
else:
@@ -31,5 +31,6 @@ def get_help_message():
3131
Description: Lists current script variables or a single variable by name.
3232
Example: vars
3333
Example: vars name:project
34+
Example: vars name:status_energy
3435
"""
3536

docs/dev/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This guide explains how Chronos fits together so you can extend it confidently.
77
- CLI Entrypoint — `modules/console.py`
88
- Adds project paths, sets UTF-8, and loads `commands/*.py` dynamically.
99
- Parses interactive input, CLI args, and `.chs` scripts (supports nested `if/elseif/else/end` and loop blocks).
10-
- Uses `modules/variables.py` for in-memory variables and token expansion (e.g., `@nickname`).
10+
- Uses `modules/variables.py` for in-memory variables and token expansion (e.g., `@nickname`, `@status_energy`).
1111
- Macro hooks: command execution is wrapped with BEFORE/AFTER hooks via `modules/macro_engine.py` (enabled by `user/scripts/macros/macros.yml`). Dashboard calls also pass through these hooks.
1212
- Autosuggest and autocomplete are registry-driven; see `docs/dev/autosuggest.md` for the slot model and refresh workflow.
1313

docs/dev/chs_scripting.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Chronos executes `.chs` scripts with one command per line. Lines support quoted
66

77
- Set a variable: `set var name:World`
88
- Use in any command: `echo Hello @name` or `echo Hello @{name}`
9+
- Current status is mirrored to vars like `@status_energy`, `@status_focus`, `@status_health`
10+
- `@location` is an alias of `@status_place` (for example, `set var location:home`)
11+
- Setting `status_*` vars writes through to status YAML: `set var status_energy:high`
12+
- `@timer_profile` mirrors timer default profile; set with `set var timer_profile:classic_pomodoro`
913
- Escape a literal `@`: use `@@`
1014
- Scope: Variables persist for the duration of the current console session or script execution.
1115
- Inspect and remove:
@@ -61,6 +65,7 @@ create note "IF Note @who" category:work priority:high
6165
6266
if exists note:"IF Note @who" then echo FOUND else echo MISSING
6367
if status:energy eq high and exists env:PATH then echo READY
68+
if @status_energy eq high and exists env:PATH then echo READY
6469
if note:"IF Note @who":priority matches ^h.* then echo STARTS_WITH_H
6570
if ( status:energy eq high and exists note:"IF Note @who" ) or status:emotion ne sad then echo OK
6671

docs/documentation_overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ today reschedule
123123
### 4. CHS Scripting Language
124124

125125
**Features**:
126-
- Variables: `@nickname`, `@var`, `@{var}`
126+
- Variables: `@nickname`, `@status_energy`, `@status_focus`, `@location` (alias `@status_place`), `@timer_profile`, `@var`, `@{var}`
127127
- Conditionals: `if/elseif/else/end` (block and single-line)
128128
- Loops: `repeat`, `for`, `while` (bounded)
129129
- Operators: `== != > < >= <= matches` (regex)

docs/guides/common_workflows.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Scripts (Automation)
436436

437437
- Variables
438438
- Use `@nickname` (from `user/profile/profile.yml`) in messages.
439+
- Use mirrored status vars in scripts/macros (for example `@status_energy`, `@status_focus`).
439440
- Set inside scripts with `set var <name> <value>` or via specific command outputs.
440441

441442
----------------------------------------

docs/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ Common commands (all item types now share the same verbs via `handle_command`)
127127

128128
Variables
129129
- The console seeds `@nickname` from `user/profile/profile.yml`. Use in scripts/messages.
130+
- Current status values are mirrored as runtime vars like `@status_energy`, `@status_focus`, `@status_health`.
131+
- `@location` is an alias of `@status_place` (same source, no duplicate storage).
132+
- Timer default profile is mirrored as `@timer_profile` (from `user/settings/timer_settings.yml`).
130133
- Set/read variables programmatically via `modules/variables.py` or CLI patterns.
131134

132135
## Dashboard

docs/reference/cli_commands.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Appends text to the content of an existing item.
3131
### `set`
3232
Sets properties of an item or defines a script variable.
3333
**Usage:** `set <type> <name> <property_key>:<value> [...]`
34+
**Notes:**
35+
- `set var status_<indicator>:<value>` also updates `user/current_status.yml` (same source used by `status`).
36+
- `set var location:<value>` is an alias for `set var status_place:<value>`.
37+
- `set var timer_profile:<name>` updates `user/settings/timer_settings.yml` (`default_profile`).
3438

3539
### `get`
3640
Retrieves the value of a specific property from an item.
@@ -171,6 +175,8 @@ Views or sets user status variables.
171175
**Usage:**
172176
- `status`
173177
- `status <indicator>:<value>`
178+
**Notes:**
179+
- Current status values are mirrored to runtime vars such as `@status_energy` and `@status_focus`.
174180

175181
### `complete`
176182
Marks an item as completed.
@@ -551,6 +557,10 @@ Runs a command while a condition remains true (bounded).
551557
### `vars`
552558
Lists current script variables or a single variable by name.
553559
**Usage:** `vars [name:<varname>]`
560+
**Examples:**
561+
- `vars`
562+
- `vars name:project`
563+
- `vars name:status_energy`
554564
555565
### `unset`
556566
Removes a script variable from the current session.

modules/console.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,32 @@ def _load_profile_and_seed_vars():
637637
Variables.set_var('nickname', nick)
638638
except Exception:
639639
pass
640+
641+
# Mirror current status values into runtime vars (e.g., @status_energy).
642+
status_candidates = [
643+
os.path.join(ROOT_DIR, "user", "current_status.yml"),
644+
os.path.join(ROOT_DIR, "user", "profile", "current_status.yml"),
645+
]
646+
status_map = {}
647+
for path in status_candidates:
648+
data = _safe_load_yaml(path)
649+
if isinstance(data, dict):
650+
status_map = data
651+
break
652+
try:
653+
Variables.sync_status_vars(status_map)
654+
except Exception:
655+
pass
656+
657+
# Mirror timer default profile into runtime var.
658+
try:
659+
timer_cfg = _safe_load_yaml(os.path.join(ROOT_DIR, "user", "settings", "timer_settings.yml")) or {}
660+
if isinstance(timer_cfg, dict):
661+
default_profile = str(timer_cfg.get("default_profile") or "").strip()
662+
if default_profile:
663+
Variables.set_var("timer_profile", default_profile)
664+
except Exception:
665+
pass
640666
except Exception:
641667
pass
642668

0 commit comments

Comments
 (0)