For now, this script seems to work ok, although some commands return “Nothing” for both failure and if nothing changed, this script doesn’t deferential and plays sound for both.
-RunPythonScript "CommandFailSound.py"
launch once to activate event listener, launch again to disable it
CommandFailSound.py
import Rhino
import scriptcontext as sc
VERSION = "26.03.31.174234"
_WATCHER_KEY = "rhino.command_fail_sound.watcher"
_APP_EVENT_NAMES = ("CommandEnded", "EndCommand", "CommandFinished")
_COMMAND_EVENT_NAMES = ("EndCommand", "CommandEnded")
_RESULT_ATTRS = ("CommandResult", "Result", "RunResult")
try:
_SUCCESS_RESULT_INT = int(Rhino.Commands.Result.Success)
_CANCEL_RESULT_INT = int(Rhino.Commands.Result.Cancel)
except Exception:
_SUCCESS_RESULT_INT = None
_CANCEL_RESULT_INT = None
def _write(msg):
try:
Rhino.RhinoApp.WriteLine("[CommandFailSound {0}] {1}".format(VERSION, msg))
except Exception:
pass
def _event_candidates():
app = getattr(Rhino, "RhinoApp", None)
if app is not None:
for name in _APP_EVENT_NAMES:
yield app, name
command_type = getattr(getattr(Rhino, "Commands", None), "Command", None)
if command_type is not None:
for name in _COMMAND_EVENT_NAMES:
yield command_type, name
def _normalize_hook(hook):
if isinstance(hook, tuple) and len(hook) == 3:
return hook
if hook:
return (Rhino.RhinoApp, "CommandEnded", hook)
return None
def _detach_handler(source, event_name, handler):
try:
if hasattr(source, event_name):
event_obj = getattr(source, event_name)
event_obj -= handler
except Exception:
pass
def _detach_current_handler_everywhere():
for source, event_name in _event_candidates():
_detach_handler(source, event_name, _on_command_ended)
def _play_sound():
try:
import sys
if sys.platform == "darwin":
try:
import subprocess
subprocess.Popen(
["afplay", "/System/Library/Sounds/Pop.aiff"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return
except Exception:
pass
try:
import subprocess
subprocess.Popen(
["osascript", "-e", "beep"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return
except Exception:
pass
except Exception:
pass
try:
import winsound
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION)
return
except Exception:
pass
try:
import System
System.Media.SystemSounds.Exclamation.Play()
return
except Exception:
pass
try:
import System
System.Console.Beep(1200, 120)
except Exception:
pass
def _event_result(event_args):
for name in _RESULT_ATTRS:
value = getattr(event_args, name, None)
if value is None:
continue
if callable(value):
try:
value = value()
except Exception:
continue
return value
return None
def _is_failure(result):
if result is None:
return False
try:
value = int(result)
if _SUCCESS_RESULT_INT is not None and value == _SUCCESS_RESULT_INT:
return False
if _CANCEL_RESULT_INT is not None and value == _CANCEL_RESULT_INT:
return False
return True
except Exception:
text = str(result).lower()
if "success" in text:
return False
if "cancel" in text:
return False
return ("fail" in text) or ("nothing" in text)
def _on_command_ended(sender, event_args):
try:
result = _event_result(event_args)
if _is_failure(result):
_play_sound()
except Exception:
# Never let event handlers throw into Rhino event loop.
pass
def _install():
if _normalize_hook(sc.sticky.get(_WATCHER_KEY)):
_write("Command-fail sound watcher is already enabled.")
return True
_detach_current_handler_everywhere()
for source, event_name in _event_candidates():
if not hasattr(source, event_name):
continue
try:
event_obj = getattr(source, event_name)
event_obj += _on_command_ended
sc.sticky[_WATCHER_KEY] = (source, event_name, _on_command_ended)
_write(
"Command-fail sound watcher enabled via {0}.{1}.".format(
getattr(source, "__name__", source.__class__.__name__), event_name
)
)
return True
except Exception:
continue
_write("No supported command-end event was found in this Rhino build.")
return False
def _remove():
hook = _normalize_hook(sc.sticky.get(_WATCHER_KEY))
if not hook:
_write("Command-fail sound watcher is not enabled.")
_detach_current_handler_everywhere()
return False
source, event_name, handler = hook
_detach_handler(source, event_name, handler)
_detach_current_handler_everywhere()
sc.sticky.pop(_WATCHER_KEY, None)
_write("Command-fail sound watcher disabled.")
return True
def ToggleCommandFailSound():
if _normalize_hook(sc.sticky.get(_WATCHER_KEY)):
_remove()
else:
_install()
if __name__ == "__main__":
ToggleCommandFailSound()
CommandFailSound.py (5.5 KB)