Conversation
…et -10) Diff stat: **+84 / -94 = -10 lines.** airc 5359 → 5283. Five new airc_core.config subcommands replace 5 inline-Python heredocs in airc bash: - `set --key K --value V` — generic single-key write - `unset_keys K1 K2 ...` — bulk-clear (used to nuke leftover host_*) - `read_parted` / `record_parted` / `clear_parted` — parted_rooms ops Bash side gains 3 one-line wrappers (`set_config_val`, `unset_config_keys`, plus the existing `_read/_record/_clear_parted_room` slimmed from 13-16 lines each to 1). Sites consolidated: - joiner-mode init (cmd_connect ~line 2225): 12-line heredoc → 4 set_config_val calls - host-mode init (cmd_connect ~line 2491): 14-line heredoc → 3 set_config_val + 1 unset_config_keys - _read_parted_rooms / _record_parted_room / _clear_parted_room: 3 × ~13 lines of inline Python → 1 line each Per Joel #205 + 'shell scripts are like classes': airc_core.config IS the config-state class; bash callers just dispatch to it. The class gains internal `_load`/`_save` helpers so each subcommand is 1-3 lines.
There was a problem hiding this comment.
Pull request overview
This PR refactors config.json mutation logic out of the monolithic airc bash script into lib/airc_core/config.py, adding new airc_core.config subcommands so bash callsites can dispatch to a single “config-state” CLI. It also unifies the parted_rooms persistence helpers (issue #136) and removes multiple inline Python heredocs in favor of those subcommands.
Changes:
- Add new
airc_core.configsubcommands:set,unset_keys,read_parted,record_parted,clear_parted. - Replace inline Python heredocs in
aircwithset_config_val/unset_config_keyscalls and thinparted_roomswrappers that call the new subcommands. - Consolidate parted-room read/record/clear logic into Python for reuse and reduced bash duplication.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
lib/airc_core/config.py |
Introduces internal load/save helpers and adds 5 new CLI subcommands for generic config mutation + parted-room persistence. |
airc |
Switches multiple config.json mutations (including parted-room helpers) from inline Python heredocs to the new airc_core.config subcommands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| set_config_val name "$name" | ||
| set_config_val host "$(get_host)" | ||
| set_config_val created "$(timestamp)" | ||
| # Host mode: clear leftover host_* from any prior joiner run in | ||
| # this scope so we don't mis-read ourselves as a joiner. | ||
| unset_config_keys host_target host_name host_port host_airc_home host_ssh_pub host_identity | ||
|
|
There was a problem hiding this comment.
Similar to the joiner branch, host-mode init now does multiple set_config_val calls plus an unset_config_keys, which becomes multiple separate load/truncate/write operations. To avoid partial updates and reduce IO, consider batching these into a single config update operation (e.g., a set_many/mutate command that can both set and delete keys in one pass).
| set_config_val name "$name" | |
| set_config_val host "$(get_host)" | |
| set_config_val created "$(timestamp)" | |
| # Host mode: clear leftover host_* from any prior joiner run in | |
| # this scope so we don't mis-read ourselves as a joiner. | |
| unset_config_keys host_target host_name host_port host_airc_home host_ssh_pub host_identity | |
| # Batch the host-mode config mutation so we only do one | |
| # load/update/write cycle and avoid leaving partial state behind. | |
| local config_host; config_host="$(get_host)" | |
| local config_created; config_created="$(timestamp)" | |
| CONFIG_NAME="$name" \ | |
| CONFIG_HOST="$config_host" \ | |
| CONFIG_CREATED="$config_created" \ | |
| "$AIRC_PYTHON" - "$CONFIG_JSON" <<'PY' | |
| import json, os, sys, tempfile | |
| path = sys.argv[1] | |
| data = {} | |
| if os.path.exists(path): | |
| with open(path, "r", encoding="utf-8") as f: | |
| raw = f.read().strip() | |
| if raw: | |
| data = json.loads(raw) | |
| data["name"] = os.environ["CONFIG_NAME"] | |
| data["host"] = os.environ["CONFIG_HOST"] | |
| data["created"] = os.environ["CONFIG_CREATED"] | |
| # Host mode: clear leftover host_* from any prior joiner run in | |
| # this scope so we don't mis-read ourselves as a joiner. | |
| for key in ( | |
| "host_target", | |
| "host_name", | |
| "host_port", | |
| "host_airc_home", | |
| "host_ssh_pub", | |
| "host_identity", | |
| ): | |
| data.pop(key, None) | |
| directory = os.path.dirname(path) or "." | |
| fd, tmp = tempfile.mkstemp(prefix=".config.", suffix=".json.tmp", dir=directory) | |
| try: | |
| with os.fdopen(fd, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2, sort_keys=True) | |
| f.write("\n") | |
| os.replace(tmp, path) | |
| except Exception: | |
| try: | |
| os.unlink(tmp) | |
| except OSError: | |
| pass | |
| raise | |
| PY |
| def _load(path): | ||
| try: return json.load(open(path)) | ||
| except (OSError, ValueError): return {} | ||
|
|
||
|
|
||
| def _save(path, c): | ||
| try: json.dump(c, open(path, "w"), indent=2); return 0 | ||
| except OSError as e: | ||
| print(f"airc-config-set-error: {e}", file=sys.stderr); return 1 |
There was a problem hiding this comment.
_load() returns {} on JSON decode errors (ValueError) the same as on missing files. That means set/unset_keys may silently treat a corrupted/truncated config as empty and then _save() overwrites the file, losing existing fields (e.g., identity). Consider distinguishing “file missing” from “invalid JSON”: only allow {} for ENOENT, but for decode errors print a clear airc-config-error: cannot read <path> and return non-zero without writing.
| def cmd_record_parted(args) -> int: | ||
| c = _load(args.config); p = list(c.get("parted_rooms", []) or []) | ||
| if args.room not in p: | ||
| p.append(args.room); c["parted_rooms"] = p; return _save(args.config, c) | ||
| return 0 | ||
|
|
||
|
|
||
| def cmd_clear_parted(args) -> int: | ||
| c = _load(args.config); cur = c.get("parted_rooms", []) or [] | ||
| new = [r for r in cur if r != args.room] | ||
| if new != cur: | ||
| c["parted_rooms"] = new; return _save(args.config, c) | ||
| return 0 |
There was a problem hiding this comment.
cmd_record_parted/cmd_clear_parted currently rely on _load() which collapses JSON parse errors to {}; on a malformed config this will write back a new file containing only parted_rooms, wiping unrelated state. The previous bash implementation intentionally no-op’d on load/parse failure to avoid state loss; these commands should keep that behavior (exit 0 without writing) when the config can’t be decoded.
| set_config_val name "$my_name" | ||
| set_config_val host "$(get_host)" | ||
| set_config_val host_target "$ssh_target" | ||
| set_config_val created "$(timestamp)" |
There was a problem hiding this comment.
This replaces a single read-modify-write of config.json with 4 separate set_config_val calls. Because airc runs with set -euo pipefail, any transient failure mid-sequence can leave config in a partially-updated state, and the extra open/truncate cycles increase the chance of a truncated JSON if interrupted. Consider adding a multi-key airc_core.config subcommand (or extending set to accept repeated key/value pairs) so these fields are written in one operation.
Diff: +84 / -94 = -10 lines. airc 5359 → 5283.
5 new
airc_core.configsubcommands replace 5 inline-Python heredocs:set/unset_keys— generic single + bulk write/deleteread_parted/record_parted/clear_parted— sticky-/part list ops (Persist subscribed rooms — /part should stick across reboots #136)Sites consolidated:
set_config_valcallsset_config_val+ 1unset_config_keys_read/record/clear_parted_roomhelpers: 3 × ~13 lines → 1 line eachPer Joel #205 + 'shell scripts are like classes':
airc_core.configis the config-state class; bash callers dispatch to it. Class gains internal_load/_savehelpers so each subcommand is 1-3 lines (heavily compacted to keep the PR net-negative).