sevaht_gui.tkapp

A tkinter application base that owns the main thread (Option A).

tkinter is not thread-safe: every widget call must happen on the one thread that created the root, and on macOS/Windows that loop is happiest on the main thread. So TkApp runs root.mainloop() on the thread that constructed it (the main thread) and runs a cross-platform tray icon – which brings its own event loop – on a worker thread instead.

Work originating off the UI thread (e.g. a tray menu callback, fired on the tray worker thread) must be marshalled back with run_on_ui_thread() / call_on_ui_thread(). A periodic poll drains that queue and, as a side effect, gives the interpreter a chance to run signal handlers while the C-level mainloop is blocked.

class sevaht_gui.tkapp.WindowPlacement(position: tuple[int, int] | None = None)[source]

Bases: object

Remembers a toplevel’s on-screen position across hide/recreate cycles.

Call remember() before hiding or destroying a window and apply() after showing or re-creating it, so it reopens where it was left. position is plain (x, y) data you can store in config and restore via the constructor; nothing is persisted automatically.

TkApp uses one of these for its main window; create your own for any additional toplevels you want to behave the same way.

remember(window: Tk | Toplevel) None[source]

Capture window’s position if it is currently on screen.

apply(window: Tk | Toplevel) bool[source]

Move window to the remembered position; return whether one was set.

class sevaht_gui.tkapp.TkApp(*, theme: str | None = 'clam', checkmark_indicator: bool = True, quit_confirm: str | None = 'Are you sure you want to quit?', quit_confirm_title: str = 'Quit', center_on_show: bool = True, window_position: tuple[int, int] | None = None, poll_interval_ms: int = 25)[source]

Bases: object

Owns a tkinter.Tk root and runs its loop on the main thread.

Construct this on the main thread, build your widgets on self.root, then call run() (optionally passing a tray icon to run alongside it).

Sensible defaults (each overridable via the constructor / method args):

  • a ttk theme with proper checkbox indicators;

  • the window is centered on the primary monitor the first time it is shown (center_on_show), then re-opens where it was last positioned;

  • quit() asks for confirmation (quit_confirm);

  • create_tray_icon() also sets the window’s title-bar icon to match the tray icon (window_icon);

  • once a tray icon exists, the window-close button hides the window (the tray houses it); with no tray it quits instead;

  • notify() shows a desktop notification via the tray when present and a standalone notifier otherwise (so it works with or without a tray).

To persist the window position across runs, read window_position when saving config and pass it back as window_position next launch; the library never writes it to disk itself.

property has_tray: bool

True if a tray icon was successfully created via this app.

property window_position: tuple[int, int] | None

The window’s (x, y) – current if on screen, else last known.

Save this in your config and pass it back as window_position to re-open the window where it was left between runs.

run_on_ui_thread(callback: Callable[[], object]) None[source]

Schedule callback to run on the UI thread (fire and forget).

call_on_ui_thread(callback: Callable[[], object], *, timeout: float = 5.0) object[source]

Run callback on the UI thread and return its result.

Returns None if the app is shutting down or the call times out.

set_window_icon(icon: IconSource) None[source]

Set (or replace) the window/title-bar icon from any IconSource.

Accepts a prepared PIL image, an image file path, or a renderer; call it again to change the icon (e.g. to reflect state).

set_tray_icon(icon: IconSource) None[source]

Set (or replace) the tray icon’s image, if a tray icon exists.

set_app_icon(icon: IconSource) None[source]

Set both the window icon and the tray icon (if any) at once.

A convenience for keeping them in sync. To control them independently – or use different images – call set_window_icon() and set_tray_icon() separately instead.

track_window_icon(window: Tk | Toplevel) None[source]

Keep window’s title-bar icon in sync with set_window_icon().

Applies the current icon immediately and updates it whenever the icon changes, until window is destroyed. Useful for dialogs/toplevels that should mirror the main window’s (possibly state-dependent) icon.

create_tray_icon(name: str, title: str, icon: IconSource, *, on_activate: Callable[[], None] | None = None, on_quit: Callable[[], None] | None = None, activate_label: str = 'Open', quit_label: str = 'Quit', window_icon: bool = True) TrayIcon | None[source]

Create a tray icon bound to this app, or None if no tray exists.

When no system tray is available the app keeps working without one (the window close button then quits instead of hiding). on_activate defaults to show() and on_quit to quit(). window_icon (default) also sets the window’s title-bar icon to icon so the two match; pass False to manage the window icon separately. Pass the result to run().

notify(title: str, message: str, *, icon: str | None = None) None[source]

Show a desktop notification.

Uses the tray icon’s native notification when a tray exists; otherwise falls back to a standalone notifier (notify-send/dbus-send/console) so notifications still work without a tray – callers need not handle that case themselves. icon is a freedesktop icon name where supported.

quit() None[source]

Request quit – confirm if configured, then stop. Thread-safe.

confirm(message: str, *, title: str = 'Confirm') bool[source]

Show a modal Yes/No dialog; return True if confirmed. Thread-safe.

primary_monitor_bounds() tuple[int, int, int, int][source]

Return the primary monitor’s (x, y, width, height).

Queries X11 RandR via python-xlib (works under X11 and XWayland) and falls back to the full Tk screen size off-X11.

center_window(window: Tk | Toplevel) None[source]

Center window on the main window (if visible) else the primary monitor. Works for the root or any toplevel (e.g. your own dialogs).

The window is positioned while withdrawn and its prior shown/hidden state is restored, so it never flashes at the window manager’s default location first – update_idletasks would otherwise map a visible (or just-created) window before the geometry is applied. This makes the flicker impossible to reintroduce at the call site.

run(tray_icon: TrayIcon | None = None, *, tray_setup: Callable[[], None] | None = None, start_hidden: bool = False, handle_signals: bool = True) None[source]

Run the UI loop on the current (main) thread until stop().

If tray_icon is given, its event loop runs on a worker thread for the lifetime of the app and is stopped when the UI loop exits.

tray_setup is passed through to tray_icon.run(setup=...) and is therefore invoked on the tray’s own thread, once the tray’s event loop is running. Use it for tray work that cannot happen before the loop starts – e.g. setting the initial icon image on Windows, where the HICON cannot be updated until pystray’s loop is live. UI work triggered from here must still be marshalled with run_on_ui_thread().

If start_hidden is true the window starts withdrawn (only the tray icon is visible); call show() to reveal it. It is ignored when there is no tray_icon, since then nothing could restore the window.

show() None[source]

Show and raise the main window. Safe to call from any thread.

hide() None[source]

Hide (withdraw) the main window. Safe to call from any thread.

stop() None[source]

Stop the UI loop. Safe to call from any thread.