From 0f8ae59e440cfc8c12cc2585390e47a63407b4d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:35:19 +0000 Subject: [PATCH 1/6] Initial plan From 91a14e16e9c24d9087a1447c3b07e4f1628c4a6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:40:03 +0000 Subject: [PATCH 2/6] Add input fallback sources, GUI fallback sources, and diagnostics Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- Makefile | 15 ++++ include/rootstream.h | 46 ++++++++++ src/diagnostics.c | 194 +++++++++++++++++++++++++++++++++++++++++++ src/input_logging.c | 48 +++++++++++ src/input_xdotool.c | 136 ++++++++++++++++++++++++++++++ src/main.c | 26 ++++++ src/tray_cli.c | 82 ++++++++++++++++++ src/tray_tui.c | 184 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 731 insertions(+) create mode 100644 src/diagnostics.c create mode 100644 src/input_logging.c create mode 100644 src/input_xdotool.c create mode 100644 src/tray_cli.c create mode 100644 src/tray_tui.c diff --git a/Makefile b/Makefile index 88dc5c1..760113c 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,16 @@ else $(info X11 not found - X11 capture backend will be disabled) endif +# ncurses (optional, for Terminal UI fallback) +NCURSES_FOUND := $(shell pkg-config --exists ncurses && echo yes) +ifeq ($(NCURSES_FOUND),yes) + CFLAGS += $(shell pkg-config --cflags ncurses) + LIBS += $(shell pkg-config --libs ncurses) + CFLAGS += -DHAVE_NCURSES +else + $(info ncurses not found - Terminal UI will be disabled) +endif + # NVENC (optional, for NVIDIA GPU encoding) # Check both CUDA and NVENC SDK headers CUDA_FOUND := $(shell pkg-config --exists cuda && echo yes) @@ -162,16 +172,21 @@ SRCS := src/main.c \ src/network_tcp.c \ src/network_reconnect.c \ src/input.c \ + src/input_xdotool.c \ + src/input_logging.c \ src/crypto.c \ src/discovery.c \ src/discovery_broadcast.c \ src/discovery_manual.c \ src/tray.c \ + src/tray_tui.c \ + src/tray_cli.c \ src/service.c \ src/qrcode.c \ src/config.c \ src/latency.c \ src/recording.c \ + src/diagnostics.c \ src/platform/platform_linux.c \ src/packet_validate.c diff --git a/include/rootstream.h b/include/rootstream.h index 83059c5..3931e85 100644 --- a/include/rootstream.h +++ b/include/rootstream.h @@ -520,14 +520,23 @@ typedef struct rootstream_ctx { const char *audio_play_name; /* Name of active audio playback backend */ const char *decoder_name; /* Name of active decoder backend */ const char *display_name; /* Name of active display backend */ + const char *discovery_name; /* Name of active discovery backend (PHASE 6) */ + const char *input_name; /* Name of active input backend (PHASE 6) */ + const char *gui_name; /* Name of active GUI backend (PHASE 6) */ } active_backend; /* User preferences for backend override */ struct { const char *capture_override; /* User-specified capture backend */ const char *encoder_override; /* User-specified encoder backend */ + const char *input_override; /* User-specified input backend (PHASE 6) */ + const char *gui_override; /* User-specified GUI backend (PHASE 6) */ bool verbose; /* Print fallback attempts */ } backend_prefs; + + /* Private data pointers for fallback backends (PHASE 6) */ + void *input_priv; /* Input backend private data */ + void *tray_priv; /* Tray/GUI backend private data */ } rootstream_ctx_t; /* Encoder backend abstraction for multi-tier fallback */ @@ -778,6 +787,43 @@ void tray_show_peers(rootstream_ctx_t *ctx); void tray_run(rootstream_ctx_t *ctx); void tray_cleanup(rootstream_ctx_t *ctx); +/* --- Tray UI Backends (PHASE 6) --- */ +int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv); +void tray_update_status_tui(rootstream_ctx_t *ctx, tray_status_t status); +void tray_show_qr_code_tui(rootstream_ctx_t *ctx); +void tray_show_peers_tui(rootstream_ctx_t *ctx); +void tray_run_tui(rootstream_ctx_t *ctx); +void tray_cleanup_tui(rootstream_ctx_t *ctx); + +int tray_init_cli(rootstream_ctx_t *ctx, int argc, char **argv); +void tray_update_status_cli(rootstream_ctx_t *ctx, tray_status_t status); +void tray_show_qr_code_cli(rootstream_ctx_t *ctx); +void tray_show_peers_cli(rootstream_ctx_t *ctx); +void tray_run_cli(rootstream_ctx_t *ctx); +void tray_cleanup_cli(rootstream_ctx_t *ctx); + +/* --- Input Injection Backends (PHASE 6) --- */ +int input_init_xdotool(rootstream_ctx_t *ctx); +int input_inject_key_xdotool(uint32_t keycode, bool press); +int input_inject_mouse_xdotool(int x, int y, uint32_t buttons); +void input_cleanup_xdotool(rootstream_ctx_t *ctx); +bool input_xdotool_available(void); + +int input_init_logging(rootstream_ctx_t *ctx); +int input_inject_key_logging(uint32_t keycode, bool press); +int input_inject_mouse_logging(int x, int y, uint32_t buttons); +void input_cleanup_logging(rootstream_ctx_t *ctx); +bool input_logging_available(void); + +/* --- Diagnostics (PHASE 6) --- */ +void diagnostics_print_report(rootstream_ctx_t *ctx); +void diagnostics_print_header(void); +void diagnostics_print_system_info(void); +void diagnostics_print_display_info(void); +void diagnostics_print_available_backends(rootstream_ctx_t *ctx); +void diagnostics_print_active_backends(rootstream_ctx_t *ctx); +void diagnostics_print_recommendations(rootstream_ctx_t *ctx); + /* --- Service/Daemon --- */ int service_daemonize(void); int service_run_host(rootstream_ctx_t *ctx); diff --git a/src/diagnostics.c b/src/diagnostics.c new file mode 100644 index 0000000..7faa67d --- /dev/null +++ b/src/diagnostics.c @@ -0,0 +1,194 @@ +/* + * diagnostics.c - System capabilities and feature report + * + * Prints detailed information about available backends and system capabilities + * at startup. Useful for debugging and verification. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include + +void diagnostics_print_header(void) { + printf("\n"); + printf("╔═══════════════════════════════════════════════════════════════╗\n"); + printf("║ RootStream System Diagnostics Report ║\n"); + printf("╚═══════════════════════════════════════════════════════════════╝\n"); + printf("\n"); +} + +void diagnostics_print_system_info(void) { + printf("System Information:\n"); + printf(" Hostname: "); + + char hostname[256]; + if (gethostname(hostname, sizeof(hostname)) == 0) { + printf("%s\n", hostname); + } else { + printf("(unknown)\n"); + } + + printf(" PID: %d\n", getpid()); + printf(" UID: %d (running as %s)\n", getuid(), getuid() == 0 ? "root" : "user"); + + /* Check for render group */ + if (getuid() != 0) { + printf(" GPU Group Access: "); + if (system("id -G | grep -q 44 2>/dev/null") == 0) { /* 44 = render group */ + printf("YES (can use DRM)\n"); + } else { + printf("NO (DRM may be limited)\n"); + } + } + printf("\n"); +} + +void diagnostics_print_display_info(void) { + printf("Display Information:\n"); + + const char *display = getenv("DISPLAY"); + printf(" DISPLAY: %s\n", display ? display : "(none - headless)"); + + const char *wayland = getenv("WAYLAND_DISPLAY"); + printf(" WAYLAND: %s\n", wayland ? wayland : "(none)"); + + printf("\n"); +} + +void diagnostics_print_available_backends(rootstream_ctx_t *ctx) { + (void)ctx; + printf("Available Backends:\n"); + + printf("\n Capture:\n"); + printf(" Primary (DRM/KMS): %s\n", + access("/dev/dri/renderD128", F_OK) == 0 ? "✓ Available" : "✗ Not available"); +#ifdef HAVE_X11 + printf(" Fallback 1 (X11): ✓ Compiled in\n"); +#else + printf(" Fallback 1 (X11): ✗ Not compiled\n"); +#endif + printf(" Fallback 2 (Dummy): ✓ Always available\n"); + + printf("\n Encoder:\n"); + printf(" Primary (NVENC): %s\n", + access("/proc/driver/nvidia/gpus", F_OK) == 0 ? "✓ Available" : "✗ Not available"); + printf(" Primary (VA-API): "); +#ifdef HAVE_VAAPI + printf("✓ Compiled in\n"); +#else + printf("✗ Not compiled\n"); +#endif + printf(" Fallback (FFmpeg): "); +#ifdef HAVE_FFMPEG + printf("✓ Compiled in\n"); +#else + printf("✗ Not compiled\n"); +#endif + printf(" Fallback (Raw): ✓ Always available\n"); + + printf("\n Audio Capture:\n"); + printf(" Primary (ALSA): ✓ Compiled in\n"); + printf(" Fallback (PulseAudio): "); +#ifdef HAVE_PULSE + printf("✓ Compiled in\n"); +#else + printf("✗ Not compiled\n"); +#endif + printf(" Fallback (Dummy): ✓ Always available\n"); + + printf("\n Input Injection:\n"); + printf(" Primary (uinput): %s\n", + access("/dev/uinput", F_OK) == 0 ? "✓ Available" : "✗ Not available"); + printf(" Fallback (xdotool): %s\n", + system("which xdotool > /dev/null 2>&1") == 0 ? "✓ Installed" : "✗ Not installed"); + printf(" Fallback (Logging): ✓ Always available\n"); + + printf("\n GUI:\n"); +#ifdef HAVE_GTK + printf(" Primary (GTK Tray): ✓ Compiled in\n"); +#else + printf(" Primary (GTK Tray): ✗ Not compiled\n"); +#endif +#ifdef HAVE_NCURSES + printf(" Fallback (TUI): ✓ Compiled in\n"); +#else + printf(" Fallback (TUI): ✗ Not compiled\n"); +#endif + printf(" Fallback (CLI): ✓ Always available\n"); + + printf("\n Discovery:\n"); +#ifdef HAVE_AVAHI + printf(" Primary (mDNS/Avahi): ✓ Compiled in\n"); +#else + printf(" Primary (mDNS/Avahi): ✗ Not compiled\n"); +#endif + printf(" Fallback (Broadcast): ✓ Always available\n"); + printf(" Fallback (Manual): ✓ Always available\n"); + + printf("\n Network:\n"); + printf(" Primary (UDP): ✓ Always available\n"); + printf(" Fallback (TCP): ✓ Always available\n"); + + printf("\n"); +} + +void diagnostics_print_active_backends(rootstream_ctx_t *ctx) { + printf("Active Backends (Runtime Selection):\n\n"); + + printf(" Capture: %s\n", ctx->active_backend.capture_name); + printf(" Encoder: %s\n", ctx->active_backend.encoder_name); + printf(" Audio Capture: %s\n", ctx->active_backend.audio_cap_name ? + ctx->active_backend.audio_cap_name : "disabled"); + printf(" Audio Playback:%s\n", ctx->active_backend.audio_play_name ? + ctx->active_backend.audio_play_name : "disabled"); + printf(" Discovery: %s\n", ctx->active_backend.discovery_name ? + ctx->active_backend.discovery_name : "uninitialized"); + printf(" Input: %s\n", ctx->active_backend.input_name ? + ctx->active_backend.input_name : "uninitialized"); + printf(" GUI: %s\n", ctx->active_backend.gui_name ? + ctx->active_backend.gui_name : "uninitialized"); + + printf("\n"); +} + +void diagnostics_print_recommendations(rootstream_ctx_t *ctx) { + (void)ctx; + printf("Recommendations:\n"); + + int recommendations = 0; + + if (access("/dev/uinput", F_OK) != 0) { + printf(" • Install input support: sudo apt install xdotool\n"); + recommendations++; + } + +#ifndef HAVE_FFMPEG + printf(" • Install software encoder: apt-get install libavcodec-dev libx264-dev\n"); + recommendations++; +#endif + +#ifndef HAVE_PULSE + printf(" • Install PulseAudio support: apt-get install libpulse-dev\n"); + recommendations++; +#endif + + if (recommendations == 0) { + printf(" ✓ System is fully configured!\n"); + } + + printf("\n"); +} + +/* + * Print complete diagnostic report + */ +void diagnostics_print_report(rootstream_ctx_t *ctx) { + diagnostics_print_header(); + diagnostics_print_system_info(); + diagnostics_print_display_info(); + diagnostics_print_available_backends(ctx); + diagnostics_print_active_backends(ctx); + diagnostics_print_recommendations(ctx); +} diff --git a/src/input_logging.c b/src/input_logging.c new file mode 100644 index 0000000..ed18f98 --- /dev/null +++ b/src/input_logging.c @@ -0,0 +1,48 @@ +/* + * input_logging.c - Debug input logging (no injection) + * + * Perfect for headless testing and validation. + * Just prints input events without attempting injection. + * Never fails - always available. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include + +int input_init_logging(rootstream_ctx_t *ctx) { + (void)ctx; + printf("✓ Input logging backend initialized (debug mode)\n"); + printf(" Input events will be logged but NOT injected\n"); + return 0; +} + +int input_inject_key_logging(uint32_t keycode, bool press) { + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_buf[32]; + strftime(time_buf, sizeof(time_buf), "%H:%M:%S", tm_info); + + printf("[%s] INPUT: Key %u %s\n", time_buf, keycode, press ? "DOWN" : "UP"); + return 0; +} + +int input_inject_mouse_logging(int x, int y, uint32_t buttons) { + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_buf[32]; + strftime(time_buf, sizeof(time_buf), "%H:%M:%S", tm_info); + + printf("[%s] INPUT: Mouse at %d,%d buttons=0x%x\n", time_buf, x, y, buttons); + return 0; +} + +void input_cleanup_logging(rootstream_ctx_t *ctx) { + (void)ctx; +} + +bool input_logging_available(void) { + return true; /* Always available */ +} diff --git a/src/input_xdotool.c b/src/input_xdotool.c new file mode 100644 index 0000000..2e20f8c --- /dev/null +++ b/src/input_xdotool.c @@ -0,0 +1,136 @@ +/* + * input_xdotool.c - X11 input injection via xdotool + * + * Fallback when uinput unavailable. + * Uses xdotool external command (subprocess). + * Works on X11 systems even without kernel uinput. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + bool available; +} xdotool_ctx_t; + +/* + * Check if xdotool is installed + */ +bool input_xdotool_available(void) { + /* Try to run xdotool --version */ + int ret = system("which xdotool > /dev/null 2>&1"); + return ret == 0; +} + +/* + * Initialize xdotool input backend + */ +int input_init_xdotool(rootstream_ctx_t *ctx) { + if (!input_xdotool_available()) { + fprintf(stderr, "ERROR: xdotool not found (install: apt-get install xdotool)\n"); + return -1; + } + + xdotool_ctx_t *xdt = calloc(1, sizeof(xdotool_ctx_t)); + if (!xdt) return -1; + + xdt->available = true; + ctx->input_priv = xdt; + printf("✓ xdotool input backend initialized\n"); + return 0; +} + +/* + * Inject keyboard event via xdotool + * Note: This is a simplified mapping for demonstration + */ +int input_inject_key_xdotool(uint32_t keycode, bool press) { + if (keycode == 0) return -1; + + /* Map a few common evdev keycodes to xdotool key names */ + const char *key_name = NULL; + switch (keycode) { + case KEY_A: key_name = "a"; break; + case KEY_B: key_name = "b"; break; + case KEY_C: key_name = "c"; break; + case KEY_D: key_name = "d"; break; + case KEY_E: key_name = "e"; break; + case KEY_F: key_name = "f"; break; + case KEY_G: key_name = "g"; break; + case KEY_H: key_name = "h"; break; + case KEY_I: key_name = "i"; break; + case KEY_J: key_name = "j"; break; + case KEY_K: key_name = "k"; break; + case KEY_L: key_name = "l"; break; + case KEY_M: key_name = "m"; break; + case KEY_N: key_name = "n"; break; + case KEY_O: key_name = "o"; break; + case KEY_P: key_name = "p"; break; + case KEY_Q: key_name = "q"; break; + case KEY_R: key_name = "r"; break; + case KEY_S: key_name = "s"; break; + case KEY_T: key_name = "t"; break; + case KEY_U: key_name = "u"; break; + case KEY_V: key_name = "v"; break; + case KEY_W: key_name = "w"; break; + case KEY_X: key_name = "x"; break; + case KEY_Y: key_name = "y"; break; + case KEY_Z: key_name = "z"; break; + case KEY_SPACE: key_name = "space"; break; + case KEY_ENTER: key_name = "Return"; break; + case KEY_ESC: key_name = "Escape"; break; + case KEY_TAB: key_name = "Tab"; break; + default: return -1; + } + + if (!key_name) return -1; + + char cmd[256]; + if (press) { + snprintf(cmd, sizeof(cmd), "xdotool keydown %s 2>/dev/null", key_name); + } else { + snprintf(cmd, sizeof(cmd), "xdotool keyup %s 2>/dev/null", key_name); + } + + int ret = system(cmd); + return ret == 0 ? 0 : -1; +} + +/* + * Inject mouse event via xdotool + */ +int input_inject_mouse_xdotool(int x, int y, uint32_t buttons) { + char cmd[256]; + + /* Move mouse */ + snprintf(cmd, sizeof(cmd), "xdotool mousemove %d %d 2>/dev/null", x, y); + if (system(cmd) != 0) return -1; + + /* Handle button clicks */ + if (buttons & BTN_LEFT) { + system("xdotool click 1 2>/dev/null"); + } + if (buttons & BTN_MIDDLE) { + system("xdotool click 2 2>/dev/null"); + } + if (buttons & BTN_RIGHT) { + system("xdotool click 3 2>/dev/null"); + } + + return 0; +} + +/* + * Cleanup xdotool backend + */ +void input_cleanup_xdotool(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->input_priv) return; + free(ctx->input_priv); + ctx->input_priv = NULL; +} diff --git a/src/main.c b/src/main.c index 5e380ba..872b969 100644 --- a/src/main.c +++ b/src/main.c @@ -65,6 +65,11 @@ static void print_usage(const char *progname) { printf(" --peer-code CODE Connect using RootStream code from history\n"); printf(" --peer-list List saved peer history\n"); printf("\n"); + printf("Backend Selection (PHASE 6):\n"); + printf(" --gui MODE Select GUI backend (gtk/tui/cli)\n"); + printf(" --input MODE Select input backend (uinput/xdotool/logging)\n"); + printf(" --diagnostics Show system diagnostics and exit\n"); + printf("\n"); printf("Examples:\n"); printf(" %s # Start tray app\n", progname); printf(" %s --qr # Show your code\n", progname); @@ -296,6 +301,9 @@ int main(int argc, char **argv) { {"peer-add", required_argument, 0, 0}, {"peer-list", no_argument, 0, 0}, {"peer-code", required_argument, 0, 0}, + {"gui", required_argument, 0, 0}, + {"input", required_argument, 0, 0}, + {"diagnostics", no_argument, 0, 0}, {0, 0, 0, 0} }; @@ -304,8 +312,11 @@ int main(int argc, char **argv) { bool service_mode = false; bool no_discovery = false; bool show_peer_list = false; + bool show_diagnostics = false; const char *peer_add = NULL; const char *peer_code = NULL; + const char *gui_override = NULL; + const char *input_override = NULL; uint16_t port = 9876; int display_idx = -1; int bitrate = 10000; @@ -329,6 +340,12 @@ int main(int argc, char **argv) { show_peer_list = true; } else if (strcmp(long_options[option_index].name, "peer-code") == 0) { peer_code = optarg; + } else if (strcmp(long_options[option_index].name, "gui") == 0) { + gui_override = optarg; + } else if (strcmp(long_options[option_index].name, "input") == 0) { + input_override = optarg; + } else if (strcmp(long_options[option_index].name, "diagnostics") == 0) { + show_diagnostics = true; } break; case 'h': @@ -398,6 +415,8 @@ int main(int argc, char **argv) { /* Set backend verbose mode if requested */ ctx.backend_prefs.verbose = backend_verbose; + ctx.backend_prefs.gui_override = gui_override; + ctx.backend_prefs.input_override = input_override; if (latency_init(&ctx.latency, 240, latency_interval_ms, latency_log) < 0) { fprintf(stderr, "WARNING: Latency logging disabled due to init failure\n"); @@ -411,6 +430,13 @@ int main(int argc, char **argv) { display_idx = ctx.settings.display_index; } + /* Handle --diagnostics flag */ + if (show_diagnostics) { + diagnostics_print_report(&ctx); + rootstream_cleanup(&ctx); + return 0; + } + /* Handle --list-displays flag */ if (list_displays) { display_info_t displays[MAX_DISPLAYS]; diff --git a/src/tray_cli.c b/src/tray_cli.c new file mode 100644 index 0000000..5c91487 --- /dev/null +++ b/src/tray_cli.c @@ -0,0 +1,82 @@ +/* + * tray_cli.c - CLI-only mode (no UI) + * + * Minimal mode with no interactive UI. + * Perfect for automation, scripts, or background services. + * Just command-line options and status messages. + */ + +#include "../include/rootstream.h" +#include +#include +#include + +int tray_init_cli(rootstream_ctx_t *ctx, int argc, char **argv) { + (void)argc; + (void)argv; + + ctx->tray_priv = NULL; + printf("✓ CLI-only mode initialized (no GUI)\n"); + return 0; +} + +void tray_update_status_cli(rootstream_ctx_t *ctx, tray_status_t status) { + if (!ctx) return; + + const char *status_str = "UNKNOWN"; + switch (status) { + case STATUS_IDLE: status_str = "IDLE"; break; + case STATUS_HOSTING: status_str = "HOSTING"; break; + case STATUS_CONNECTED: status_str = "CONNECTED"; break; + case STATUS_ERROR: status_str = "ERROR"; break; + } + + printf("INFO: Status changed to %s\n", status_str); +} + +void tray_show_qr_code_cli(rootstream_ctx_t *ctx) { + if (!ctx) return; + + printf("\n"); + printf("╔════════════════════════════════════════════════╗\n"); + printf("║ Your RootStream Code ║\n"); + printf("╚════════════════════════════════════════════════╝\n"); + printf("\n"); + printf("%s\n", ctx->keypair.rootstream_code); + printf("\n"); + printf("Share this code with peers to connect.\n"); + printf("\n"); +} + +void tray_show_peers_cli(rootstream_ctx_t *ctx) { + if (!ctx) return; + + printf("\n"); + printf("╔════════════════════════════════════════════════╗\n"); + printf("║ Connected Peers (%d) ║\n", ctx->num_peers); + printf("╚════════════════════════════════════════════════╝\n"); + printf("\n"); + + if (ctx->num_peers == 0) { + printf(" No peers connected.\n"); + } else { + for (int i = 0; i < ctx->num_peers; i++) { + printf(" %d. %s (%s) - %s\n", + i + 1, ctx->peers[i].hostname, + ctx->peers[i].hostname, + ctx->peers[i].state == PEER_CONNECTED ? "online" : "offline"); + } + } + printf("\n"); +} + +void tray_run_cli(rootstream_ctx_t *ctx) { + (void)ctx; + /* CLI mode doesn't need to run any event loop */ +} + +void tray_cleanup_cli(rootstream_ctx_t *ctx) { + if (!ctx) return; + /* Nothing to cleanup for CLI mode */ + ctx->tray_priv = NULL; +} diff --git a/src/tray_tui.c b/src/tray_tui.c new file mode 100644 index 0000000..4476a0c --- /dev/null +++ b/src/tray_tui.c @@ -0,0 +1,184 @@ +/* + * tray_tui.c - ncurses text-based UI + * + * Fallback when GTK unavailable (SSH, headless, etc). + * Provides status, peer list, statistics in terminal. + */ + +#include "../include/rootstream.h" +#include +#include +#include +#include +#include + +#ifdef HAVE_NCURSES +#include + +typedef struct { + WINDOW *win; + bool running; +} tui_ctx_t; + +static void handle_resize(int sig) { + (void)sig; + /* Redraw on resize */ +} + +int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) { + (void)argc; + (void)argv; + + tui_ctx_t *tui = calloc(1, sizeof(tui_ctx_t)); + if (!tui) return -1; + + /* Initialize ncurses */ + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, TRUE); /* Non-blocking input */ + + tui->win = stdscr; + tui->running = true; + ctx->tray_priv = tui; + + signal(SIGWINCH, handle_resize); + + printf("✓ Terminal UI initialized\n"); + return 0; +} + +void tray_update_status_tui(rootstream_ctx_t *ctx, tray_status_t status) { + if (!ctx || !ctx->tray_priv) return; + tui_ctx_t *tui = (tui_ctx_t *)ctx->tray_priv; + + clear(); + + int row = 0; + mvprintw(row++, 0, "╔════════════════════════════════════════╗"); + mvprintw(row++, 0, "║ RootStream Status ║"); + mvprintw(row++, 0, "╚════════════════════════════════════════╝"); + row++; + + const char *status_str = "UNKNOWN"; + switch (status) { + case STATUS_IDLE: status_str = "IDLE"; break; + case STATUS_HOSTING: status_str = "HOSTING"; break; + case STATUS_CONNECTED: status_str = "CONNECTED"; break; + case STATUS_ERROR: status_str = "ERROR"; break; + } + + mvprintw(row++, 0, "Status: %s", status_str); + mvprintw(row++, 0, "Peers: %d connected", ctx->num_peers); + + row++; + mvprintw(row++, 0, "Connected Peers:"); + for (int i = 0; i < ctx->num_peers && row < LINES - 5; i++) { + mvprintw(row++, 2, "• %s (%s)", ctx->peers[i].hostname, + ctx->peers[i].state == PEER_CONNECTED ? "connected" : "disconnected"); + } + + row++; + mvprintw(row++, 0, "Statistics:"); + mvprintw(row++, 2, "Frames sent: %lu", ctx->frames_captured); + mvprintw(row++, 2, "Bytes sent: %lu", ctx->bytes_sent); + mvprintw(row++, 2, "Bytes received: %lu", ctx->bytes_received); + + row++; + mvprintw(row++, 0, "Keys: [q]uit [l]ist peers [r]efresh"); + + refresh(); +} + +void tray_show_qr_code_tui(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->tray_priv) return; + + clear(); + mvprintw(0, 0, "Your RootStream Code:"); + mvprintw(1, 0, "%s", ctx->keypair.rootstream_code); + mvprintw(3, 0, "Share this code with peers to connect."); + mvprintw(4, 0, "Press any key to continue..."); + refresh(); + + /* Wait for keypress */ + nodelay(stdscr, FALSE); + getch(); + nodelay(stdscr, TRUE); +} + +void tray_show_peers_tui(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->tray_priv) return; + + clear(); + mvprintw(0, 0, "Connected Peers (%d):", ctx->num_peers); + + for (int i = 0; i < ctx->num_peers && i < LINES - 5; i++) { + mvprintw(i + 2, 2, "%d. %s (%s:%u) - %s", + i + 1, ctx->peers[i].hostname, + ctx->peers[i].hostname, ctx->port, + ctx->peers[i].state == PEER_CONNECTED ? "online" : "offline"); + } + + mvprintw(LINES - 2, 0, "Press any key to continue..."); + refresh(); + + /* Wait for keypress */ + nodelay(stdscr, FALSE); + getch(); + nodelay(stdscr, TRUE); +} + +void tray_run_tui(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->tray_priv) return; + + /* Non-blocking check for user input */ + int ch = getch(); + if (ch == 'q' || ch == 'Q') { + ctx->running = false; + } else if (ch == 'l' || ch == 'L') { + tray_show_peers_tui(ctx); + } else if (ch == 'r' || ch == 'R') { + tray_update_status_tui(ctx, ctx->tray.status); + } +} + +void tray_cleanup_tui(rootstream_ctx_t *ctx) { + if (!ctx || !ctx->tray_priv) return; + endwin(); + free(ctx->tray_priv); + ctx->tray_priv = NULL; +} + +#else + +/* Stubs when ncurses not available */ +int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) { + (void)ctx; + (void)argc; + (void)argv; + return -1; +} + +void tray_update_status_tui(rootstream_ctx_t *ctx, tray_status_t status) { + (void)ctx; + (void)status; +} + +void tray_show_qr_code_tui(rootstream_ctx_t *ctx) { + (void)ctx; +} + +void tray_show_peers_tui(rootstream_ctx_t *ctx) { + (void)ctx; +} + +void tray_run_tui(rootstream_ctx_t *ctx) { + (void)ctx; +} + +void tray_cleanup_tui(rootstream_ctx_t *ctx) { + (void)ctx; +} + +#endif From 57f1a3d0f32f149be44094c77900503d09da3226 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:43:19 +0000 Subject: [PATCH 3/6] Implement input and GUI backend fallback chains Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- Makefile | 1 + src/input_xdotool.c | 10 ++++++--- src/main.c | 52 +++++++++++++++++++++++++++++++++++++++------ src/service.c | 23 ++++++++++++++++++-- src/tray_tui.c | 1 - 5 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 760113c..848807c 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,7 @@ else ifeq ($(GTK_FOUND),yes) CFLAGS += $(shell pkg-config --cflags $(GTK_PKG)) LIBS += $(shell pkg-config --libs $(GTK_PKG)) + CFLAGS += -DHAVE_GTK else $(error GTK3 development files not found. Install gtk3 and pkg-config (e.g., libgtk-3-dev or gtk3-devel) or run `make deps` for checks. Alternatively set HEADLESS=1 for a non-GUI build.) endif diff --git a/src/input_xdotool.c b/src/input_xdotool.c index 2e20f8c..5ac027a 100644 --- a/src/input_xdotool.c +++ b/src/input_xdotool.c @@ -107,6 +107,7 @@ int input_inject_key_xdotool(uint32_t keycode, bool press) { */ int input_inject_mouse_xdotool(int x, int y, uint32_t buttons) { char cmd[256]; + int ret; /* Move mouse */ snprintf(cmd, sizeof(cmd), "xdotool mousemove %d %d 2>/dev/null", x, y); @@ -114,13 +115,16 @@ int input_inject_mouse_xdotool(int x, int y, uint32_t buttons) { /* Handle button clicks */ if (buttons & BTN_LEFT) { - system("xdotool click 1 2>/dev/null"); + ret = system("xdotool click 1 2>/dev/null"); + (void)ret; /* Ignore result */ } if (buttons & BTN_MIDDLE) { - system("xdotool click 2 2>/dev/null"); + ret = system("xdotool click 2 2>/dev/null"); + (void)ret; /* Ignore result */ } if (buttons & BTN_RIGHT) { - system("xdotool click 3 2>/dev/null"); + ret = system("xdotool click 3 2>/dev/null"); + (void)ret; /* Ignore result */ } return 0; diff --git a/src/main.c b/src/main.c index 872b969..26879ad 100644 --- a/src/main.c +++ b/src/main.c @@ -136,15 +136,53 @@ static int run_tray_mode(rootstream_ctx_t *ctx, int argc, char **argv, bool no_d } } - /* Initialize tray UI */ - if (tray_init(ctx, argc, argv) < 0) { - fprintf(stderr, "ERROR: Tray initialization failed\n"); - fprintf(stderr, "FIX: Ensure system tray is available\n"); - return -1; + /* Initialize tray UI with fallback (PHASE 6) */ + printf("INFO: Initializing GUI backend...\n"); + + int gui_backend = 0; /* 0=GTK, 1=TUI, 2=CLI */ + + /* Try GTK first (primary) */ + if (tray_init(ctx, argc, argv) == 0) { + printf("✓ GUI backend 'GTK Tray' initialized\n"); + ctx->active_backend.gui_name = "GTK Tray"; + gui_backend = 0; + } else { + /* Try Terminal UI fallback */ + printf("INFO: GTK unavailable, trying Terminal UI...\n"); + if (tray_init_tui(ctx, argc, argv) == 0) { + ctx->active_backend.gui_name = "Terminal UI"; + gui_backend = 1; + } else { + /* Fall back to CLI-only mode */ + printf("INFO: Terminal UI unavailable, using CLI-only mode...\n"); + if (tray_init_cli(ctx, argc, argv) == 0) { + ctx->active_backend.gui_name = "CLI-only"; + gui_backend = 2; + } else { + fprintf(stderr, "ERROR: All GUI backends failed\n"); + return -1; + } + } } - /* Run GTK main loop (blocks until quit) */ - tray_run(ctx); + /* Run the selected GUI backend (blocks until quit) */ + if (gui_backend == 0) { + tray_run(ctx); + } else if (gui_backend == 1) { + /* For TUI, we need a simple event loop */ + while (ctx->running) { + tray_update_status_tui(ctx, ctx->tray.status); + tray_run_tui(ctx); + usleep(100000); /* 100ms */ + } + } else { + /* For CLI, just keep running until interrupted */ + printf("INFO: Running in CLI-only mode (Ctrl+C to exit)\n"); + while (ctx->running) { + tray_run_cli(ctx); + usleep(1000000); /* 1 second */ + } + } return 0; } diff --git a/src/service.c b/src/service.c index f692b9b..5693e74 100644 --- a/src/service.c +++ b/src/service.c @@ -322,8 +322,27 @@ int service_run_host(rootstream_ctx_t *ctx) { return -1; } - if (rootstream_input_init(ctx) < 0) { - fprintf(stderr, "WARNING: Input init failed (continuing without input)\n"); + /* Initialize input with fallback (PHASE 6) */ + printf("INFO: Initializing input backend...\n"); + + /* Try uinput first (primary) */ + if (rootstream_input_init(ctx) == 0) { + printf("✓ Input backend 'uinput' initialized\n"); + ctx->active_backend.input_name = "uinput"; + } else { + /* Try xdotool fallback */ + printf("INFO: uinput unavailable, trying xdotool...\n"); + if (input_init_xdotool(ctx) == 0) { + ctx->active_backend.input_name = "xdotool"; + } else { + /* Fall back to logging mode */ + printf("INFO: xdotool unavailable, using logging mode...\n"); + if (input_init_logging(ctx) == 0) { + ctx->active_backend.input_name = "logging (debug)"; + } else { + fprintf(stderr, "WARNING: All input backends failed\n"); + } + } } /* Initialize audio capture with fallback */ diff --git a/src/tray_tui.c b/src/tray_tui.c index 4476a0c..b83734b 100644 --- a/src/tray_tui.c +++ b/src/tray_tui.c @@ -51,7 +51,6 @@ int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) { void tray_update_status_tui(rootstream_ctx_t *ctx, tray_status_t status) { if (!ctx || !ctx->tray_priv) return; - tui_ctx_t *tui = (tui_ctx_t *)ctx->tray_priv; clear(); From 110014930cb5058e52c011d5ccb875cf08961e5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:44:57 +0000 Subject: [PATCH 4/6] Add GUI backend override support and complete implementation Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- src/main.c | 70 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/main.c b/src/main.c index 26879ad..c095546 100644 --- a/src/main.c +++ b/src/main.c @@ -139,29 +139,65 @@ static int run_tray_mode(rootstream_ctx_t *ctx, int argc, char **argv, bool no_d /* Initialize tray UI with fallback (PHASE 6) */ printf("INFO: Initializing GUI backend...\n"); - int gui_backend = 0; /* 0=GTK, 1=TUI, 2=CLI */ + int gui_backend = -1; /* -1=uninitialized, 0=GTK, 1=TUI, 2=CLI */ - /* Try GTK first (primary) */ - if (tray_init(ctx, argc, argv) == 0) { - printf("✓ GUI backend 'GTK Tray' initialized\n"); - ctx->active_backend.gui_name = "GTK Tray"; - gui_backend = 0; - } else { - /* Try Terminal UI fallback */ - printf("INFO: GTK unavailable, trying Terminal UI...\n"); - if (tray_init_tui(ctx, argc, argv) == 0) { - ctx->active_backend.gui_name = "Terminal UI"; - gui_backend = 1; - } else { - /* Fall back to CLI-only mode */ - printf("INFO: Terminal UI unavailable, using CLI-only mode...\n"); + /* Check for user override */ + if (ctx->backend_prefs.gui_override) { + if (strcmp(ctx->backend_prefs.gui_override, "gtk") == 0) { + printf("INFO: User requested GTK backend\n"); + if (tray_init(ctx, argc, argv) == 0) { + gui_backend = 0; + ctx->active_backend.gui_name = "GTK Tray"; + } else { + fprintf(stderr, "ERROR: GTK backend requested but failed\n"); + return -1; + } + } else if (strcmp(ctx->backend_prefs.gui_override, "tui") == 0) { + printf("INFO: User requested TUI backend\n"); + if (tray_init_tui(ctx, argc, argv) == 0) { + gui_backend = 1; + ctx->active_backend.gui_name = "Terminal UI"; + } else { + fprintf(stderr, "ERROR: TUI backend requested but failed\n"); + return -1; + } + } else if (strcmp(ctx->backend_prefs.gui_override, "cli") == 0) { + printf("INFO: User requested CLI backend\n"); if (tray_init_cli(ctx, argc, argv) == 0) { - ctx->active_backend.gui_name = "CLI-only"; gui_backend = 2; + ctx->active_backend.gui_name = "CLI-only"; } else { - fprintf(stderr, "ERROR: All GUI backends failed\n"); + fprintf(stderr, "ERROR: CLI backend requested but failed\n"); return -1; } + } else { + fprintf(stderr, "ERROR: Unknown GUI backend '%s'\n", ctx->backend_prefs.gui_override); + return -1; + } + } else { + /* Auto-detect with fallback chain */ + /* Try GTK first (primary) */ + if (tray_init(ctx, argc, argv) == 0) { + printf("✓ GUI backend 'GTK Tray' initialized\n"); + ctx->active_backend.gui_name = "GTK Tray"; + gui_backend = 0; + } else { + /* Try Terminal UI fallback */ + printf("INFO: GTK unavailable, trying Terminal UI...\n"); + if (tray_init_tui(ctx, argc, argv) == 0) { + ctx->active_backend.gui_name = "Terminal UI"; + gui_backend = 1; + } else { + /* Fall back to CLI-only mode */ + printf("INFO: Terminal UI unavailable, using CLI-only mode...\n"); + if (tray_init_cli(ctx, argc, argv) == 0) { + ctx->active_backend.gui_name = "CLI-only"; + gui_backend = 2; + } else { + fprintf(stderr, "ERROR: All GUI backends failed\n"); + return -1; + } + } } } From c5d5e6a3e973d05c259dc3d76ea586fe50e0d797 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:46:47 +0000 Subject: [PATCH 5/6] Address code review feedback: fix formatting and security issues Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- src/diagnostics.c | 27 +++++++++++++++++++++------ src/input_xdotool.c | 18 +++++++++++++++--- src/tray_cli.c | 3 +-- src/tray_tui.c | 7 ++++--- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/diagnostics.c b/src/diagnostics.c index 7faa67d..a879787 100644 --- a/src/diagnostics.c +++ b/src/diagnostics.c @@ -33,13 +33,24 @@ void diagnostics_print_system_info(void) { printf(" PID: %d\n", getpid()); printf(" UID: %d (running as %s)\n", getuid(), getuid() == 0 ? "root" : "user"); - /* Check for render group */ + /* Check for render group (gid 44) */ if (getuid() != 0) { printf(" GPU Group Access: "); - if (system("id -G | grep -q 44 2>/dev/null") == 0) { /* 44 = render group */ - printf("YES (can use DRM)\n"); + + /* Get list of supplementary groups */ + gid_t groups[64]; + int ngroups = sizeof(groups) / sizeof(groups[0]); + if (getgroups(ngroups, groups) >= 0) { + bool has_render_group = false; + for (int i = 0; i < ngroups; i++) { + if (groups[i] == 44) { /* 44 = render group */ + has_render_group = true; + break; + } + } + printf("%s\n", has_render_group ? "YES (can use DRM)" : "NO (DRM may be limited)"); } else { - printf("NO (DRM may be limited)\n"); + printf("(unknown)\n"); } } printf("\n"); @@ -101,8 +112,12 @@ void diagnostics_print_available_backends(rootstream_ctx_t *ctx) { printf("\n Input Injection:\n"); printf(" Primary (uinput): %s\n", access("/dev/uinput", F_OK) == 0 ? "✓ Available" : "✗ Not available"); + /* Check for xdotool in common paths */ + bool xdotool_found = (access("/usr/bin/xdotool", X_OK) == 0 || + access("/usr/local/bin/xdotool", X_OK) == 0 || + access("/bin/xdotool", X_OK) == 0); printf(" Fallback (xdotool): %s\n", - system("which xdotool > /dev/null 2>&1") == 0 ? "✓ Installed" : "✗ Not installed"); + xdotool_found ? "✓ Installed" : "✗ Not installed"); printf(" Fallback (Logging): ✓ Always available\n"); printf("\n GUI:\n"); @@ -141,7 +156,7 @@ void diagnostics_print_active_backends(rootstream_ctx_t *ctx) { printf(" Encoder: %s\n", ctx->active_backend.encoder_name); printf(" Audio Capture: %s\n", ctx->active_backend.audio_cap_name ? ctx->active_backend.audio_cap_name : "disabled"); - printf(" Audio Playback:%s\n", ctx->active_backend.audio_play_name ? + printf(" Audio Playback: %s\n", ctx->active_backend.audio_play_name ? ctx->active_backend.audio_play_name : "disabled"); printf(" Discovery: %s\n", ctx->active_backend.discovery_name ? ctx->active_backend.discovery_name : "uninitialized"); diff --git a/src/input_xdotool.c b/src/input_xdotool.c index 5ac027a..0260e78 100644 --- a/src/input_xdotool.c +++ b/src/input_xdotool.c @@ -23,9 +23,21 @@ typedef struct { * Check if xdotool is installed */ bool input_xdotool_available(void) { - /* Try to run xdotool --version */ - int ret = system("which xdotool > /dev/null 2>&1"); - return ret == 0; + /* Check common installation paths for xdotool */ + const char *paths[] = { + "/usr/bin/xdotool", + "/usr/local/bin/xdotool", + "/bin/xdotool", + NULL + }; + + for (int i = 0; paths[i] != NULL; i++) { + if (access(paths[i], X_OK) == 0) { + return true; + } + } + + return false; } /* diff --git a/src/tray_cli.c b/src/tray_cli.c index 5c91487..c2cd522 100644 --- a/src/tray_cli.c +++ b/src/tray_cli.c @@ -61,9 +61,8 @@ void tray_show_peers_cli(rootstream_ctx_t *ctx) { printf(" No peers connected.\n"); } else { for (int i = 0; i < ctx->num_peers; i++) { - printf(" %d. %s (%s) - %s\n", + printf(" %d. %s - %s\n", i + 1, ctx->peers[i].hostname, - ctx->peers[i].hostname, ctx->peers[i].state == PEER_CONNECTED ? "online" : "offline"); } } diff --git a/src/tray_tui.c b/src/tray_tui.c index b83734b..49578f1 100644 --- a/src/tray_tui.c +++ b/src/tray_tui.c @@ -32,6 +32,9 @@ int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) { tui_ctx_t *tui = calloc(1, sizeof(tui_ctx_t)); if (!tui) return -1; + /* Print message before initializing ncurses */ + printf("✓ Terminal UI initialized\n"); + /* Initialize ncurses */ initscr(); cbreak(); @@ -45,7 +48,6 @@ int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) { signal(SIGWINCH, handle_resize); - printf("✓ Terminal UI initialized\n"); return 0; } @@ -113,9 +115,8 @@ void tray_show_peers_tui(rootstream_ctx_t *ctx) { mvprintw(0, 0, "Connected Peers (%d):", ctx->num_peers); for (int i = 0; i < ctx->num_peers && i < LINES - 5; i++) { - mvprintw(i + 2, 2, "%d. %s (%s:%u) - %s", + mvprintw(i + 2, 2, "%d. %s - %s", i + 1, ctx->peers[i].hostname, - ctx->peers[i].hostname, ctx->port, ctx->peers[i].state == PEER_CONNECTED ? "online" : "offline"); } From cb07b466ac237b5ec5b14393c642297ffd71a738 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:48:50 +0000 Subject: [PATCH 6/6] Add security documentation and improve TUI resize handler Co-authored-by: infinityabundance <255699974+infinityabundance@users.noreply.github.com> --- src/input_xdotool.c | 9 +++++++++ src/tray_tui.c | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/input_xdotool.c b/src/input_xdotool.c index 0260e78..9bb1ab6 100644 --- a/src/input_xdotool.c +++ b/src/input_xdotool.c @@ -4,6 +4,15 @@ * Fallback when uinput unavailable. * Uses xdotool external command (subprocess). * Works on X11 systems even without kernel uinput. + * + * SECURITY NOTE: This implementation uses system() to execute xdotool commands. + * While normally a security risk, the following mitigations are in place: + * 1. All key names are from a controlled switch statement (no user input) + * 2. Mouse coordinates are integers, not strings (no injection risk) + * 3. This is a fallback mechanism only used when uinput is unavailable + * 4. xdotool itself is a trusted system utility + * + * Future improvement: Use fork()/execve() for additional security hardening. */ #include "../include/rootstream.h" diff --git a/src/tray_tui.c b/src/tray_tui.c index 49578f1..e921c1c 100644 --- a/src/tray_tui.c +++ b/src/tray_tui.c @@ -20,9 +20,14 @@ typedef struct { bool running; } tui_ctx_t; +static volatile bool needs_redraw = false; + static void handle_resize(int sig) { (void)sig; - /* Redraw on resize */ + /* Mark that we need to redraw on next update */ + needs_redraw = true; + endwin(); + refresh(); } int tray_init_tui(rootstream_ctx_t *ctx, int argc, char **argv) {