This commit is contained in:
2026-01-09 01:22:06 -08:00
parent 89ac3bb7e8
commit 1deddfda5c
10 changed files with 1004 additions and 179 deletions

View File

@@ -15,6 +15,121 @@ from SYS import pipeline as ctx
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text"
_MATRIX_MENU_STATE_KEY = "matrix_menu_state"
_MATRIX_SELECTED_SETTING_KEY_KEY = "matrix_selected_setting_key"
def _extract_piped_value(result: Any) -> Optional[str]:
"""Extract the piped value from result (string, number, or dict with 'value' key)."""
if isinstance(result, str):
return result.strip() if result.strip() else None
if isinstance(result, (int, float)):
return str(result)
if isinstance(result, dict):
# Fallback to value field if it's a dict
val = result.get("value")
if val is not None:
return str(val).strip()
return None
def _extract_value_arg(args: Sequence[str]) -> Optional[str]:
"""Extract a fallback value from command-line args (value flag or positional)."""
if not args:
return None
tokens = [str(tok) for tok in args if tok is not None]
value_flags = {"-value", "--value", "-set-value", "--set-value"}
for idx, tok in enumerate(tokens):
low = tok.strip()
if not low:
continue
low_lower = low.lower()
if low_lower in value_flags and idx + 1 < len(tokens):
candidate = str(tokens[idx + 1]).strip()
if candidate:
return candidate
if "=" in low_lower:
head, val = low_lower.split("=", 1)
if head in value_flags and val:
return val.strip()
# Fallback to first non-flag token
for tok in tokens:
text = str(tok).strip()
if text and not text.startswith("-"):
return text
return None
def _extract_set_value_arg(args: Sequence[str]) -> Optional[str]:
"""Extract the value from -set-value flag."""
if not args:
return None
try:
tokens = list(args)
except Exception:
return None
for i, tok in enumerate(tokens):
try:
if str(tok).lower() == "-set-value" and i + 1 < len(tokens):
return str(tokens[i + 1]).strip()
except Exception:
continue
return None
def _update_matrix_config(config: Dict[str, Any], key: str, value: Any) -> bool:
"""Update a Matrix config value and write to config file.
Returns True if successful, False otherwise.
"""
try:
from SYS.config import get_config_path
from configparser import ConfigParser
if not isinstance(config, dict):
return False
# Ensure provider.matrix section exists
providers = config.get("provider", {})
if not isinstance(providers, dict):
providers = {}
config["provider"] = providers
matrix_conf = providers.get("matrix", {})
if not isinstance(matrix_conf, dict):
matrix_conf = {}
providers["matrix"] = matrix_conf
# Update the in-memory config
matrix_conf[key] = value
# Try to write to config file using configparser
try:
config_path = get_config_path()
if not config_path:
return False
parser = ConfigParser()
if Path(config_path).exists():
parser.read(config_path)
section_name = "provider=matrix"
if not parser.has_section(section_name):
parser.add_section(section_name)
parser.set(section_name, key, str(value))
with open(config_path, "w") as f:
parser.write(f)
return True
except Exception as exc:
debug(f"[matrix] Failed to write config file: {exc}")
# Config was updated in memory at least
return True
except Exception as exc:
debug(f"[matrix] Failed to update Matrix config: {exc}")
return False
def _has_flag(args: Sequence[str], flag: str) -> bool:
@@ -466,9 +581,282 @@ def _resolve_upload_path(item: Any, config: Dict[str, Any]) -> Optional[str]:
return None
def _show_main_menu() -> int:
"""Display main menu: Rooms or Settings."""
table = ResultTable("Matrix (select with @N)")
table.set_table("matrix")
table.set_source_command(".matrix", [])
menu_items = [
{
"title": "Rooms",
"description": "List and select rooms for uploads",
"action": "rooms",
},
{
"title": "Settings",
"description": "View and modify Matrix configuration",
"action": "settings",
},
]
for item in menu_items:
row = table.add_row()
row.add_column("Action", item["title"])
row.add_column("Description", item["description"])
ctx.set_last_result_table_overlay(table, menu_items)
ctx.set_current_stage_table(table)
ctx.set_pending_pipeline_tail([[".matrix", "-menu-select"]], ".matrix")
return 0
def _show_settings_table(config: Dict[str, Any]) -> int:
"""Display Matrix configuration settings as a modifiable table."""
table = ResultTable("Matrix Settings (select with @N to modify)")
table.set_table("matrix")
table.set_source_command(".matrix", ["-settings"])
matrix_conf = {}
try:
if isinstance(config, dict):
providers = config.get("provider")
if isinstance(providers, dict):
matrix_conf = providers.get("matrix") or {}
except Exception:
pass
settings_items = []
if isinstance(matrix_conf, dict):
for key in sorted(matrix_conf.keys()):
value = matrix_conf[key]
# Skip sensitive/complex values
if key in ("password",):
value = "***"
settings_items.append({
"key": key,
"value": str(value),
"original_value": value,
})
if not settings_items:
log("No Matrix settings configured. Edit config.conf manually.", file=sys.stderr)
return 0
for item in settings_items:
row = table.add_row()
row.add_column("Key", item["key"])
row.add_column("Value", item["value"])
ctx.set_last_result_table_overlay(table, settings_items)
ctx.set_current_stage_table(table)
ctx.set_pending_pipeline_tail([[".matrix", "-settings-edit"]], ".matrix")
return 0
def _handle_menu_selection(selected: Any, config: Dict[str, Any]) -> int:
"""Handle main menu selection (rooms or settings)."""
items = _normalize_to_list(selected)
if not items:
return 1
item = items[0] # Only consider first selection
action = None
if isinstance(item, dict):
action = item.get("action")
else:
action = getattr(item, "action", None)
if action == "settings":
return _show_settings_table(config)
else:
return _show_rooms_table(config)
def _handle_settings_edit(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Handle settings modification from selected rows.
Two-stage flow:
1. User selects @4 from settings table (@4 stores selection index in context)
2. User pipes value (@4 | "30" → result becomes "30")
3. Handler uses both:
- get_last_selection() → get the row index
- result → the piped value
- Retrieve the setting key from stored items
- Update config
Usage: @4 | "30"
"""
# Get the last selected indices (@4 would give [3])
selection_indices = []
try:
selection_indices = ctx.get_last_selection() or []
except Exception:
pass
# Get the last result items (the settings table items)
last_items = []
try:
last_items = ctx.get_last_result_items() or []
except Exception:
pass
if not selection_indices or not last_items:
log("No setting selected. Use @N to select a setting first.", file=sys.stderr)
return 1
# Get the selected settings item
idx = selection_indices[0]
if idx < 0 or idx >= len(last_items):
log("Invalid selection", file=sys.stderr)
return 1
selected_item = last_items[idx]
key = None
if isinstance(selected_item, dict):
key = selected_item.get("key")
else:
key = getattr(selected_item, "key", None)
if not key:
log("Invalid settings selection", file=sys.stderr)
return 1
# Prevent modifying sensitive settings
if key in ("password", "access_token"):
log(f"Cannot modify sensitive setting: {key}", file=sys.stderr)
return 1
# Extract the piped value or fallback to CLI args
new_value = _extract_piped_value(result) or _extract_value_arg(args)
if new_value is None:
log(f"To modify '{key}', pipe a literal value (e.g. @N | '30') or pass it as an arg: .matrix -settings-edit 30", file=sys.stderr)
return 1
# Update the config with the new value
if _update_matrix_config(config, str(key), new_value):
log(f"✓ Updated {key} = {new_value}")
return 0
else:
log(f"✗ Failed to update {key}", file=sys.stderr)
return 1
def _show_rooms_table(config: Dict[str, Any]) -> int:
"""Display rooms (refactored original behavior)."""
from Provider.matrix import Matrix
try:
provider = Matrix(config)
except Exception as exc:
log(f"Matrix not available: {exc}", file=sys.stderr)
return 1
try:
configured_ids = None
# Use `-all` flag to override room filter
all_rooms_flag = False
if hasattr(ctx, "get_last_args"):
try:
last_args = ctx.get_last_args() or []
all_rooms_flag = any(str(a).lower() == "-all" for a in last_args)
except Exception:
all_rooms_flag = False
if not all_rooms_flag:
ids = [
str(v).strip() for v in _parse_config_room_filter_ids(config)
if str(v).strip()
]
if ids:
configured_ids = ids
rooms = provider.list_rooms(room_ids=configured_ids)
except Exception as exc:
log(f"Failed to list Matrix rooms: {exc}", file=sys.stderr)
return 1
# Diagnostics if a configured filter yields no rows
if not rooms and not all_rooms_flag:
configured_ids_dbg = [
str(v).strip() for v in _parse_config_room_filter_ids(config)
if str(v).strip()
]
if configured_ids_dbg:
try:
joined_ids = provider.list_joined_room_ids()
debug(f"[matrix] Configured room filter IDs: {configured_ids_dbg}")
debug(f"[matrix] Joined room IDs (from Matrix): {joined_ids}")
except Exception:
pass
if not rooms:
if _parse_config_room_filter_ids(config) and not all_rooms_flag:
log(
"No joined rooms matched the configured Matrix room filter (use: .matrix -all)",
file=sys.stderr,
)
else:
log("No joined rooms found.", file=sys.stderr)
return 0
table = ResultTable("Matrix Rooms (select with @N)")
table.set_table("matrix")
table.set_source_command(".matrix", [])
for room in rooms:
row = table.add_row()
name = str(room.get("name") or "").strip() if isinstance(room, dict) else ""
room_id = str(room.get("room_id") or ""
).strip() if isinstance(room,
dict) else ""
row.add_column("Name", name)
row.add_column("Room", room_id)
# Make selection results clearer
room_items: List[Dict[str, Any]] = []
for room in rooms:
if not isinstance(room, dict):
continue
room_id = str(room.get("room_id") or "").strip()
name = str(room.get("name") or "").strip()
room_items.append(
{
**room,
"store": "matrix",
"title": name or room_id or "Matrix Room",
}
)
ctx.set_last_result_table_overlay(table, room_items)
ctx.set_current_stage_table(table)
ctx.set_pending_pipeline_tail([[".matrix", "-send"]], ".matrix")
return 0
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Main Matrix cmdlet execution.
Flow:
1. First call: Show main menu (Rooms or Settings)
2. User selects @1 (rooms) or @2 (settings)
3. -menu-select: Route to appropriate handler
4. -send: Send files to selected room(s) (when uploading)
5. -settings-edit: Handle settings modification
"""
# Handle menu selection routing
if _has_flag(args, "-menu-select"):
return _handle_menu_selection(result, config)
# Handle settings view/edit
if _has_flag(args, "-settings"):
return _show_settings_table(config)
if _has_flag(args, "-settings-edit"):
return _handle_settings_edit(result, args, config)
# Internal stage: send previously selected items to selected rooms.
if any(str(a).lower() == "-send" for a in (args or [])):
if _has_flag(args, "-send"):
# Ensure we don't re-print the rooms picker table on the send stage.
try:
if hasattr(ctx, "set_last_result_table_overlay"):
@@ -596,109 +984,27 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
pass
return 1 if any_failed else 0
# Default stage: show rooms, then wait for @N selection to resume sending.
# Default stage: handle piped vs non-piped behavior
selected_items = _normalize_to_list(result)
if not selected_items:
log(
"Usage: @N | .matrix (select items first, then pick a room)",
file=sys.stderr
)
return 1
ctx.store_value(_MATRIX_PENDING_ITEMS_KEY, selected_items)
try:
ctx.store_value(_MATRIX_PENDING_TEXT_KEY, _extract_text_arg(args))
except Exception:
pass
from Provider.matrix import Matrix
try:
provider = Matrix(config)
except Exception as exc:
log(f"Matrix not available: {exc}", file=sys.stderr)
return 1
try:
configured_ids = None
if not _has_flag(args, "-all"):
ids = [
str(v).strip() for v in _parse_config_room_filter_ids(config)
if str(v).strip()
]
if ids:
configured_ids = ids
rooms = provider.list_rooms(room_ids=configured_ids)
except Exception as exc:
log(f"Failed to list Matrix rooms: {exc}", file=sys.stderr)
return 1
# Diagnostics if a configured filter yields no rows (provider filtered before name lookups for speed).
if not rooms and not _has_flag(args, "-all"):
configured_ids_dbg = [
str(v).strip() for v in _parse_config_room_filter_ids(config)
if str(v).strip()
]
if configured_ids_dbg:
try:
joined_ids = provider.list_joined_room_ids()
debug(f"[matrix] Configured room filter IDs: {configured_ids_dbg}")
debug(f"[matrix] Joined room IDs (from Matrix): {joined_ids}")
except Exception:
pass
if not rooms:
if _parse_config_room_filter_ids(config) and not _has_flag(args, "-all"):
log(
"No joined rooms matched the configured Matrix room filter (use: .matrix -all)",
file=sys.stderr,
)
else:
log("No joined rooms found.", file=sys.stderr)
return 0
table = ResultTable("Matrix Rooms (select with @N)")
table.set_table("matrix")
table.set_source_command(".matrix", [])
for room in rooms:
row = table.add_row()
name = str(room.get("name") or "").strip() if isinstance(room, dict) else ""
room_id = str(room.get("room_id") or ""
).strip() if isinstance(room,
dict) else ""
row.add_column("Name", name)
row.add_column("Room", room_id)
# Make selection results clearer: stash a friendly title/store on the backing items.
# This avoids confusion when the selection handler prints PipeObject debug info.
room_items: List[Dict[str, Any]] = []
for room in rooms:
if not isinstance(room, dict):
continue
room_id = str(room.get("room_id") or "").strip()
name = str(room.get("name") or "").strip()
room_items.append(
{
**room,
"store": "matrix",
"title": name or room_id or "Matrix Room",
}
)
# Overlay table: user selects @N, then we resume with `.matrix -send`.
ctx.set_last_result_table_overlay(table, room_items)
ctx.set_current_stage_table(table)
ctx.set_pending_pipeline_tail([[".matrix", "-send"]], ".matrix")
return 0
# When piped (result has data), show rooms directly.
# When not piped (first command), show main menu.
if selected_items:
return _show_rooms_table(config)
else:
return _show_main_menu()
CMDLET = Cmdlet(
name=".matrix",
alias=["matrix",
"rooms"],
summary="Send selected items to a Matrix room",
summary="Send selected items to a Matrix room or manage settings",
usage="@N | .matrix",
arg=[
CmdletArg(
@@ -707,6 +1013,30 @@ CMDLET = Cmdlet(
description="(internal) Send to selected room(s)",
required=False,
),
CmdletArg(
name="menu-select",
type="bool",
description="(internal) Handle menu selection (rooms/settings)",
required=False,
),
CmdletArg(
name="settings",
type="bool",
description="(internal) Show settings table",
required=False,
),
CmdletArg(
name="settings-edit",
type="bool",
description="(internal) Handle settings modification",
required=False,
),
CmdletArg(
name="set-value",
type="string",
description="New value for selected setting (use with -settings-edit)",
required=False,
),
CmdletArg(
name="all",
type="bool",