dulus-bar 0.3.0__py3-none-any.whl
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.
- dulus_bar/__init__.py +6 -0
- dulus_bar/__main__.py +6 -0
- dulus_bar/agents.py +111 -0
- dulus_bar/assets/dulus-bird.png +0 -0
- dulus_bar/macos_native.py +118 -0
- dulus_bar/main.py +42 -0
- dulus_bar/native/__init__.py +99 -0
- dulus_bar/native/base.py +130 -0
- dulus_bar/native/linux.py +103 -0
- dulus_bar/native/macos.py +205 -0
- dulus_bar/native/windows.py +97 -0
- dulus_bar/overlay.py +1099 -0
- dulus_bar/server.py +193 -0
- dulus_bar-0.3.0.dist-info/METADATA +212 -0
- dulus_bar-0.3.0.dist-info/RECORD +19 -0
- dulus_bar-0.3.0.dist-info/WHEEL +5 -0
- dulus_bar-0.3.0.dist-info/entry_points.txt +2 -0
- dulus_bar-0.3.0.dist-info/licenses/LICENSE +21 -0
- dulus_bar-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""macOS native backend: camera-notch geometry + always-visible overlay.
|
|
2
|
+
|
|
3
|
+
Depends on pyobjc (``pyobjc-framework-Cocoa`` / ``-Quartz``). Every AppKit call
|
|
4
|
+
is guarded so that on a machine without pyobjc the backend simply degrades to
|
|
5
|
+
osascript-based activation and a plain top-most window (no notch hugging).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from .base import NativeBackend, NotchGeometry, WindowInfo
|
|
14
|
+
|
|
15
|
+
try: # pragma: no cover - platform specific
|
|
16
|
+
import AppKit # type: ignore
|
|
17
|
+
import objc # type: ignore
|
|
18
|
+
|
|
19
|
+
_HAVE_APPKIT = True
|
|
20
|
+
except Exception: # pragma: no cover
|
|
21
|
+
_HAVE_APPKIT = False
|
|
22
|
+
|
|
23
|
+
try: # pragma: no cover - platform specific
|
|
24
|
+
import Quartz # type: ignore
|
|
25
|
+
|
|
26
|
+
_HAVE_QUARTZ = True
|
|
27
|
+
except Exception: # pragma: no cover
|
|
28
|
+
_HAVE_QUARTZ = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# NSWindow level / collection-behavior constants (avoid importing symbols that
|
|
32
|
+
# may be missing on older pyobjc builds).
|
|
33
|
+
_NS_MAIN_MENU_LEVEL = 24
|
|
34
|
+
_CB_CAN_JOIN_ALL_SPACES = 1 << 0 # 1
|
|
35
|
+
_CB_STATIONARY = 1 << 4 # 16
|
|
36
|
+
_CB_FULLSCREEN_AUX = 1 << 8 # 256
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MacOSBackend(NativeBackend):
|
|
40
|
+
name = "macos"
|
|
41
|
+
|
|
42
|
+
# --- notch ----------------------------------------------------------
|
|
43
|
+
def notch_geometry(self) -> Optional[NotchGeometry]:
|
|
44
|
+
if not _HAVE_APPKIT:
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
screen = AppKit.NSScreen.mainScreen()
|
|
48
|
+
if screen is None:
|
|
49
|
+
return None
|
|
50
|
+
frame = screen.frame()
|
|
51
|
+
screen_w = int(round(frame.size.width))
|
|
52
|
+
screen_h = int(round(frame.size.height))
|
|
53
|
+
|
|
54
|
+
# safeAreaInsets.top > 0 is the reliable "has notch" signal (macOS 12+).
|
|
55
|
+
top_inset = 0.0
|
|
56
|
+
if screen.respondsToSelector_("safeAreaInsets"):
|
|
57
|
+
try:
|
|
58
|
+
top_inset = float(screen.safeAreaInsets().top)
|
|
59
|
+
except Exception:
|
|
60
|
+
top_inset = 0.0
|
|
61
|
+
|
|
62
|
+
if top_inset <= 0:
|
|
63
|
+
return None # no notch on this display
|
|
64
|
+
|
|
65
|
+
# Width of the notch = screen width minus the two menu-bar wings.
|
|
66
|
+
notch_w = 0
|
|
67
|
+
if screen.respondsToSelector_("auxiliaryTopLeftArea") and screen.respondsToSelector_(
|
|
68
|
+
"auxiliaryTopRightArea"
|
|
69
|
+
):
|
|
70
|
+
try:
|
|
71
|
+
left = screen.auxiliaryTopLeftArea()
|
|
72
|
+
right = screen.auxiliaryTopRightArea()
|
|
73
|
+
notch_w = int(round(screen_w - left.size.width - right.size.width))
|
|
74
|
+
except Exception:
|
|
75
|
+
notch_w = 0
|
|
76
|
+
if notch_w <= 0:
|
|
77
|
+
notch_w = 210 # sensible default (~14"/16" MBP notch width in pt)
|
|
78
|
+
|
|
79
|
+
notch_h = int(round(top_inset))
|
|
80
|
+
x = (screen_w - notch_w) // 2
|
|
81
|
+
return NotchGeometry(
|
|
82
|
+
x=x,
|
|
83
|
+
y=0,
|
|
84
|
+
width=notch_w,
|
|
85
|
+
height=notch_h,
|
|
86
|
+
screen_width=screen_w,
|
|
87
|
+
screen_height=screen_h,
|
|
88
|
+
)
|
|
89
|
+
except Exception:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# --- always visible -------------------------------------------------
|
|
93
|
+
def configure_always_visible(self, win: object) -> None:
|
|
94
|
+
if not _HAVE_APPKIT or win is None:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
win_id = int(win.winId()) # NSView* pointer on macOS
|
|
98
|
+
view = objc.objc_object(c_void_p=win_id)
|
|
99
|
+
ns_window = view.window()
|
|
100
|
+
if ns_window is None:
|
|
101
|
+
return
|
|
102
|
+
# Sit just above the menu bar so the island reads as part of the notch.
|
|
103
|
+
ns_window.setLevel_(_NS_MAIN_MENU_LEVEL + 1)
|
|
104
|
+
ns_window.setCollectionBehavior_(
|
|
105
|
+
_CB_CAN_JOIN_ALL_SPACES | _CB_STATIONARY | _CB_FULLSCREEN_AUX
|
|
106
|
+
)
|
|
107
|
+
ns_window.setHidesOnDeactivate_(False)
|
|
108
|
+
try:
|
|
109
|
+
ns_window.setMovableByWindowBackground_(False)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
except Exception:
|
|
113
|
+
# Best effort — worst case it behaves like a normal top-most window.
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# --- window discovery / focus (Quartz + osascript) ------------------
|
|
117
|
+
def list_windows(self, only_visible: bool = True) -> List[WindowInfo]:
|
|
118
|
+
if not _HAVE_QUARTZ:
|
|
119
|
+
return []
|
|
120
|
+
try:
|
|
121
|
+
opts = Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements
|
|
122
|
+
infos = Quartz.CGWindowListCopyWindowInfo(opts, Quartz.kCGNullWindowID)
|
|
123
|
+
except Exception:
|
|
124
|
+
return []
|
|
125
|
+
out: List[WindowInfo] = []
|
|
126
|
+
for info in infos or []:
|
|
127
|
+
try:
|
|
128
|
+
owner = str(info.get("kCGWindowOwnerName", "") or "")
|
|
129
|
+
title = str(info.get("kCGWindowName", "") or "")
|
|
130
|
+
pid = int(info.get("kCGWindowOwnerPID", 0) or 0)
|
|
131
|
+
except Exception:
|
|
132
|
+
continue
|
|
133
|
+
# handle carries the pid so activate_window can raise the whole app.
|
|
134
|
+
out.append(WindowInfo(handle=pid, title=title or owner, pid=pid, process_name=owner.lower()))
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
def activate_window(self, handle: object) -> bool:
|
|
138
|
+
pid = handle if isinstance(handle, int) else 0
|
|
139
|
+
if _HAVE_APPKIT and pid:
|
|
140
|
+
try:
|
|
141
|
+
app = AppKit.NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
|
|
142
|
+
if app is not None:
|
|
143
|
+
# NSApplicationActivateIgnoringOtherApps = 1 << 1
|
|
144
|
+
return bool(app.activateWithOptions_(1 << 1))
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def jump_to_terminal(self, title_hint: str = "") -> bool:
|
|
150
|
+
# Try the precise window first (needs Screen-Recording perm for titles).
|
|
151
|
+
if super().jump_to_terminal(title_hint):
|
|
152
|
+
return True
|
|
153
|
+
# Fallback: activate a common terminal / editor by app name.
|
|
154
|
+
for app in ("iTerm", "Terminal", "Ghostty", "Warp", "Visual Studio Code", "Cursor"):
|
|
155
|
+
if self._osascript_activate(app):
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def open_terminal(self) -> bool:
|
|
160
|
+
try:
|
|
161
|
+
subprocess.Popen(["open", "-a", "Terminal"])
|
|
162
|
+
return True
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
def open_terminal_running(self, command, title="", cwd=None) -> bool:
|
|
167
|
+
import shlex
|
|
168
|
+
|
|
169
|
+
parts = []
|
|
170
|
+
if cwd:
|
|
171
|
+
parts.append(f"cd {shlex.quote(str(cwd))}")
|
|
172
|
+
parts.append(" ".join(shlex.quote(str(c)) for c in command))
|
|
173
|
+
shell_cmd = " && ".join(parts)
|
|
174
|
+
# Prefer iTerm if present, else Terminal.
|
|
175
|
+
script_iterm = (
|
|
176
|
+
'tell application "iTerm"\n'
|
|
177
|
+
' create window with default profile\n'
|
|
178
|
+
f' tell current session of current window to write text "{shell_cmd}"\n'
|
|
179
|
+
' activate\n'
|
|
180
|
+
'end tell'
|
|
181
|
+
)
|
|
182
|
+
script_term = f'tell application "Terminal" to do script "{shell_cmd}"\ntell application "Terminal" to activate'
|
|
183
|
+
for script in (script_term, script_iterm):
|
|
184
|
+
try:
|
|
185
|
+
r = subprocess.run(["osascript", "-e", script], capture_output=True, timeout=5)
|
|
186
|
+
if r.returncode == 0:
|
|
187
|
+
return True
|
|
188
|
+
except Exception:
|
|
189
|
+
continue
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _osascript_activate(app_name: str) -> bool:
|
|
194
|
+
try:
|
|
195
|
+
r = subprocess.run(
|
|
196
|
+
["osascript", "-e", f'tell application "{app_name}" to activate'],
|
|
197
|
+
capture_output=True,
|
|
198
|
+
timeout=2,
|
|
199
|
+
)
|
|
200
|
+
return r.returncode == 0
|
|
201
|
+
except Exception:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def default_font_family(self) -> str:
|
|
205
|
+
return "SF Pro Text"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Windows (win32) native backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
import win32api
|
|
10
|
+
import win32con
|
|
11
|
+
import win32gui
|
|
12
|
+
import win32process
|
|
13
|
+
|
|
14
|
+
from .base import NativeBackend, WindowInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowsBackend(NativeBackend):
|
|
18
|
+
name = "windows"
|
|
19
|
+
|
|
20
|
+
def _process_name(self, pid: int) -> str:
|
|
21
|
+
try:
|
|
22
|
+
return psutil.Process(pid).name().lower()
|
|
23
|
+
except Exception:
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
def list_windows(self, only_visible: bool = True) -> List[WindowInfo]:
|
|
27
|
+
results: List[WindowInfo] = []
|
|
28
|
+
|
|
29
|
+
def callback(hwnd, _):
|
|
30
|
+
if only_visible and not win32gui.IsWindowVisible(hwnd):
|
|
31
|
+
return
|
|
32
|
+
title = win32gui.GetWindowText(hwnd).strip()
|
|
33
|
+
if not title:
|
|
34
|
+
return
|
|
35
|
+
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
|
36
|
+
results.append(WindowInfo(hwnd, title, pid, self._process_name(pid)))
|
|
37
|
+
|
|
38
|
+
win32gui.EnumWindows(callback, None)
|
|
39
|
+
return results
|
|
40
|
+
|
|
41
|
+
def activate_window(self, handle: object) -> bool:
|
|
42
|
+
hwnd = handle
|
|
43
|
+
try:
|
|
44
|
+
if win32gui.IsIconic(hwnd):
|
|
45
|
+
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
|
46
|
+
current_thread = win32api.GetCurrentThreadId()
|
|
47
|
+
target_thread, _ = win32process.GetWindowThreadProcessId(hwnd)
|
|
48
|
+
attached = False
|
|
49
|
+
if current_thread != target_thread:
|
|
50
|
+
try:
|
|
51
|
+
win32process.AttachThreadInput(current_thread, target_thread, True)
|
|
52
|
+
attached = True
|
|
53
|
+
except Exception:
|
|
54
|
+
attached = False
|
|
55
|
+
try:
|
|
56
|
+
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
|
|
57
|
+
win32gui.BringWindowToTop(hwnd)
|
|
58
|
+
win32gui.SetForegroundWindow(hwnd)
|
|
59
|
+
except Exception:
|
|
60
|
+
# Windows often blocks SetForegroundWindow from a background
|
|
61
|
+
# thread — not fatal; the user can still alt-tab.
|
|
62
|
+
pass
|
|
63
|
+
finally:
|
|
64
|
+
if attached:
|
|
65
|
+
try:
|
|
66
|
+
win32process.AttachThreadInput(current_thread, target_thread, False)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return True
|
|
70
|
+
except Exception:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def open_terminal(self) -> bool:
|
|
74
|
+
for exe in ("wt.exe", "cmd.exe"):
|
|
75
|
+
try:
|
|
76
|
+
subprocess.Popen([exe], shell=False, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
77
|
+
return True
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def open_terminal_running(self, command, title="", cwd=None) -> bool:
|
|
83
|
+
# CREATE_NEW_CONSOLE gives the child its own console with a live stdin,
|
|
84
|
+
# so the wrapper can pipe the agent and inject Allow/Deny.
|
|
85
|
+
try:
|
|
86
|
+
subprocess.Popen(
|
|
87
|
+
list(command),
|
|
88
|
+
cwd=cwd,
|
|
89
|
+
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
|
90
|
+
)
|
|
91
|
+
return True
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
print(f"[native] open_terminal_running failed: {exc}")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def default_font_family(self) -> str:
|
|
97
|
+
return "Segoe UI"
|