Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 7 additions & 54 deletions src/accessiweather/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,18 @@
import argparse


def parse_args() -> argparse.Namespace:
"""Parse CLI arguments. Mirrors the argument definitions in main.py."""
parser = argparse.ArgumentParser(description="AccessiWeather - Accessible Weather Application")
parser.add_argument(
"--config-dir",
help="Custom configuration directory path",
default=None,
)
parser.add_argument(
"--portable",
action="store_true",
help="Run in portable mode (config stored in app directory)",
)
parser.add_argument(
"--debug",
action="store_true",
help="Enable debug logging",
)
parser.add_argument(
"--fake-version",
help="Fake version for testing updates (e.g., '0.1.0')",
default=None,
)
parser.add_argument(
"--fake-nightly",
help="Fake nightly tag for testing updates (e.g., 'nightly-20250101')",
default=None,
)
parser.add_argument(
"--wizard",
action="store_true",
help="Force the onboarding wizard to run even if it has already been shown",
)
parser.add_argument(
"--updated",
action="store_true",
help="Skip lock-file prompt (set automatically after an update restart)",
)
return parser.parse_args()
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
"""Parse CLI arguments using the shared desktop entrypoint parser."""
from accessiweather.main import parse_args as parse_main_args

return parse_main_args(args)


def main() -> None:
"""Run the AccessiWeather application."""
from accessiweather.main import setup_logging

args = parse_args()
setup_logging(debug=args.debug)

from accessiweather.app import main as app_main
from accessiweather.main import main as run_main

app_main(
config_dir=args.config_dir,
portable_mode=args.portable,
debug=args.debug,
fake_version=args.fake_version,
fake_nightly=args.fake_nightly,
force_wizard=args.wizard,
updated=args.updated,
)
run_main()


if __name__ == "__main__":
Expand Down
26 changes: 23 additions & 3 deletions src/accessiweather/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import logging
import sys

from .notification_activation import extract_activation_request_from_argv


def setup_logging(debug: bool = False) -> None:
"""Set up logging configuration."""
Expand All @@ -17,8 +19,8 @@ def setup_logging(debug: bool = False) -> None:
)


def main() -> None:
"""Run the AccessiWeather application."""
def _build_parser() -> argparse.ArgumentParser:
"""Build the shared parser for desktop entrypoints."""
parser = argparse.ArgumentParser(description="AccessiWeather - Accessible Weather Application")
parser.add_argument(
"--config-dir",
Expand Down Expand Up @@ -55,7 +57,24 @@ def main() -> None:
action="store_true",
help="Skip lock-file prompt (set automatically after an update restart)",
)
args = parser.parse_args()
return parser


def parse_args(args: list[str] | None = None) -> argparse.Namespace:
"""Parse desktop entrypoint arguments, allowing Windows toast activation tokens."""
parser = _build_parser()
parsed_args, extras = parser.parse_known_args(args)
token_argv = [sys.argv[0], *extras] if args is None else extras
parsed_args.activation_request = extract_activation_request_from_argv(token_argv)
unknown = [arg for arg in extras if extract_activation_request_from_argv([arg]) is None]
if unknown:
parser.error(f"unrecognized arguments: {' '.join(unknown)}")
return parsed_args


def main() -> None:
"""Run the AccessiWeather application."""
args = parse_args()

setup_logging(debug=args.debug)

Expand All @@ -69,6 +88,7 @@ def main() -> None:
fake_nightly=args.fake_nightly,
force_wizard=args.wizard,
updated=args.updated,
activation_request=args.activation_request,
)


Expand Down
41 changes: 39 additions & 2 deletions src/accessiweather/notifications/toast_notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
import logging
import sys
import threading
import xml.etree.ElementTree as ET
from collections.abc import Callable
from typing import Any
from xml.sax.saxutils import escape as _xml_escape

from ..constants import WINDOWS_APP_USER_MODEL_ID
from ..sound_events import DEFAULT_MUTED_SOUND_EVENTS
from ..windows_toast_identity import WINDOWS_TOAST_PROTOCOL_SCHEME
from .sound_player import (
normalize_muted_sound_events,
play_notification_sound,
Expand Down Expand Up @@ -113,6 +115,33 @@ def __init__(self, arguments: str) -> None:
self.is_dismissed = False


def _uses_protocol_activation(activation_arguments: str | None) -> bool:
"""Return True when the activation payload is a registered URI scheme."""
return bool(
activation_arguments
and activation_arguments.startswith(f"{WINDOWS_TOAST_PROTOCOL_SCHEME}:")
)


def _apply_protocol_activation_to_xml(xml_payload: str, activation_arguments: str | None) -> str:
"""Force protocol activation on the toast root element for stub-CLSID mode."""
if not _uses_protocol_activation(activation_arguments):
return xml_payload

try:
root = ET.fromstring(xml_payload)
except ET.ParseError:
logger.debug("[toasted] Failed to parse toast XML for protocol activation", exc_info=True)
return xml_payload

if root.tag != "toast":
return xml_payload

root.set("activationType", "protocol")
root.set("launch", activation_arguments or "")
return ET.tostring(root, encoding="unicode")


# ---------------------------------------------------------------------------
# ToastedWindowsNotifier — Windows backend using the 'toasted' package
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -264,7 +293,10 @@ def _set_activation_arguments(self, toast, activation_arguments: str | None) ->
for attr in ("arguments", "launch"):
with contextlib.suppress(Exception):
setattr(toast, attr, activation_arguments)
return
if _uses_protocol_activation(activation_arguments):
for attr in ("activation_type", "activationType"):
with contextlib.suppress(Exception):
setattr(toast, attr, "protocol")

def _send_in_worker(
self,
Expand Down Expand Up @@ -350,13 +382,18 @@ def _show_toast_direct(self, toast, activation_arguments: str | None) -> bool:
try:
# Generate XML from the toasted Toast object
xml_string = toast.to_xml_string()
xml_string = _apply_protocol_activation_to_xml(xml_string, activation_arguments)

xml_doc = _WinRT_XmlDocument()
xml_doc.load_xml(xml_string)
notification = _WinRT_ToastNotification(xml_doc)

# Register persistent activated handler
if activation_arguments and self.on_activation is not None:
if (
activation_arguments
and self.on_activation is not None
and not _uses_protocol_activation(activation_arguments)
):
on_activation = self.on_activation

def _on_activated(sender, args):
Expand Down
Loading
Loading