Compare commits

...

9 Commits

Author SHA1 Message Date
4b68e3f051 [darwin] Configure AutoRaise
Add delays. This works way better with accordion views where the cursor
is often hovering right around window boundaries
2025-10-16 15:29:41 -07:00
81a3657759 [darwin] Add AutoRaise
provides focus-follows-mouse
2025-10-16 10:00:12 -07:00
32e1b81034 [aerospace] Fix fullscreen chord
Cmd-F is too ingrained in muscle memory for Find
2025-10-15 11:14:50 -07:00
6f00c72540 [app-launcher-server] process detection fixes 2025-10-14 18:09:36 -07:00
d26007aa61 [aerospace] More tweaking 2025-10-14 13:43:44 -07:00
1caa8bba3e [aerospace] Further tweaks 2025-10-14 08:26:51 -07:00
d3cb09040a [kodi] Fix autologin for boxy 2025-10-13 14:32:28 -07:00
4bfacffa17 [development] Remove goose 2025-10-13 14:26:08 -07:00
a6961f05ca [app-launcher] Add app-launcher to boxy 2025-10-13 14:25:51 -07:00
7 changed files with 326 additions and 11 deletions

View File

@@ -2,6 +2,7 @@
let let
customPkgs = pkgs.callPackage ../packages {}; customPkgs = pkgs.callPackage ../packages {};
leader = "cmd"; # Change this to experiment with different leader keys (e.g., "cmd", "ctrl")
in in
{ {
# Provide arguments to role modules # Provide arguments to role modules
@@ -13,6 +14,26 @@ in
home.homeDirectory = lib.mkForce "/Users/johno"; home.homeDirectory = lib.mkForce "/Users/johno";
home.stateVersion = "24.05"; home.stateVersion = "24.05";
# System packages
home.packages = with pkgs; [
autoraise
];
# Auto-start autoraise on login
launchd.agents.autoraise = {
enable = true;
config = {
ProgramArguments = [
"${pkgs.autoraise}/bin/AutoRaise"
"-pollMillis" "50"
"-delay" "2"
"-focusDelay" "2"
];
RunAtLoad = true;
KeepAlive = true;
};
};
# Override Darwin-incompatible settings from base role # Override Darwin-incompatible settings from base role
programs.rbw.settings.pinentry = lib.mkForce pkgs.pinentry_mac; programs.rbw.settings.pinentry = lib.mkForce pkgs.pinentry_mac;
@@ -43,6 +64,86 @@ in
home.shell.enableShellIntegration = true; home.shell.enableShellIntegration = true;
# TODO: Move this to its own role and/or module
programs.aerospace = {
enable = true;
launchd.enable = true;
userSettings.mode.main.binding = {
"${leader}-slash" = "layout tiles horizontal vertical";
"${leader}-comma" = "layout accordion horizontal vertical";
"${leader}-shift-q" = "close";
"${leader}-shift-f" = "fullscreen";
"${leader}-h" = "focus left";
"${leader}-j" = "focus down";
"${leader}-k" = "focus up";
"${leader}-l" = "focus right";
"${leader}-shift-h" = "move left";
"${leader}-shift-j" = "move down";
"${leader}-shift-k" = "move up";
"${leader}-shift-l" = "move right";
"${leader}-minus" = "resize smart -50";
"${leader}-equal" = "resize smart +50";
"${leader}-1" = "workspace 1";
"${leader}-2" = "workspace 2";
"${leader}-3" = "workspace 3";
"${leader}-4" = "workspace 4";
"${leader}-5" = "workspace 5";
"${leader}-6" = "workspace 6";
"${leader}-7" = "workspace 7";
"${leader}-8" = "workspace 8";
"${leader}-9" = "workspace 9";
"${leader}-0" = "workspace 10";
"${leader}-shift-1" = "move-node-to-workspace 1";
"${leader}-shift-2" = "move-node-to-workspace 2";
"${leader}-shift-3" = "move-node-to-workspace 3";
"${leader}-shift-4" = "move-node-to-workspace 4";
"${leader}-shift-5" = "move-node-to-workspace 5";
"${leader}-shift-6" = "move-node-to-workspace 6";
"${leader}-shift-7" = "move-node-to-workspace 7";
"${leader}-shift-8" = "move-node-to-workspace 8";
"${leader}-shift-9" = "move-node-to-workspace 9";
"${leader}-shift-0" = "move-node-to-workspace 10";
"${leader}-tab" = "workspace-back-and-forth";
"${leader}-shift-tab" = "move-workspace-to-monitor --wrap-around next";
"${leader}-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Terminal"
set newWindow to make new window
activate
tell newWindow to set index to 1
end tell
APPLESCRIPT
'';
"${leader}-shift-enter" = ''
exec-and-forget osascript <<'APPLESCRIPT'
tell application "Google Chrome"
set newWindow to make new window
activate
tell newWindow to set index to 1
end tell
APPLESCRIPT
'';
"${leader}-shift-e" = "exec-and-forget zsh --login -c \"emacsclient -c -n\"";
"${leader}-i" = "mode service";
};
userSettings.mode.service.binding = {
esc = ["reload-config" "mode main"];
r = ["flatten-workspace-tree" "mode main"]; # reset layout
f = ["layout floating tiling" "mode main"]; # Toggle between floating and tiling layout
backspace = ["close-all-windows-but-current" "mode main"];
"${leader}-shift-h" = ["join-with left" "mode main"];
"${leader}-shift-j" = ["join-with down" "mode main"];
"${leader}-shift-k" = ["join-with up" "mode main"];
"${leader}-shift-l" = ["join-with right" "mode main"];
};
};
home.roles = { home.roles = {
base.enable = true; base.enable = true;
}; };

View File

@@ -14,7 +14,6 @@ in
home.packages = [ home.packages = [
pkgs.claude-code pkgs.claude-code
pkgs.codex pkgs.codex
pkgs.goose-cli
# Custom packages # Custom packages
customPkgs.tea-rbw customPkgs.tea-rbw

View File

@@ -24,7 +24,7 @@ with lib;
}; };
kodi = { kodi = {
enable = true; enable = true;
autologin = false; autologin = true;
wayland = true; wayland = true;
}; };
users.enable = true; users.enable = true;

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
import json
import logging
import os
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
import psutil
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Allowlisted applications that can be launched
ALLOWED_APPS = {
'firefox': 'firefox',
'kodi': 'kodi'
}
def is_app_running(app_name):
"""Check if an application is already running, returns (is_running, pid)"""
command = ALLOWED_APPS.get(app_name)
if not command:
return False, None
logger.debug(f"Looking for processes related to app '{app_name}' (command: '{command}')")
for proc in psutil.process_iter(['name', 'cmdline', 'pid']):
try:
proc_name = proc.info['name']
cmdline = proc.info['cmdline'] or []
logger.debug(f"Checking process PID {proc.info['pid']}: name='{proc_name}', cmdline={cmdline}")
# Check multiple patterns for the application:
# 1. Process name exactly matches command
# 2. Process name contains the command (e.g., "kodi.bin" contains "kodi")
# 3. Command line starts with the command
# 4. Command line contains the wrapped version (e.g., ".kodi-wrapped")
# 5. Any command line argument ends with the command executable
matches = False
match_reason = ""
if proc_name == command:
matches = True
match_reason = f"exact process name match: '{proc_name}'"
elif command in proc_name:
matches = True
match_reason = f"process name contains command: '{proc_name}' contains '{command}'"
elif cmdline and cmdline[0] == command:
matches = True
match_reason = f"exact cmdline match: '{cmdline[0]}'"
elif cmdline and cmdline[0].endswith('/' + command):
matches = True
match_reason = f"cmdline path ends with command: '{cmdline[0]}'"
elif cmdline and any(f'.{command}-wrapped' in arg for arg in cmdline):
matches = True
match_reason = f"wrapped command in cmdline: {cmdline}"
elif cmdline and any(f'{command}.bin' in arg for arg in cmdline):
matches = True
match_reason = f"binary command in cmdline: {cmdline}"
if matches:
logger.info(f"Found running {app_name} process: PID {proc.info['pid']} ({match_reason})")
return True, proc.info['pid']
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
logger.debug(f"No running process found for {app_name}")
return False, None
class AppLauncherHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
logger.info(format % args)
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'running',
'available_apps': list(ALLOWED_APPS.keys()),
'usage': 'POST /launch/<app_name> to launch an application'
}
self.wfile.write(json.dumps(response, indent=2).encode())
else:
self.send_error(404)
def do_POST(self):
parsed_path = urlparse(self.path)
path_parts = parsed_path.path.strip('/').split('/')
if len(path_parts) == 2 and path_parts[0] == 'launch':
app_name = path_parts[1]
self.launch_app(app_name)
else:
self.send_error(404, "Invalid endpoint. Use /launch/<app_name>")
def launch_app(self, app_name):
if app_name not in ALLOWED_APPS:
self.send_error(400, f"Application '{app_name}' not allowed. Available apps: {list(ALLOWED_APPS.keys())}")
return
command = ALLOWED_APPS[app_name]
# Check if app is already running
is_running, existing_pid = is_app_running(app_name)
if is_running:
logger.info(f"Application {app_name} is already running (PID: {existing_pid}), skipping launch")
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'success',
'message': f'{app_name} is already running',
'pid': existing_pid,
'already_running': True
}
self.wfile.write(json.dumps(response).encode())
return
try:
# Launch the application in the background
# Ensure we have the proper environment for GUI apps
env = os.environ.copy()
logger.info(f"Launching application: {command}")
process = subprocess.Popen(
[command],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = {
'status': 'success',
'message': f'Successfully launched {app_name}',
'pid': process.pid,
'already_running': False
}
self.wfile.write(json.dumps(response).encode())
except FileNotFoundError:
logger.error(f"Application not found: {command}")
self.send_error(500, f"Application '{app_name}' not found on system")
except Exception as e:
logger.error(f"Error launching {command}: {e}")
self.send_error(500, f"Failed to launch {app_name}: {str(e)}")
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081
server = HTTPServer(('0.0.0.0', port), AppLauncherHandler)
logger.info(f"App launcher server starting on port {port}")
logger.info(f"Available applications: {list(ALLOWED_APPS.keys())}")
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("Server shutting down...")
server.server_close()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,10 @@
{ pkgs }:
let
python = pkgs.python3.withPackages (ps: with ps; [
psutil
]);
in
pkgs.writeShellScriptBin "app-launcher-server" ''
exec ${python}/bin/python3 ${./app-launcher-server.py} "$@"
''

View File

@@ -2,4 +2,5 @@
{ {
vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {}; vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {};
tea-rbw = pkgs.callPackage ./tea-rbw {}; tea-rbw = pkgs.callPackage ./tea-rbw {};
app-launcher-server = pkgs.callPackage ./app-launcher-server {};
} }

View File

@@ -4,6 +4,7 @@ with lib;
let let
cfg = config.roles.kodi; cfg = config.roles.kodi;
customPkgs = pkgs.callPackage ../../packages {};
in in
{ {
options.roles.kodi = { options.roles.kodi = {
@@ -14,6 +15,18 @@ in
wayland = mkOption { wayland = mkOption {
default = true; default = true;
}; };
appLauncherServer = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable HTTP app launcher server for remote control";
};
port = mkOption {
type = types.int;
default = 8081;
description = "Port for the app launcher HTTP server";
};
};
}; };
@@ -33,24 +46,39 @@ in
}; };
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 8080 ]; allowedTCPPorts = [ 8080 ] ++ optional cfg.appLauncherServer.enable cfg.appLauncherServer.port;
allowedUDPPorts = [ 8080 ]; allowedUDPPorts = [ 8080 ];
}; };
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
kodiPkg kodiPkg
wget wget
]; firefox
] ++ optional cfg.appLauncherServer.enable customPkgs.app-launcher-server;
programs.kdeconnect.enable = true; programs.kdeconnect.enable = true;
services = if cfg.autologin then { systemd.user.services = mkIf cfg.appLauncherServer.enable {
displayManager = { app-launcher-server = {
description = "HTTP App Launcher Server";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${customPkgs.app-launcher-server}/bin/app-launcher-server ${toString cfg.appLauncherServer.port}";
Restart = "always";
RestartSec = "5s";
Environment = [
"PATH=${pkgs.firefox}/bin:${kodiPkg}/bin:/run/current-system/sw/bin"
];
};
};
};
services.displayManager = mkIf cfg.autologin {
autoLogin.enable = true; autoLogin.enable = true;
autoLogin.user = "kodi"; autoLogin.user = "kodi";
defaultSession = "kodi"; defaultSession = "plasma";
sessionData.autologinSession = "plasma";
}; };
} else {};
}; };
} }