Skip to content

Architecture

The UI is platform-agnostic; every OS-specific behaviour lives behind a small backend interface chosen at runtime. Adding a platform means writing one class.


System overview

graph TD
    subgraph UI["๐Ÿ–ฅ๏ธ UI Layer (platform-agnostic)"]
        TK["<b>Tkinter Window</b><br/>Status card ยท Interval<br/>Options: lid-close, autostart<br/>Scheduled power action"]
        TRAY["<b>SystemTray</b><br/>pystray (Win) /<br/>Dock fallback (macOS)"]
        WARN["<b>Warning dialog</b><br/>30s cancelable"]
    end

    subgraph Core["โš™๏ธ Application Core"]
        APP["<b>DontLockPC</b><br/>Orchestrator + event handling"]
        THREAD["<b>Keep-Alive Thread</b><br/>Daemon nudge loop"]
        TIMER["<b>Power timer</b><br/>root.after countdown"]
    end

    subgraph Backend["๐Ÿ”Œ Backend Abstraction"]
        BASE["<b>KeepAwakeBackend</b><br/>prevent/allow_sleep ยท nudge<br/>prevent/restore_lid_sleep ยท power_action"]
        WIN["<b>WindowsBackend</b><br/>SetThreadExecutionState ยท SendInput F15<br/>powercfg lid ยท SetSuspendState / shutdown"]
        MAC["<b>MacOSBackend</b><br/>caffeinate -dimsu ยท Quartz F15<br/>pmset sleepnow / osascript shutdown"]
    end

    subgraph OS["๐Ÿ”ง System integration"]
        AUTO["<b>autostart</b><br/>Run key (Win) / LaunchAgent (macOS)"]
    end

    TK <--> APP
    TRAY <--> APP
    APP --> WARN
    APP -->|"START"| THREAD
    APP -->|"START (if armed)"| TIMER
    APP --> AUTO
    THREAD --> BASE
    TIMER -->|"deadline โ†’ warn โ†’ act"| BASE
    BASE -.->|"sys.platform == win32"| WIN
    BASE -.->|"sys.platform == darwin"| MAC

Keep-alive flow

flowchart TD
    A(["โ–ถ START"]) --> B["Validate interval (default 30s)"]
    B --> C["Update UI ยท start pulse"]
    C --> D["Spawn daemon thread"]
    D --> E["backend.prevent_sleep()"]
    E --> F{"running?"}
    F -- "No" --> K["backend.allow_sleep()"] --> L(["๐Ÿ›‘ Thread exits"])
    F -- "Yes" --> G["backend.nudge()<br/>mouse ยฑ1px + F15"]
    G --> H["Increment counter ยท timestamp ยท update UI"]
    H --> I["sleep(interval)"]
    I --> F

    style A fill:#a6e3a1,color:#11111b
    style L fill:#f38ba8,color:#11111b
    style E fill:#89b4fa,color:#11111b
    style G fill:#89b4fa,color:#11111b

The loop runs on a daemon thread so it can never keep the process alive after the window closes. All UI updates are marshalled back to the Tk main thread with root.after(0, ...).


Scheduled power action

flowchart TD
    S(["โ–ถ START (action armed)"]) --> P["Parse timer field<br/>N minutes or HH:MM"]
    P --> Q{"deadline reached?<br/>(checked while running)"}
    Q -- "No" --> Q
    Q -- "Yes" --> X["Show 30s cancelable warning"]
    X -- "Cancel / STOP" --> Y(["Aborted ยท keep-alive keeps running"])
    X -- "Countdown ends" --> W["Release keep-alive<br/>allow_sleep() + restore lid"]
    W --> Z["backend.power_action()<br/>Sleep ยท Hibernate ยท Shutdown"]

    style S fill:#a6e3a1,color:#11111b
    style Y fill:#a6e3a1,color:#11111b
    style Z fill:#f38ba8,color:#11111b
    style W fill:#89b4fa,color:#11111b

The countdown uses Tk's after() on the main thread โ€” no extra thread, and it stops cleanly whenever running becomes false.


The backend contract

KeepAwakeBackend is an ABC with three required methods and three optional capability hooks:

Member Kind Purpose
prevent_sleep() required Ask the OS to keep system + display awake
allow_sleep() required Restore default power/idle behaviour
nudge() required Emit a tiny, invisible input event
prevent_lid_sleep() optional Override lid-close action; returns bool
restore_lid_sleep() optional Put the lid-close action back
power_action(action) optional Sleep / Hibernate / Shut down
close() provided Calls restore_lid_sleep() + allow_sleep()

Capabilities are advertised as class attributes so the UI can adapt without platform checks:

lid_close_supported: bool = False
power_actions: tuple[str, ...] = ()

The UI only renders the lid-close checkbox when lid_close_supported is true, and only builds the power-action menu from power_actions โ€” which is why macOS never shows Hibernate.

get_backend() reads sys.platform at call time, which keeps it trivially monkeypatchable in tests.


Project structure

do-not-lock-my-system/
โ”œโ”€โ”€ src/dontlockpc/
โ”‚   โ”œโ”€โ”€ __init__.py           # package metadata / version
โ”‚   โ”œโ”€โ”€ __main__.py           # `python -m dontlockpc`
โ”‚   โ”œโ”€โ”€ app.py                # Tkinter UI + keep-alive orchestrator
โ”‚   โ”œโ”€โ”€ autostart.py          # cross-platform "run at login" management
โ”‚   โ”œโ”€โ”€ tray.py               # system-tray wrapper (graceful degradation)
โ”‚   โ””โ”€โ”€ backends/
โ”‚       โ”œโ”€โ”€ __init__.py       # get_backend() platform factory
โ”‚       โ”œโ”€โ”€ base.py           # KeepAwakeBackend abstract interface
โ”‚       โ”œโ”€โ”€ windows.py        # Win32 ctypes implementation
โ”‚       โ””โ”€โ”€ macos.py          # caffeinate + Quartz implementation
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ test_backends.py      # backend factory + contract + capabilities
โ”‚   โ”œโ”€โ”€ test_power.py         # power-timer deadline parser
โ”‚   โ””โ”€โ”€ test_power_actions.py # cross-platform power actions + dialog
โ”œโ”€โ”€ docs/                     # this documentation site (MkDocs)
โ”œโ”€โ”€ .github/workflows/        # CI, release, PyPI publish, docs
โ”œโ”€โ”€ dontlockpc.spec           # PyInstaller build spec
โ””โ”€โ”€ pyproject.toml            # packaging + tooling config

Design principles

  • No platform checks in the UI โ€” capabilities are advertised by the backend.
  • Always restore what you change โ€” lid-close settings and execution state are put back on STOP, exit, and before any power action.
  • Fail soft โ€” an unavailable tray, autostart, or optional capability degrades gracefully instead of crashing.
  • Nothing leaves the machine โ€” no network calls, no telemetry.

Next: Python API