3 On-Screen Display Python Client Library
4 (c) 2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
11 osd.notify("first line", "second line")
13 Each argument is either a single string (a notification line)
14 or a pair of strings ("property", "value"). For available
15 properties consult OSD Daemon documentation. These arguments
16 are position sensitive.
18 Properties may also be added as named parameters. For example:
19 osd.notify("Hello world", color="red")
21 Due to the notation of OSD parameters and Python limitations of named
22 arguments, all occurrences of '_' in the argument name are be replaced by '-'.
24 Named arguments are treated as positional arguments placed before all others,
25 in unspecified relative order. This behavior was chosen as in most cases, these
26 arguments will affect all notification lines. If you need more control, use the
29 Therefore, these two invocations are equivalent:
30 osd.notify("a line", outline_color="red")
31 osd.notify(("outline-color", "red"), "a line")
36 If you don't want to send the notification to the default display,
37 you can create new connection class via `osd.new_connection(DISPLAY_NAME)`
38 or using `osd.Display` constructor which takes an instance of `Xlib.display.Display`.
40 The instance of `Connection` class has the same `notify` method.
42 You can reassign a default connection into `osd.default_connection`.
47 If there is a problem with connection to the X server, library raises the corresponding
50 If a message contains any forbidden characters such as new line,
51 OSDArgumentError will be raised. The `notify_sanitized` method
52 filters out forbidden characters, therefore it never raises OSDArgumentError.
56 from Xlib.display import Display
58 from Xlib.xobject.drawable import Window
60 from typing import Callable, List, Tuple, Optional, Union
63 class OSDArgumentError(RuntimeError):
68 """ See module documentation. """
73 def __init__(self, display: Display):
74 self.display = display
75 self.root_window = self.display.screen().root
76 self.osd_queue_atom = self.display.get_atom('OSD_QUEUE')
78 def _send_raw(self, val: str) -> None:
79 self.root_window.change_property(
81 property_type=Xlib.Xatom.STRING,
84 mode=Xlib.X.PropModeAppend
88 def notify_with_error_handler(self, error_handler: Callable[[str, str, bool], str], *args: Union[str, Tuple[str, str]], **kwargs: str) -> None:
91 def add_line(key, val):
92 def remove_char(s, ch, is_key):
96 out.append(error_handler(s, ch, is_key))
101 key = remove_char(remove_char(str(key), "\n", True), ":", True)
102 val = remove_char(str(val), "\n", False)
104 lines.append(f"{key}:{val}")
106 for key, val in kwargs.items():
107 add_line(key.replace("_", "-"), val)
109 if isinstance(x, tuple):
114 msg = "\n".join(lines) + "\n\n"
117 def notify(self, *args: Union[str, Tuple[str, str]], **kwargs: str) -> None:
118 def error_handler(s, ch, is_key):
119 raise OSDArgumentError(f"{'Key' if is_key else 'Value'} {repr(s)} contain forbidden character {repr(ch)}.")
120 self.notify_with_error_handler(error_handler, *args, **kwargs)
122 def notify_sanitized(self, *args: Union[str, Tuple[str, str]], **kwargs: str) -> None:
123 def error_handler(s, ch, is_key):
125 self.notify_with_error_handler(error_handler, *args, **kwargs)
128 def new_connection(display_name: Optional[str] = None) -> Connection:
129 """ See module documentation. """
130 return Connection(Display(display_name))
133 default_connection: Optional[Connection] = None
136 def _default_or_new_connection() -> Connection:
137 global default_connection
138 if default_connection is None:
139 default_connection = new_connection()
140 return default_connection
143 def notify(*args: Union[str, Tuple[str, str]], **kwargs: str) -> None:
144 """ See module documentation. """
145 _default_or_new_connection().notify(*args, **kwargs)
148 def notify_sanitized(*args: Union[str, Tuple[str, str]], **kwargs: str) -> None:
149 """ See module documentation. """
150 _default_or_new_connection().notify_sanitized(*args, **kwargs)