refactor(management): replace curses TUIs with API-backed ncp subcommands; panel/autostart/install integration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+241
@@ -0,0 +1,241 @@
|
||||
"""Shared base for NexusOS popup menus — override_redirect window, no WM placement."""
|
||||
# PyGObject (gi.repository) is dynamically generated and its API is Optional-heavy
|
||||
# (e.g. Gdk.Display.get_default() is typed Display|None). These are fine at runtime,
|
||||
# so silence the type-checker noise for this GTK desktop script.
|
||||
# pyright: reportMissingModuleSource=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportAttributeAccessIssue=false
|
||||
|
||||
import gi, fcntl, os, sys, signal, subprocess
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
|
||||
|
||||
NEXUS_CSS = b"""
|
||||
* { font-family: sans-serif; }
|
||||
|
||||
window {
|
||||
background-color: #2a1d33;
|
||||
color: #f2f2f2;
|
||||
border: 1px solid #4d3461;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
notebook {
|
||||
background-color: #2a1d33;
|
||||
}
|
||||
notebook > header {
|
||||
background-color: #1e1526;
|
||||
border-bottom: 1px solid #4d3461;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
notebook > header > tabs > tab {
|
||||
color: #a8a8a8;
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
notebook > header > tabs > tab:checked {
|
||||
color: #8cc63f;
|
||||
border-bottom: 2px solid #8cc63f;
|
||||
}
|
||||
notebook > header > tabs > tab:hover:not(:checked) {
|
||||
color: #f2f2f2;
|
||||
background-color: rgba(140,198,63,0.10);
|
||||
}
|
||||
notebook stack {
|
||||
background-color: #2a1d33;
|
||||
}
|
||||
|
||||
treeview {
|
||||
background-color: #1e1526;
|
||||
color: #f2f2f2;
|
||||
}
|
||||
treeview:selected {
|
||||
background-color: #88008f;
|
||||
color: #ffffff;
|
||||
}
|
||||
treeview:hover {
|
||||
background-color: rgba(140,198,63,0.14);
|
||||
}
|
||||
treeview header button {
|
||||
background-color: #1e1526;
|
||||
color: #a8a8a8;
|
||||
border: none;
|
||||
border-bottom: 1px solid #4d3461;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
label { color: #f2f2f2; }
|
||||
|
||||
button {
|
||||
background-color: #2e3236;
|
||||
color: #f2f2f2;
|
||||
border: 1px solid #3a3d41;
|
||||
border-radius: 4px;
|
||||
padding: 5px 14px;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
-gtk-icon-shadow: none;
|
||||
}
|
||||
button:hover {
|
||||
background-color: rgba(140,198,63,0.18);
|
||||
border-color: #8cc63f;
|
||||
color: #8cc63f;
|
||||
}
|
||||
button:active {
|
||||
background-color: #8cc63f;
|
||||
color: #0a0a00;
|
||||
}
|
||||
|
||||
scale trough {
|
||||
background-color: #1e1526;
|
||||
border: 1px solid #4d3461;
|
||||
border-radius: 4px;
|
||||
min-height: 6px;
|
||||
}
|
||||
scale trough highlight {
|
||||
background-color: #8cc63f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
scale slider {
|
||||
background-color: #8cc63f;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
separator {
|
||||
background-color: #4d3461;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
scrolledwindow { background-color: #1e1526; }
|
||||
|
||||
.section-header {
|
||||
color: #a8a8a8;
|
||||
font-size: 0.8em;
|
||||
padding: 4px 8px 2px 8px;
|
||||
}
|
||||
.action-row {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.action-row:hover {
|
||||
background-color: rgba(140,198,63,0.14);
|
||||
color: #8cc63f;
|
||||
}
|
||||
.dim { color: #a8a8a8; }
|
||||
"""
|
||||
|
||||
|
||||
def apply_css():
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_data(NEXUS_CSS)
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
|
||||
def make_popup_window(width, height):
|
||||
"""Create an override_redirect popup window — bypasses WM placement entirely."""
|
||||
win = Gtk.Window(type=Gtk.WindowType.POPUP)
|
||||
win.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU)
|
||||
win.set_decorated(False)
|
||||
win.set_skip_taskbar_hint(True)
|
||||
win.set_skip_pager_hint(True)
|
||||
win.set_keep_above(True)
|
||||
win.set_default_size(width, height)
|
||||
win.set_resizable(False)
|
||||
|
||||
win.connect('key-press-event', lambda w, e: w.destroy() if e.keyval == Gdk.KEY_Escape else None)
|
||||
|
||||
def on_button_press(w, event):
|
||||
wx, wy = w.get_position()
|
||||
ww = w.get_allocated_width()
|
||||
wh = w.get_allocated_height()
|
||||
rx, ry = int(event.x_root), int(event.y_root)
|
||||
if rx < wx or rx >= wx + ww or ry < wy or ry >= wy + wh:
|
||||
w.destroy()
|
||||
return False
|
||||
win.connect('button-press-event', on_button_press)
|
||||
|
||||
def on_map(w, _event):
|
||||
def do_grab():
|
||||
gdk_win = w.get_window()
|
||||
if gdk_win:
|
||||
seat = Gdk.Display.get_default().get_default_seat()
|
||||
seat.grab(gdk_win, Gdk.SeatCapabilities.ALL, True, None, None, None)
|
||||
return False # don't repeat
|
||||
GLib.timeout_add(150, do_grab)
|
||||
return False
|
||||
win.connect('map-event', on_map)
|
||||
|
||||
def on_destroy(w):
|
||||
Gdk.Display.get_default().get_default_seat().ungrab()
|
||||
win.connect('destroy', on_destroy)
|
||||
|
||||
return win
|
||||
|
||||
|
||||
def get_mouse_position():
|
||||
display = Gdk.Display.get_default()
|
||||
seat = display.get_default_seat()
|
||||
_, x, y = seat.get_pointer().get_position()
|
||||
return x, y
|
||||
|
||||
|
||||
def get_panel_bottom():
|
||||
"""Return y-coordinate just below the top panel via _NET_WORKAREA."""
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
['xprop', '-root', '_NET_WORKAREA'],
|
||||
text=True, stderr=subprocess.DEVNULL
|
||||
)
|
||||
nums = [int(n.strip()) for n in out.split('=')[1].split(',')]
|
||||
return nums[1] # workarea y = where usable area begins (= panel height)
|
||||
except Exception:
|
||||
return 40
|
||||
|
||||
|
||||
def position_window(win, width, height):
|
||||
"""Position popup just below the panel, horizontally centered on click."""
|
||||
mx, _ = get_mouse_position()
|
||||
screen = Gdk.Screen.get_default()
|
||||
sw, sh = screen.get_width(), screen.get_height()
|
||||
|
||||
panel_bottom = get_panel_bottom()
|
||||
x = mx - width // 2
|
||||
y = panel_bottom + 2
|
||||
|
||||
x = max(4, min(x, sw - width - 4))
|
||||
if height > 0:
|
||||
y = min(y, sh - height - 4)
|
||||
|
||||
win.move(x, y)
|
||||
|
||||
|
||||
def single_instance(lockfile, pidfile):
|
||||
"""flock-based single instance. Returns lock_fd on success, exits on collision."""
|
||||
fd = open(lockfile, 'w')
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError:
|
||||
try:
|
||||
with open(pidfile) as f:
|
||||
os.kill(int(f.read().strip()), signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
with open(pidfile, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
return fd
|
||||
|
||||
|
||||
def load_icon(path, size=16):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user