diff --git a/packages/app-launcher-server/app-launcher-server.py b/packages/app-launcher-server/app-launcher-server.py new file mode 100644 index 0000000..287a136 --- /dev/null +++ b/packages/app-launcher-server/app-launcher-server.py @@ -0,0 +1,104 @@ +#!/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 + +# 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' +} + +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/ 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/") + + 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] + + 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 + } + 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() \ No newline at end of file diff --git a/packages/app-launcher-server/default.nix b/packages/app-launcher-server/default.nix new file mode 100644 index 0000000..c86209a --- /dev/null +++ b/packages/app-launcher-server/default.nix @@ -0,0 +1,5 @@ +{ pkgs }: + +pkgs.writeShellScriptBin "app-launcher-server" '' + exec ${pkgs.python3}/bin/python3 ${./app-launcher-server.py} "$@" +'' \ No newline at end of file diff --git a/packages/default.nix b/packages/default.nix index 26a79dc..d6645e1 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -2,4 +2,5 @@ { vulkanHDRLayer = pkgs.callPackage ./vulkan-hdr-layer {}; tea-rbw = pkgs.callPackage ./tea-rbw {}; + app-launcher-server = pkgs.callPackage ./app-launcher-server {}; } diff --git a/roles/kodi/default.nix b/roles/kodi/default.nix index 7283c27..c40bff8 100644 --- a/roles/kodi/default.nix +++ b/roles/kodi/default.nix @@ -4,6 +4,7 @@ with lib; let cfg = config.roles.kodi; + customPkgs = pkgs.callPackage ../../packages {}; in { options.roles.kodi = { @@ -14,6 +15,18 @@ in wayland = mkOption { 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,17 +46,35 @@ in }; networking.firewall = { - allowedTCPPorts = [ 8080 ]; + allowedTCPPorts = [ 8080 ] ++ optional cfg.appLauncherServer.enable cfg.appLauncherServer.port; allowedUDPPorts = [ 8080 ]; }; environment.systemPackages = with pkgs; [ kodiPkg wget - ]; + firefox + ] ++ optional cfg.appLauncherServer.enable customPkgs.app-launcher-server; programs.kdeconnect.enable = true; + systemd.user.services = mkIf cfg.appLauncherServer.enable { + 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 = if cfg.autologin then { displayManager = { autoLogin.enable = true;