Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: forms.toast() does not support Unicode characters. (Enhancement) #2484

Open
5 tasks done
Denver-22 opened this issue Dec 19, 2024 · 4 comments
Open
5 tasks done
Assignees
Labels
Bug Bug that stops user from using the tool or a major portion of pyRevit functionality [class] DotNet API Issues related to pyRevitLabs libraries [subsystem]

Comments

@Denver-22
Copy link

✈ Pre-Flight checks

  • I don't have SentinelOne antivirus installed (see above for the solution)
  • I have searched in the issues (open and closed) but couldn't find a similar issue
  • I have searched in the pyRevit Forum for similar issues
  • I already followed the installation troubleshooting guide thoroughly
  • I am using the latest pyRevit Version

🐞 Describe the bug

All text fields of function forms.toast() do not support Unicode characters.
Please add support.
toast

⌨ Error/Debug Message

No errors.
Just scribbles.

♻️ To Reproduce

No response

⏲️ Expected behavior

No response

🖥️ Hardware and Software Setup (please complete the following information)

Win10.
Revit 2023.1.6
5.0.0.24325+1012-wip:2712:2023.1.60

Additional context

No response

@Denver-22 Denver-22 added the Bug Bug that stops user from using the tool or a major portion of pyRevit functionality [class] label Dec 19, 2024
@sanzoghenzo
Copy link
Contributor

this is a bug of the go-toast app that pyrevit uses to show the toasts.
Unfortunately the project seems abandoned, and the fork that solved the issue doesn't have a compiled executable because it was turn into a go library (no more cli that we need).

The core of the project is a single go module that calls powershell, so it could be turn into a python script more or less easily.

We're open to contributions!

@sanzoghenzo
Copy link
Contributor

A few GPT-4o prompts later

import subprocess
import time
from xml.etree.ElementTree import Element, SubElement, tostring

# Global script template
SCRIPT_TEMPLATE = """
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null

$APP_ID = "{app_id}"

$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml([xml]@"
{xml_content}
@")

$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
"""

# Mapping for audio friendly names to ms-winsoundevent strings
AUDIO_MAPPING = {
    "default": "ms-winsoundevent:Notification.Default",
    "im": "ms-winsoundevent:Notification.IM",
    "mail": "ms-winsoundevent:Notification.Mail",
    "reminder": "ms-winsoundevent:Notification.Reminder",
    "sms": "ms-winsoundevent:Notification.SMS",
    "silent": "silent",
    "looping_alarm": "ms-winsoundevent:Notification.Looping.Alarm",
    "looping_call": "ms-winsoundevent:Notification.Looping.Call"
}

def build_toast_xml(title, message, icon=None, activation_type="protocol", 
                     activation_arguments=None, actions=None, audio="default", 
                     loop=False, duration="short"):
    if duration not in {"short", "long"}:
        raise ValueError("Invalid duration: must be 'short' or 'long'")
    audio = AUDIO_MAPPING.get(audio.lower(), AUDIO_MAPPING["default"])
    toast = Element("toast", {
        "activationType": activation_type,
        "launch": activation_arguments or "",
        "duration": duration
    })
    visual = SubElement(toast, "visual")
    binding = SubElement(visual, "binding", {"template": "ToastGeneric"})
    if icon:
        SubElement(binding, "image", {"placement": "appLogoOverride", "src": icon})
    if title:
        SubElement(binding, "text").text = "<![CDATA[{}]]>".format(title)
    if message:
        SubElement(binding, "text").text = "<![CDATA[{}]]>".format(message)
    if audio == "silent":
        SubElement(toast, "audio", {"silent": "true"})
    else:
        audio_element = SubElement(
          toast, "audio", {"src": audio, "loop": "true" if loop else "false"}
        )
    if actions:
        actions_element = SubElement(toast, "actions")
        for action in actions:
            SubElement(actions_element, "action", {
                "activationType": action.get("type", ""),
                "content": action.get("label", ""),
                "arguments": action.get("arguments", "")
            })
    return tostring(toast, encoding="unicode")

def send_notification(app_id, title, message, icon=None, activation_type="protocol",
                      activation_arguments=None, actions=None, audio="default",
                      loop=False, duration="short"):
    app_id = app_id or "Windows App"

    # Generate the XML content
    xml_content = build_toast_xml(title, message, icon, activation_type, 
                                  activation_arguments, actions, audio, loop, duration)

    # Format PowerShell script using the global template
    script = SCRIPT_TEMPLATE.format(app_id=app_id, xml_content=xml_content)

    try:
        subprocess.Popen(["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        print("Error sending notification: {0}".format(str(e)))
    except subprocess.TimeoutExpired:
        print("Notification script timed out.")

# Example Usage
if __name__ == "__main__":
    send_notification(
        app_id="Example App",
        title="My notification",
        message="Some message about how important something is...",
        icon="path/to/icon.png",
        actions=[
            {"type": "protocol", "label": "Open Maps", "arguments": "bingmaps:?q=sushi"},
            {"type": "protocol", "label": "Another Action", "arguments": ""}
        ],
        audio="mail",
        loop=False,
        duration="short"
    )

This is completely untested, feel free to try it, fix it and return back with a better version 😉

I've hit the limit for the free gpt-4o prompts, I asked how to further improve the code, here's the answer:

  1. Error Handling Improvements:

    • Add specific error messages for cases where actions contains invalid data (e.g., missing type or label).
    • Ensure icon is a valid path (if provided) before including it in the XML.
  2. Validation:

    • Validate audio against AUDIO_MAPPING.keys() and raise an exception if an invalid key is passed.
    • Check if activation_arguments are meaningful URLs or paths based on activation_type.
  3. Logging:

    • Add logging for debugging purposes to capture errors and script execution status.
  4. Script Output Handling:

    • Capture and log PowerShell script output (stdout and stderr) for better debugging.
  5. Default Parameters:

    • Move app_id defaulting to "Windows App" directly inside send_notification instead of the call site.
  6. Script Timeout:

    • Add a timeout option to send_notification and pass it to subprocess.Popen.
  7. Cross-Platform Considerations:

    • Add checks or warnings if the script is executed on a non-Windows system, as PowerShell is Windows-specific.
  8. XML Escaping:

    • Ensure proper escaping of special characters in title and message for XML safety.
  9. Test Cases:

    • Create unit tests for build_toast_xml to verify it generates valid XML for a variety of inputs.
  10. Documentation:

    • Add detailed docstrings for functions explaining parameters, types, and valid values (e.g., list valid audio keys, activation_type options).

@sanzoghenzo sanzoghenzo added the Good First Issue Bug or feature with trivial solution that awaits you to be fixed! label Dec 27, 2024
@sanzoghenzo sanzoghenzo added this to the pyRevit 5 RC milestone Dec 27, 2024
@sanzoghenzo sanzoghenzo self-assigned this Dec 27, 2024
@sanzoghenzo sanzoghenzo moved this to In Progress in pyRevit Dec 27, 2024
sanzoghenzo pushed a commit to sanzoghenzo/pyRevit that referenced this issue Dec 27, 2024
@sanzoghenzo sanzoghenzo removed this from the pyRevit 5 RC milestone Jan 6, 2025
@sanzoghenzo sanzoghenzo removed this from pyRevit Jan 6, 2025
@sanzoghenzo sanzoghenzo added DotNet API Issues related to pyRevitLabs libraries [subsystem] and removed Good First Issue Bug or feature with trivial solution that awaits you to be fixed! labels Jan 8, 2025
@sanzoghenzo
Copy link
Contributor

As I mentioned in #2493 this is no easy fix on the python side because subprocess doesn't handle unicode properly.

We should implement this on the .net/C# side, but I have no incentives to work on that.

@Denver-22
Copy link
Author

@sanzoghenzo, thank you for the detailed answers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Bug that stops user from using the tool or a major portion of pyRevit functionality [class] DotNet API Issues related to pyRevitLabs libraries [subsystem]
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants