dulus-bar 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Rojo (Dulus)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: dulus-bar
3
+ Version: 0.3.0
4
+ Summary: Dulus Bar — a cross-platform Dynamic Island for your AI agents (Windows / macOS / Linux)
5
+ Author-email: Dulus <dev@dulus.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Dulus-Ai/Dulus-bar
8
+ Project-URL: Repository, https://github.com/Dulus-Ai/Dulus-bar
9
+ Project-URL: Issues, https://github.com/Dulus-Ai/Dulus-bar/issues
10
+ Keywords: ai,agents,overlay,dynamic-island,notch,claude,codex,gemini,cursor,dulus,windows,macos,linux
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PyQt6>=6.4.0
23
+ Requires-Dist: websockets>=12.0
24
+ Requires-Dist: psutil>=5.9.0
25
+ Requires-Dist: pywin32>=306; sys_platform == "win32"
26
+ Requires-Dist: pyobjc-framework-Cocoa>=9.0; sys_platform == "darwin"
27
+ Requires-Dist: pyobjc-framework-Quartz>=9.0; sys_platform == "darwin"
28
+ Provides-Extra: build
29
+ Requires-Dist: pyinstaller>=6.0; extra == "build"
30
+ Dynamic: license-file
31
+
32
+ <h1 align="center">Dulus Bar 🦅</h1>
33
+
34
+ <p align="center">
35
+ <strong>A cross-platform Dynamic Island for your AI agents.</strong><br>
36
+ Free. Native. MIT. Works with <em>any</em> agent — no account, no cloud, no lock-in.
37
+ </p>
38
+
39
+ <p align="center">
40
+ <code>pip install dulus-bar</code>
41
+ </p>
42
+
43
+ <p align="center">
44
+ <img alt="MIT" src="https://img.shields.io/badge/license-MIT-green">
45
+ <img alt="Python" src="https://img.shields.io/badge/python-3.10%2B-blue">
46
+ <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%C2%B7%20macOS%20%C2%B7%20Linux-lightgrey">
47
+ </p>
48
+
49
+ ---
50
+
51
+ Dulus Bar puts a floating status island at the top of your screen that shows every
52
+ AI coding agent you're running — Claude Code, Codex, Gemini, Cursor, Dulus, and
53
+ any other CLI — and lets you **Allow / Deny** their permission prompts without
54
+ diving back into a terminal.
55
+
56
+ It's the idea itself, shipped for everyone. **Not tied to Dulus** — wrap any agent
57
+ and it shows up. We built the island we wanted; here it is for the whole world. 🌎
58
+
59
+ - **All platforms** — Windows, macOS, and Linux.
60
+ - **macOS notch-native** — on MacBooks with a camera notch the island hugs the
61
+ notch and stays visible across every Space and full-screen app.
62
+ - **Reveal on hover** — everywhere else it rests as a slim tab at the top edge and
63
+ slides open when your mouse approaches, or when an agent needs you.
64
+ - **Any agent** — anything with a CLI works through a thin wrapper; well-known
65
+ agents get their own icon and accent color.
66
+ - **100% local** — no cloud, no account, no telemetry. One WebSocket on
67
+ `127.0.0.1:17372`.
68
+
69
+ ---
70
+
71
+ ## Install
72
+
73
+ Requires Python 3.10+.
74
+
75
+ ```bash
76
+ pip install dulus-bar
77
+ dulusbar
78
+ ```
79
+
80
+ That's it — the island appears at the top of your screen. Move your mouse to the
81
+ top-center to reveal it; right-click the tray icon for the menu.
82
+
83
+ <details>
84
+ <summary><strong>From source (for the wired-agent extras)</strong></summary>
85
+
86
+ ```bash
87
+ git clone https://github.com/Dulus-Ai/Dulus-bar.git
88
+ cd Dulus-bar
89
+
90
+ # macOS / Linux
91
+ ./setup.sh
92
+
93
+ # Windows
94
+ .\setup.ps1
95
+ ```
96
+
97
+ > Linux tip: for click-to-focus of a terminal on X11, install `wmctrl` or `xdotool`.
98
+ >
99
+ > macOS: `setup.sh` also builds the native SwiftUI/AppKit notch surface using the
100
+ > Swift toolchain in Xcode Command Line Tools. The Python process stays the local
101
+ > WebSocket hub; Windows and Linux use the PyQt overlay.
102
+
103
+ </details>
104
+
105
+ ---
106
+
107
+ ## Wire up any AI agent — zero monopoly
108
+
109
+ Dulus Bar is **agent-agnostic**. Wrap any CLI and it reports to the island:
110
+
111
+ ```bash
112
+ python wrappers/agent_wrapper.py "Codex" codex fix auth bug
113
+ python wrappers/agent_wrapper.py "Gemini" gemini
114
+ python wrappers/claude_wrapper.py # Claude Code helper
115
+ ```
116
+
117
+ `agent_wrapper.py "Display Name" <command> [args...]` reports that agent to the
118
+ bar. Known names (Claude, Codex, Gemini, Cursor, Copilot, Kimi, DeepSeek, Grok,
119
+ Qwen, Mistral, Windsurf, Zed, Aider, Ollama, …) get a curated icon + color;
120
+ anything else gets a stable auto-assigned color.
121
+
122
+ Or **right-click the island → Open agent…** and pick any executable from a file
123
+ dialog — Dulus Bar launches it in its own terminal, already wired.
124
+
125
+ ---
126
+
127
+ ## Works beautifully with Dulus (optional)
128
+
129
+ If you run [Dulus](https://dulus.ai), the island also shows the active **model**
130
+ and live **context usage** — e.g. `kimi/kimi-k2.5 · ctx 38%`. But you never need
131
+ Dulus to use Dulus Bar. The bar is the gift; Dulus is just one of many agents it
132
+ speaks to.
133
+
134
+ ---
135
+
136
+ ## What it does
137
+
138
+ - Floating island, top-center (or notch-anchored on macOS).
139
+ - Lists active agents with a live status dot (running / waiting / done / error).
140
+ - When an agent asks for permission, shows **Allow / Deny** right on the island and
141
+ forwards your choice back to the agent over stdin.
142
+ - Reveals on hover, pops open on agent activity, and **stays open on a permission
143
+ request** until you answer.
144
+ - Click an agent to jump to its terminal / editor window.
145
+
146
+ ---
147
+
148
+ ## How it works
149
+
150
+ Agents connect to a tiny local WebSocket hub and send status; the island renders
151
+ it and relays your Allow/Deny back.
152
+
153
+ **Agent → bar:**
154
+ ```json
155
+ { "agent": "Claude", "type": "tool_request", "session_id": "abc123",
156
+ "payload": { "tool": "Bash", "args": "rm -rf build/" } }
157
+ ```
158
+ Event types: `session_started`, `message`, `tool_request`, `tool_approved`,
159
+ `tool_denied`, `completed`, `error`. (`model` / `ctx` payload fields are optional.)
160
+
161
+ **Bar → agent (Allow/Deny):**
162
+ ```json
163
+ { "type": "decision", "session_id": "abc123", "payload": { "approved": true } }
164
+ ```
165
+
166
+ ```
167
+ dulus_bar/
168
+ overlay.py Floating island UI (PyQt6), notch-aware + reveal-on-hover
169
+ server.py WebSocket hub (ws://127.0.0.1:17372)
170
+ agents.py Per-agent icon + accent color registry
171
+ native/ win32 / pyobjc / wmctrl window focus per platform
172
+ wrappers/
173
+ base_wrapper.py Shared: pipe stdio, detect prompts, inject Allow/Deny
174
+ agent_wrapper.py Generic wrapper for any CLI agent
175
+ macos/ Native SwiftUI/AppKit notch surface
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Build a standalone binary (optional)
181
+
182
+ ```bash
183
+ pip install pyinstaller
184
+ pyinstaller --noconfirm DulusBar.spec
185
+ ```
186
+
187
+ Output lands in `dist/` — `DulusBar.exe` on Windows, `DulusBar.app` on macOS
188
+ (bundled as an `LSUIElement`, so no Dock icon).
189
+
190
+ ---
191
+
192
+ ## Platform notes
193
+
194
+ - **Windows** — fully tested. `pywin32` for window focus; PyQt overlay with
195
+ reveal-on-hover.
196
+ - **macOS** — notch hugging + always-visible via `pyobjc`. Without it, still runs
197
+ as a plain top-most island.
198
+ - **Linux** — X11 window focus via `wmctrl`/`xdotool`; on Wayland the island shows
199
+ and stays on top, jump-to-terminal is best-effort.
200
+
201
+ ---
202
+
203
+ ## Contributing
204
+
205
+ PRs welcome — new agent icons/colors, platform fixes, packaging. Open an issue,
206
+ fork it, ship it.
207
+
208
+ ## License
209
+
210
+ **MIT.** Use it, fork it, ship it. We shipped the idea on purpose — no monopoly.
211
+
212
+ <p align="center"><sub>Built by one builder in the Dominican Republic 🇩🇴 — for everyone. 🦅</sub></p>
@@ -0,0 +1,181 @@
1
+ <h1 align="center">Dulus Bar 🦅</h1>
2
+
3
+ <p align="center">
4
+ <strong>A cross-platform Dynamic Island for your AI agents.</strong><br>
5
+ Free. Native. MIT. Works with <em>any</em> agent — no account, no cloud, no lock-in.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <code>pip install dulus-bar</code>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <img alt="MIT" src="https://img.shields.io/badge/license-MIT-green">
14
+ <img alt="Python" src="https://img.shields.io/badge/python-3.10%2B-blue">
15
+ <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%C2%B7%20macOS%20%C2%B7%20Linux-lightgrey">
16
+ </p>
17
+
18
+ ---
19
+
20
+ Dulus Bar puts a floating status island at the top of your screen that shows every
21
+ AI coding agent you're running — Claude Code, Codex, Gemini, Cursor, Dulus, and
22
+ any other CLI — and lets you **Allow / Deny** their permission prompts without
23
+ diving back into a terminal.
24
+
25
+ It's the idea itself, shipped for everyone. **Not tied to Dulus** — wrap any agent
26
+ and it shows up. We built the island we wanted; here it is for the whole world. 🌎
27
+
28
+ - **All platforms** — Windows, macOS, and Linux.
29
+ - **macOS notch-native** — on MacBooks with a camera notch the island hugs the
30
+ notch and stays visible across every Space and full-screen app.
31
+ - **Reveal on hover** — everywhere else it rests as a slim tab at the top edge and
32
+ slides open when your mouse approaches, or when an agent needs you.
33
+ - **Any agent** — anything with a CLI works through a thin wrapper; well-known
34
+ agents get their own icon and accent color.
35
+ - **100% local** — no cloud, no account, no telemetry. One WebSocket on
36
+ `127.0.0.1:17372`.
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ Requires Python 3.10+.
43
+
44
+ ```bash
45
+ pip install dulus-bar
46
+ dulusbar
47
+ ```
48
+
49
+ That's it — the island appears at the top of your screen. Move your mouse to the
50
+ top-center to reveal it; right-click the tray icon for the menu.
51
+
52
+ <details>
53
+ <summary><strong>From source (for the wired-agent extras)</strong></summary>
54
+
55
+ ```bash
56
+ git clone https://github.com/Dulus-Ai/Dulus-bar.git
57
+ cd Dulus-bar
58
+
59
+ # macOS / Linux
60
+ ./setup.sh
61
+
62
+ # Windows
63
+ .\setup.ps1
64
+ ```
65
+
66
+ > Linux tip: for click-to-focus of a terminal on X11, install `wmctrl` or `xdotool`.
67
+ >
68
+ > macOS: `setup.sh` also builds the native SwiftUI/AppKit notch surface using the
69
+ > Swift toolchain in Xcode Command Line Tools. The Python process stays the local
70
+ > WebSocket hub; Windows and Linux use the PyQt overlay.
71
+
72
+ </details>
73
+
74
+ ---
75
+
76
+ ## Wire up any AI agent — zero monopoly
77
+
78
+ Dulus Bar is **agent-agnostic**. Wrap any CLI and it reports to the island:
79
+
80
+ ```bash
81
+ python wrappers/agent_wrapper.py "Codex" codex fix auth bug
82
+ python wrappers/agent_wrapper.py "Gemini" gemini
83
+ python wrappers/claude_wrapper.py # Claude Code helper
84
+ ```
85
+
86
+ `agent_wrapper.py "Display Name" <command> [args...]` reports that agent to the
87
+ bar. Known names (Claude, Codex, Gemini, Cursor, Copilot, Kimi, DeepSeek, Grok,
88
+ Qwen, Mistral, Windsurf, Zed, Aider, Ollama, …) get a curated icon + color;
89
+ anything else gets a stable auto-assigned color.
90
+
91
+ Or **right-click the island → Open agent…** and pick any executable from a file
92
+ dialog — Dulus Bar launches it in its own terminal, already wired.
93
+
94
+ ---
95
+
96
+ ## Works beautifully with Dulus (optional)
97
+
98
+ If you run [Dulus](https://dulus.ai), the island also shows the active **model**
99
+ and live **context usage** — e.g. `kimi/kimi-k2.5 · ctx 38%`. But you never need
100
+ Dulus to use Dulus Bar. The bar is the gift; Dulus is just one of many agents it
101
+ speaks to.
102
+
103
+ ---
104
+
105
+ ## What it does
106
+
107
+ - Floating island, top-center (or notch-anchored on macOS).
108
+ - Lists active agents with a live status dot (running / waiting / done / error).
109
+ - When an agent asks for permission, shows **Allow / Deny** right on the island and
110
+ forwards your choice back to the agent over stdin.
111
+ - Reveals on hover, pops open on agent activity, and **stays open on a permission
112
+ request** until you answer.
113
+ - Click an agent to jump to its terminal / editor window.
114
+
115
+ ---
116
+
117
+ ## How it works
118
+
119
+ Agents connect to a tiny local WebSocket hub and send status; the island renders
120
+ it and relays your Allow/Deny back.
121
+
122
+ **Agent → bar:**
123
+ ```json
124
+ { "agent": "Claude", "type": "tool_request", "session_id": "abc123",
125
+ "payload": { "tool": "Bash", "args": "rm -rf build/" } }
126
+ ```
127
+ Event types: `session_started`, `message`, `tool_request`, `tool_approved`,
128
+ `tool_denied`, `completed`, `error`. (`model` / `ctx` payload fields are optional.)
129
+
130
+ **Bar → agent (Allow/Deny):**
131
+ ```json
132
+ { "type": "decision", "session_id": "abc123", "payload": { "approved": true } }
133
+ ```
134
+
135
+ ```
136
+ dulus_bar/
137
+ overlay.py Floating island UI (PyQt6), notch-aware + reveal-on-hover
138
+ server.py WebSocket hub (ws://127.0.0.1:17372)
139
+ agents.py Per-agent icon + accent color registry
140
+ native/ win32 / pyobjc / wmctrl window focus per platform
141
+ wrappers/
142
+ base_wrapper.py Shared: pipe stdio, detect prompts, inject Allow/Deny
143
+ agent_wrapper.py Generic wrapper for any CLI agent
144
+ macos/ Native SwiftUI/AppKit notch surface
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Build a standalone binary (optional)
150
+
151
+ ```bash
152
+ pip install pyinstaller
153
+ pyinstaller --noconfirm DulusBar.spec
154
+ ```
155
+
156
+ Output lands in `dist/` — `DulusBar.exe` on Windows, `DulusBar.app` on macOS
157
+ (bundled as an `LSUIElement`, so no Dock icon).
158
+
159
+ ---
160
+
161
+ ## Platform notes
162
+
163
+ - **Windows** — fully tested. `pywin32` for window focus; PyQt overlay with
164
+ reveal-on-hover.
165
+ - **macOS** — notch hugging + always-visible via `pyobjc`. Without it, still runs
166
+ as a plain top-most island.
167
+ - **Linux** — X11 window focus via `wmctrl`/`xdotool`; on Wayland the island shows
168
+ and stays on top, jump-to-terminal is best-effort.
169
+
170
+ ---
171
+
172
+ ## Contributing
173
+
174
+ PRs welcome — new agent icons/colors, platform fixes, packaging. Open an issue,
175
+ fork it, ship it.
176
+
177
+ ## License
178
+
179
+ **MIT.** Use it, fork it, ship it. We shipped the idea on purpose — no monopoly.
180
+
181
+ <p align="center"><sub>Built by one builder in the Dominican Republic 🇩🇴 — for everyone. 🦅</sub></p>
@@ -0,0 +1,6 @@
1
+ """Dulus Bar — a cross-platform Dynamic Island for your AI agents.
2
+
3
+ Free, native, no subscription. Windows / macOS (notch-aware) / Linux.
4
+ """
5
+
6
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """python -m dulus_bar"""
2
+
3
+ from .main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,111 @@
1
+ """Agent catalog — per-agent identity (icon + accent color) for any AI CLI.
2
+
3
+ Dulus Bar is agent-agnostic: it will happily show *any* agent name a wrapper
4
+ reports. This module just gives the well-known ones a recognizable emoji and
5
+ accent color, and hands unknown agents a stable, nicely-distributed color so
6
+ the UI still looks intentional. It is presentation-only.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ from dataclasses import dataclass
13
+ from typing import Dict, List, Tuple
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class AgentStyle:
18
+ key: str
19
+ display: str
20
+ emoji: str
21
+ accent: str # hex
22
+ aliases: tuple = ()
23
+
24
+
25
+ # Curated identities. `accent` roughly follows each brand.
26
+ _AGENTS: List[AgentStyle] = [
27
+ AgentStyle("dulus", "Dulus", "🦅", "#f59e0b", ("dulus", "interant")),
28
+ AgentStyle("claude", "Claude Code", "✳️", "#d97757", ("claude", "claude code", "anthropic")),
29
+ AgentStyle("codex", "Codex", "◆", "#10a37f", ("codex", "openai", "gpt")),
30
+ AgentStyle("gemini", "Gemini", "✦", "#4285f4", ("gemini", "google")),
31
+ AgentStyle("cursor", "Cursor", "▸", "#7c3aed", ("cursor",)),
32
+ AgentStyle("copilot", "Copilot", "⧉", "#6e7681", ("copilot", "github copilot")),
33
+ AgentStyle("kimi", "Kimi", "◐", "#2563eb", ("kimi", "moonshot")),
34
+ AgentStyle("deepseek", "DeepSeek", "≋", "#4d6bfe", ("deepseek",)),
35
+ AgentStyle("grok", "Grok", "𝕏", "#111827", ("grok", "xai")),
36
+ AgentStyle("qwen", "Qwen", "✺", "#615ced", ("qwen", "alibaba", "tongyi")),
37
+ AgentStyle("mistral", "Mistral", "◢", "#fa5010", ("mistral", "codestral")),
38
+ AgentStyle("windsurf", "Windsurf", "≈", "#09b6a2", ("windsurf", "codeium")),
39
+ AgentStyle("zed", "Zed", "⚡", "#084ccf", ("zed",)),
40
+ AgentStyle("aider", "Aider", "✎", "#22c55e", ("aider",)),
41
+ AgentStyle("cline", "Cline", "◔", "#0ea5e9", ("cline", "roo")),
42
+ AgentStyle("ollama", "Ollama", "◍", "#000000", ("ollama", "llama")),
43
+ ]
44
+
45
+ _BY_ALIAS: Dict[str, AgentStyle] = {}
46
+ for _a in _AGENTS:
47
+ _BY_ALIAS[_a.key] = _a
48
+ for _al in _a.aliases:
49
+ _BY_ALIAS[_al] = _a
50
+
51
+ # Fallback palette for unknown agents (kept colorblind-distinct-ish).
52
+ _FALLBACK_PALETTE = (
53
+ "#38bdf8",
54
+ "#a78bfa",
55
+ "#34d399",
56
+ "#fbbf24",
57
+ "#fb7185",
58
+ "#e879f9",
59
+ "#5eead4",
60
+ "#93c5fd",
61
+ )
62
+
63
+
64
+ def style_for(agent_name: str) -> AgentStyle:
65
+ """Return an AgentStyle for any agent name (never raises)."""
66
+ name = (agent_name or "").strip()
67
+ low = name.lower()
68
+ if low in _BY_ALIAS:
69
+ return _BY_ALIAS[low]
70
+ for alias, style in _BY_ALIAS.items():
71
+ if alias in low:
72
+ return style
73
+ # Deterministic fallback color from the name.
74
+ idx = sum(ord(c) for c in low) % len(_FALLBACK_PALETTE) if low else 0
75
+ return AgentStyle(
76
+ key=low or "agent",
77
+ display=name or "Agent",
78
+ emoji="●",
79
+ accent=_FALLBACK_PALETTE[idx],
80
+ )
81
+
82
+
83
+ def is_dulus(agent_name: str) -> bool:
84
+ return style_for(agent_name).key == "dulus"
85
+
86
+
87
+ # CLI executable names for known agents, used by "quick launch" in the UI.
88
+ # The first name found on PATH wins.
89
+ _LAUNCH_COMMANDS: Dict[str, Tuple[str, ...]] = {
90
+ "claude": ("claude",),
91
+ "codex": ("codex",),
92
+ "gemini": ("gemini",),
93
+ "cursor": ("cursor-agent", "cursor"),
94
+ "aider": ("aider",),
95
+ "qwen": ("qwen",),
96
+ "grok": ("grok",),
97
+ "copilot": ("copilot",),
98
+ "opencode": ("opencode",),
99
+ }
100
+
101
+
102
+ def detect_installed() -> List[Tuple[AgentStyle, str]]:
103
+ """Return (style, resolved_executable) for known agents found on PATH."""
104
+ found: List[Tuple[AgentStyle, str]] = []
105
+ for key, exe_names in _LAUNCH_COMMANDS.items():
106
+ for exe in exe_names:
107
+ path = shutil.which(exe)
108
+ if path:
109
+ found.append((_BY_ALIAS[key], path))
110
+ break
111
+ return found
@@ -0,0 +1,118 @@
1
+ """Lifecycle bridge for the native SwiftUI/AppKit notch surface on macOS."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import os
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from .server import AgentEventServer
15
+
16
+ WS_PORT = 17372
17
+ _NATIVE_EXECUTABLE = "DulusBarNative"
18
+
19
+
20
+ def project_root() -> Path:
21
+ return Path(__file__).resolve().parent.parent
22
+
23
+
24
+ def native_binary(root: Optional[Path] = None) -> Path:
25
+ """Return the preferred release helper path."""
26
+ base = root or project_root()
27
+ override = os.environ.get("DULUS_BAR_NATIVE_BINARY")
28
+ if override:
29
+ return Path(override).expanduser().resolve()
30
+ return base / "macos" / ".build" / "release" / _NATIVE_EXECUTABLE
31
+
32
+
33
+ def native_available(root: Optional[Path] = None) -> bool:
34
+ path = native_binary(root)
35
+ return path.is_file() and os.access(path, os.X_OK)
36
+
37
+
38
+ def build_native(root: Optional[Path] = None) -> Path:
39
+ """Build the native helper and return its executable path."""
40
+ base = root or project_root()
41
+ package = base / "macos"
42
+ if not (package / "Package.swift").is_file():
43
+ raise FileNotFoundError(f"native package missing: {package}")
44
+ subprocess.run(
45
+ ["swift", "build", "-c", "release"],
46
+ cwd=package,
47
+ check=True,
48
+ )
49
+ binary = native_binary(base)
50
+ if not binary.is_file():
51
+ raise FileNotFoundError(f"native build completed without {binary}")
52
+ return binary
53
+
54
+
55
+ def _terminate(process: subprocess.Popen) -> None:
56
+ if process.poll() is not None:
57
+ return
58
+ process.terminate()
59
+ try:
60
+ process.wait(timeout=3)
61
+ except subprocess.TimeoutExpired:
62
+ process.kill()
63
+ process.wait(timeout=2)
64
+
65
+
66
+ def run_native() -> int:
67
+ """Own the WebSocket hub and run the native helper until it exits."""
68
+ binary = native_binary()
69
+ if not native_available():
70
+ if os.environ.get("DULUS_BAR_NATIVE_AUTOBUILD", "1") == "0":
71
+ raise FileNotFoundError(f"native helper not built: {binary}")
72
+ print("[dulusbar] native helper missing; building release binary...")
73
+ binary = build_native()
74
+
75
+ server = AgentEventServer(port=WS_PORT)
76
+ server.start()
77
+ process = subprocess.Popen(
78
+ [str(binary), "--websocket", f"ws://127.0.0.1:{WS_PORT}"],
79
+ cwd=project_root(),
80
+ env={**os.environ, "DULUS_BAR_PARENT_PID": str(os.getpid())},
81
+ )
82
+
83
+ cleaned = False
84
+
85
+ def cleanup() -> None:
86
+ nonlocal cleaned
87
+ if cleaned:
88
+ return
89
+ cleaned = True
90
+ _terminate(process)
91
+ server.stop()
92
+
93
+ def handle_signal(signum: int, _frame: object) -> None:
94
+ cleanup()
95
+ raise SystemExit(128 + signum)
96
+
97
+ atexit.register(cleanup)
98
+ previous_handlers = {}
99
+ for signum in (signal.SIGINT, signal.SIGTERM):
100
+ previous_handlers[signum] = signal.getsignal(signum)
101
+ signal.signal(signum, handle_signal)
102
+
103
+ try:
104
+ while True:
105
+ code = process.poll()
106
+ if code is not None:
107
+ return code
108
+ time.sleep(0.25)
109
+ finally:
110
+ cleanup()
111
+ for signum, handler in previous_handlers.items():
112
+ signal.signal(signum, handler)
113
+
114
+
115
+ def should_use_native() -> bool:
116
+ if sys.platform != "darwin":
117
+ return False
118
+ return os.environ.get("DULUS_BAR_FORCE_QT", "0").lower() not in {"1", "true", "yes"}