From 7c1930833a4aa7bb1898bb3c7108af07d54a40a4 Mon Sep 17 00:00:00 2001 From: daredoole Date: Tue, 28 Apr 2026 14:26:37 -0400 Subject: [PATCH 1/4] feat(packaging): include mwb_tray in RPM package --- packaging/rpm/inputflow.spec | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packaging/rpm/inputflow.spec b/packaging/rpm/inputflow.spec index 9285e8d..c45afa2 100644 --- a/packaging/rpm/inputflow.spec +++ b/packaging/rpm/inputflow.spec @@ -14,6 +14,8 @@ BuildRequires: openssl-devel BuildRequires: pkgconf-pkg-config BuildRequires: systemd-rpm-macros BuildRequires: zlib-devel +BuildRequires: gtk3-devel +BuildRequires: libayatana-appindicator3-devel Requires: systemd %{?systemd_requires} @@ -31,12 +33,13 @@ retain the mwb-client naming for compatibility. %autosetup -n %{name}-%{version} %build -%cmake +%cmake -DMWB_BUILD_TRAY=ON %cmake_build %install -%cmake_build --target mwb_client +%cmake_build --target mwb_client mwb_tray install -Dpm0755 %{_vpath_builddir}/mwb_client %{buildroot}%{_bindir}/mwb_client +install -Dpm0755 %{_vpath_builddir}/mwb_tray %{buildroot}%{_bindir}/mwb_tray install -Dpm0644 packaging/usr/lib/sysusers.d/mwb-client.conf %{buildroot}%{_sysusersdir}/mwb-client.conf install -Dpm0644 packaging/usr/lib/modules-load.d/mwb-client-uinput.conf %{buildroot}%{_modulesloaddir}/mwb-client-uinput.conf install -Dpm0644 packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules %{buildroot}%{_udevrulesdir}/70-mwb-client-uinput.rules @@ -62,6 +65,7 @@ udevadm trigger --name-match=uinput || : %license LICENSE %doc README.md packaging/README.md %{_bindir}/mwb_client +%{_bindir}/mwb_tray %{_sysusersdir}/mwb-client.conf %{_modulesloaddir}/mwb-client-uinput.conf %{_udevrulesdir}/70-mwb-client-uinput.rules From a3efdfa0e03d07d308d11a566bafbc53c750e5f1 Mon Sep 17 00:00:00 2001 From: daredoole Date: Tue, 28 Apr 2026 19:09:47 -0400 Subject: [PATCH 2/4] Harden beta setup and diagnostics --- README.md | 9 + docs/beta-workflow.md | 98 +++++ docs/feedback-inspired-roadmap.md | 63 ++++ .../cross-platform-kvm-feedback-prompt.md | 51 +++ mwb-desktop-ui.sh | 186 ++++++++- packaging/README.md | 13 + packaging/rpm/inputflow.spec | 10 + .../usr/share/applications/inputflow.desktop | 10 + scripts/inputflow-diagnostics-bundle.sh | 354 ++++++++++++++++++ scripts/validate-rpm-packaging.sh | 33 ++ src/TrayController.cpp | 32 ++ src/main.cpp | 146 +++++++- 12 files changed, 997 insertions(+), 8 deletions(-) create mode 100644 docs/beta-workflow.md create mode 100644 docs/feedback-inspired-roadmap.md create mode 100644 docs/research/cross-platform-kvm-feedback-prompt.md create mode 100644 packaging/usr/share/applications/inputflow.desktop create mode 100755 scripts/inputflow-diagnostics-bundle.sh diff --git a/README.md b/README.md index c5939bd..e0c024f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Recommended first-run flow for most users: - Run the exported `.ps1` script on your Windows machine to register the Linux peer. 5. **Start:** Choose **Start Service** or launch the tray with `./build/mwb_tray`. +For the full beta setup, health-check, diagnostics, connection-quality, and packaging-verification workflow, see [docs/beta-workflow.md](docs/beta-workflow.md). + --- ## 🛠️ Build & Installation @@ -93,6 +95,13 @@ For power users who prefer manual control: See the full [documentation section](#detailed-documentation) for environment variables and protocol details. +User-facing beta operations: + +- [Guided Windows pairing and export helper](docs/beta-workflow.md#guided-pairing-and-export-helper) +- [Health checks and diagnostics bundle](docs/beta-workflow.md#health-check) +- [Connection quality and latency reporting](docs/beta-workflow.md#connection-quality) +- [Packaging verification](docs/beta-workflow.md#packaging-verification) + ## Detailed Documentation diff --git a/docs/beta-workflow.md b/docs/beta-workflow.md new file mode 100644 index 0000000..d7cd3b9 --- /dev/null +++ b/docs/beta-workflow.md @@ -0,0 +1,98 @@ +# InputFlow Public Beta Workflow + +This guide covers the user-facing beta path for pairing Linux with PowerToys Mouse Without Borders on Windows, checking health, collecting diagnostics, tuning connection quality, and validating package output. + +## Guided Pairing And Export Helper + +Use the desktop controller for the guided path: + +```bash +./mwb-desktop-ui.sh menu +``` + +1. Open **Settings** and enter the Windows host IP, local machine name, port, and exactly one authentication source: inline key, `key_file`, or Secret Service key ID. +2. Use **Connection Behavior** to choose automatic reconnect behavior before starting the service. +3. Export a Windows helper from Linux when the UI exposes the action, or use the CLI fallback: + +```bash +./build/mwb_client export-windows-pair \ + --config ~/.config/mwb-client/config.ini \ + --output inputflow-windows-pair.ps1 \ + --position auto +``` + +The helper writes the Linux peer name, IP, layout position, shared key, `MachinePool`, `MachineMatrixString`, and `Name2IP` values expected by PowerToys. On Windows, install and open PowerToys Mouse Without Borders once, then run the exported script in PowerShell: + +```powershell +powershell -ExecutionPolicy Bypass -File .\inputflow-windows-pair.ps1 +``` + +Keep the exported `.ps1` private because it contains pairing material. Delete it after confirming Windows sees the Linux peer. + +![Pairing helper walkthrough](screenshots/pairing-helper.svg) + +## Health Check + +Run the built-in doctor before filing a beta issue or after changing package/service setup: + +```bash +./build/mwb_client doctor --config ~/.config/mwb-client/config.ini +``` + +The desktop controller shows the same health check together with the user service status through: + +```bash +./mwb-desktop-ui.sh status +``` + +Review warnings for missing `/dev/uinput` access, missing `inputflow` group membership, missing packaged files, unavailable clipboard helpers, or invalid authentication configuration. + +## Diagnostics Bundle + +Until a one-click diagnostics bundle command is available, collect this minimal bundle for beta reports: + +```bash +./build/mwb_client doctor --config ~/.config/mwb-client/config.ini > inputflow-doctor.txt +systemctl --user status --no-pager mwb-client.service > inputflow-service-status.txt +journalctl --user -u mwb-client.service --since "30 minutes ago" --no-pager > inputflow-service-log.txt +``` + +Also include `~/.config/mwb-client/config.ini` with `key`, `key_file`, `key_secret_id`, Windows IPs, and hostnames redacted as needed. Do not attach exported Windows helper scripts or unredacted Secret Service identifiers to public issues. + +## Connection Quality + +For most beta users, keep automatic reconnect enabled and tune only if the service reconnects too aggressively or too slowly: + +```ini +auto_connect_enabled=true +reconnect_initial_backoff_ms=500 +reconnect_max_backoff_ms=30000 +reconnect_idle_retry_ms=5000 +latency_report=false +``` + +Enable latency reporting only while debugging missed or delayed input: + +```bash +./build/mwb_client run --config ~/.config/mwb-client/config.ini --latency-report +``` + +You can also set `latency_report=true` in `config.ini` or run with `MWB_LATENCY_REPORT=1`. The report prints client-side queue and injection timing when the client shuts down. + +## Packaging Verification + +Validate RPM metadata without a full RPM build: + +```bash +scripts/validate-rpm-packaging.sh +``` + +Run full Fedora RPM validation when the build dependencies are available: + +```bash +MWB_VALIDATE_RPM_BUILD=1 scripts/validate-rpm-packaging.sh +``` + +The validation checks that the package keeps the compatibility binary and service names, includes the user unit, sysusers integration, modules-load file, and `/dev/uinput` udev rule, and does not introduce undocumented command aliases. + +![Tray and controller workflow](screenshots/tray-controller.svg) diff --git a/docs/feedback-inspired-roadmap.md b/docs/feedback-inspired-roadmap.md new file mode 100644 index 0000000..b5243da --- /dev/null +++ b/docs/feedback-inspired-roadmap.md @@ -0,0 +1,63 @@ +# Feedback-Inspired Roadmap + +This plan turns user feedback from Linux/Windows keyboard-mouse sharing discussions into concrete InputFlow work. The recurring signal is that users do not just want another Synergy-style KVM. They want Mouse Without Borders behavior on Linux: reliable Windows interoperability, safe setup, shared-key pairing, sane multi-monitor traversal, and recoverable diagnostics. + +## Product Goals + +- Preserve PowerToys Mouse Without Borders protocol compatibility. +- Make Windows plus Linux setup safer and easier than Synergy-family tools. +- Treat multi-monitor geometry as a first-class feature, not a side effect. +- Make beta failures diagnosable without asking users to hand-copy logs. +- Avoid invasive installers or helpers that stop unrelated Windows software. + +## Compatibility Targets + +- Windows side: Microsoft PowerToys Mouse Without Borders settings and protocol behavior. +- Linux side: X11 and Wayland sessions where input injection is available through `/dev/uinput`. +- Network: trusted LAN operation over the existing MWB-compatible ports `15101` for input and `15100` for clipboard. +- Auth: existing shared security key model, including inline config, key file, and Secret Service-backed key references. +- Packaging: user-scoped systemd service gated by config presence, distro package install must not auto-enable remote-control behavior. +- Existing names: keep `mwb_client`, `mwb_tray`, `mwb-client.service`, and `~/.config/mwb-client/config.ini` until an alias migration is explicitly designed. + +## Phase 1: Stabilize Current Beta Flow + +- Keep the health check, diagnostics bundle, connection quality panel, and guided pairing flow. +- Add beta issue templates that request diagnostics bundle output, distro, desktop session, Windows version, PowerToys version, monitor layout, and whether clipboard is enabled. +- Add release notes that explain the trust boundary: InputFlow is for trusted LANs, not internet exposure. + +## Phase 2: Screen Topology And Wrap + +- Add a topology model that represents machines and individual displays separately. +- Add explicit wrap policies: `none`, `horizontal`, `vertical`, and `both`. +- Support edge-transition layouts that users call out as broken elsewhere: `AAB`, `BAA`, `ABA`, `BAB`, stacked displays, and asymmetric resolutions. +- Validate impossible layouts before saving them. +- Add tests for edge traversal, release/press preservation across transitions, and monitor-boundary ambiguity. + +## Phase 3: Layout Wizard + +- Add a visual layout wizard in the controller for Linux and Windows displays. +- Provide presets for common two-machine and three-screen layouts. +- Surface warnings for known protocol limitations before users start the service. +- Export the selected layout into the Windows helper flow when PowerToys settings need to be seeded. + +## Phase 4: Safe Windows Helper + +- Add dry-run output that shows exact PowerToys settings changes. +- Back up the PowerToys Mouse Without Borders settings file before writing. +- Provide a restore command or restore instructions in the generated helper. +- Do not stop browsers, VPNs, endpoint security, UPS utilities, or unrelated Windows processes. +- Keep the helper idempotent so users can rerun it after changing display topology. + +## Phase 5: Migration And Positioning + +- Add docs for users coming from Barrier, Input Leap, Deskflow, Synergy, Cursr, and Wine/MWB attempts. +- Explain that InputFlow is MWB-compatible, not Synergy-protocol compatible. +- Call out the design difference: InputFlow should prioritize MWB-style peer behavior, shared key setup, and wrap/topology handling over generic client-server KVM behavior. + +## Decision Gates + +- Do not claim competitor status or maintenance state without fresh primary-source verification. +- Do not add a new protocol mode unless MWB compatibility remains the default path. +- Do not ship topology changes without regression tests for the existing absolute cursor and reconnect behavior. +- Do not package auto-start behavior that activates remote input before a user creates config and opts in. + diff --git a/docs/research/cross-platform-kvm-feedback-prompt.md b/docs/research/cross-platform-kvm-feedback-prompt.md new file mode 100644 index 0000000..f4cf715 --- /dev/null +++ b/docs/research/cross-platform-kvm-feedback-prompt.md @@ -0,0 +1,51 @@ +# Research Prompt: Cross-Platform KVM Feedback And Compatibility + +Use this prompt for a focused research pass before implementing feedback-inspired features. + +## Objective + +Identify the user-visible failures and winning features across Linux/Windows keyboard-mouse sharing tools, then convert them into InputFlow requirements that preserve Microsoft PowerToys Mouse Without Borders compatibility. + +## Sources To Review + +- Official documentation and release notes for PowerToys Mouse Without Borders. +- Official repositories, docs, and issue trackers for Barrier, Input Leap, Deskflow, Synergy, and Cursr. +- Recent user feedback threads from Reddit, GitHub issues, forums, and distro communities. +- InputFlow local docs and source, especially `README.md`, `SECURITY.md`, `src/InputManager.cpp`, `src/ScreenGeometry.h`, `src/main.cpp`, and `mwb-desktop-ui.sh`. + +## Questions To Answer + +- Which tools currently support Windows plus Linux, and what protocol or architecture do they use? +- Which projects are maintained, deprecated, forked, or commercially supported as of the research date? +- What setup steps users praise or complain about? +- What multi-monitor layouts fail most often? +- How do tools model displays: per-machine rectangle, per-monitor topology, or explicit edge graph? +- Which tools support edge wrap, bidirectional traversal, and layouts like `B A B`? +- What clipboard formats are supported, and where do users report reliability problems? +- What installer or helper behaviors are considered unsafe or invasive? +- What security model is used: shared key, TLS/certificates, accounts/cloud, or unauthenticated LAN? + +## InputFlow Compatibility Requirements + +- Must remain compatible with PowerToys Mouse Without Borders on Windows. +- Must keep the existing AES-256-CBC MWB-compatible protocol unless a separate opt-in mode is designed. +- Must use ports `15101` for input and `15100` for clipboard unless PowerToys compatibility changes. +- Must support existing config keys and auth sources. +- Must preserve Linux input injection through `/dev/uinput`. +- Must work with the existing user systemd service and tray/controller workflow. +- Must not require installing a Windows service or stopping unrelated Windows applications. +- Must keep diagnostics redacted by default. + +## Deliverables + +- A dated evidence table with source links, project status, supported platforms, topology behavior, security model, and install friction. +- A ranked list of InputFlow feature candidates with implementation risk. +- A topology requirements document covering `AAB`, `BAA`, `ABA`, `BAB`, stacked displays, asymmetric resolutions, and wrap modes. +- A compatibility risk assessment for any feature that touches protocol, encryption, clipboard, input mapping, or Windows helper behavior. + +## Output Format + +- Keep direct quotes short and cite each source. +- Separate verified facts from inferred product recommendations. +- Flag unstable facts that need re-checking before release notes or marketing copy. + diff --git a/mwb-desktop-ui.sh b/mwb-desktop-ui.sh index 7f013c4..ac0c58d 100755 --- a/mwb-desktop-ui.sh +++ b/mwb-desktop-ui.sh @@ -13,6 +13,7 @@ RECONNECT_IDLE_CONFIG_KEY="${MWB_RECONNECT_IDLE_CONFIG_KEY:-reconnect_idle_retry MPRIS_MEDIA_KEYS_CONFIG_KEY="${MWB_MPRIS_MEDIA_KEYS_CONFIG_KEY:-mpris_media_keys_enabled}" MPRIS_PLAYER_CONFIG_KEY="${MWB_MPRIS_PLAYER_CONFIG_KEY:-mpris_player}" LATENCY_REPORT_CONFIG_KEY="${MWB_LATENCY_REPORT_CONFIG_KEY:-latency_report}" +DIAGNOSTICS_BUNDLE_SCRIPT="$SCRIPT_DIR/scripts/inputflow-diagnostics-bundle.sh" DEFAULT_AUTO_CONNECT_ENABLED="${MWB_DEFAULT_AUTO_CONNECT_ENABLED:-true}" DEFAULT_RECONNECT_INITIAL_MS="${MWB_DEFAULT_RECONNECT_INITIAL_MS:-1000}" DEFAULT_RECONNECT_MAX_MS="${MWB_DEFAULT_RECONNECT_MAX_MS:-300000}" @@ -680,7 +681,7 @@ menu_summary_text() { show_status() { local status_text doctor_text status_text="$(systemctl --user status --no-pager "$SERVICE_NAME" 2>&1 || true)" - doctor_text="$("$APP_BIN" doctor --config "$CONFIG_PATH" 2>&1 || true)" + doctor_text="$("$APP_BIN" doctor --config "$CONFIG_PATH" --state "$STATE_PATH" 2>&1 || true)" zenity --text-info --title="$APP_NAME service status" --width=900 --height=600 <<<"$doctor_text ---- @@ -688,6 +689,110 @@ show_status() { $status_text" } +append_check_line() { + local status="$1" name="$2" detail="$3" + printf '%-5s %-28s %s\n' "$status" "$name" "$detail" +} + +probe_tcp_port() { + local host="$1" port="$2" + MWB_PROBE_HOST="$host" MWB_PROBE_PORT="$port" timeout 2 bash -c ':/dev/null 2>&1 +} + +health_check() { + require_client_binary || return 1 + local host port key key_file secret_id auth_count service_status health_text doctor_text + host="$(read_config_value host)" + port="$(read_config_value port)"; [[ -n "$port" ]] || port="15101" + key="$(read_config_value key)" + key_file="$(read_config_value key_file)" + secret_id="$(read_secret_id_value)" + auth_count="$(configured_auth_source_count "$key" "$key_file" "$secret_id")" + service_status="$(service_state)" + + health_text="$( + append_check_line "$([[ -f "$CONFIG_PATH" ]] && printf OK || printf WARN)" "config file" "$CONFIG_PATH" + append_check_line "$([[ -e /dev/uinput ]] && printf OK || printf WARN)" "uinput device" "$([[ -e /dev/uinput ]] && ls -l /dev/uinput 2>/dev/null || printf 'missing; install udev rule and reload')" + append_check_line "$([[ "$service_status" == "active" ]] && printf OK || printf WARN)" "user service" "$(service_state_label "$service_status")" + append_check_line "$([[ -n "$host" ]] && printf OK || printf WARN)" "Windows host" "${host:-not configured}" + append_check_line "$([[ "$auth_count" == "1" ]] && printf OK || printf WARN)" "authentication" "$(configured_auth_label "$key" "$key_file" "$secret_id")" + if [[ -n "$host" ]] && command -v timeout >/dev/null 2>&1; then + if probe_tcp_port "$host" "$port"; then + append_check_line OK "input port" "$host:$port reachable" + else + append_check_line WARN "input port" "$host:$port not reachable" + fi + if probe_tcp_port "$host" "15100"; then + append_check_line OK "clipboard port" "$host:15100 reachable" + else + append_check_line WARN "clipboard port" "$host:15100 not reachable" + fi + else + append_check_line WARN "port probe" "host or timeout command unavailable" + fi + )" + doctor_text="$("$APP_BIN" doctor --config "$CONFIG_PATH" --state "$STATE_PATH" 2>&1 || true)" + zenity --text-info --title="$APP_NAME health check" --width=900 --height=620 <<<"$health_text + +---- +Client doctor +---- +$doctor_text" +} + +connection_quality() { + local host port state auto_connect_enabled reconnect_initial_backoff_ms reconnect_max_backoff_ms reconnect_idle_retry_ms + local clipboard_enabled clipboard_send_enabled clipboard_force_poll clipboard_poll_ms latency_report quality_text peer_lines + host="$(read_config_value host)" + port="$(read_config_value port)"; [[ -n "$port" ]] || port="15101" + state="$(service_state)" + IFS=$'\t' read -r auto_connect_enabled reconnect_initial_backoff_ms reconnect_max_backoff_ms reconnect_idle_retry_ms < <(read_connection_behavior_values) + clipboard_enabled="$(read_config_value clipboard_enabled)"; [[ -n "$clipboard_enabled" ]] || clipboard_enabled="true" + clipboard_send_enabled="$(read_config_value clipboard_send_enabled)"; [[ -n "$clipboard_send_enabled" ]] || clipboard_send_enabled="true" + clipboard_force_poll="$(read_config_value clipboard_force_poll)"; [[ -n "$clipboard_force_poll" ]] || clipboard_force_poll="false" + clipboard_poll_ms="$(read_config_value clipboard_poll_ms)"; [[ -n "$clipboard_poll_ms" ]] || clipboard_poll_ms="1000" + latency_report="$(read_config_value "$LATENCY_REPORT_CONFIG_KEY")"; [[ -n "$latency_report" ]] || latency_report="false" + + peer_lines="No peer state has been recorded yet." + if [[ -f "$STATE_PATH" ]]; then + peer_lines="$(awk -F'\t' ' + /^peer=/ { + sub(/^peer=/, "", $1) + name=$2; approved=$3; connected=$4; last_seen=$5; last_connected=$6 + if (name == "") name="unknown" + printf "- %s (%s): paired=%s connected=%s last_seen=%s last_connected=%s\n", name, $1, approved, connected, last_seen, last_connected + } + ' "$STATE_PATH")" + [[ -n "$peer_lines" ]] || peer_lines="No peer entries found in $STATE_PATH." + fi + + quality_text="Service: $(service_state_label "$state") ($state) +Configured host: ${host:-not configured} +Input port: $port +Clipboard port: 15100 +Reconnect mode: $( [[ "$auto_connect_enabled" == "true" ]] && printf 'Auto' || printf 'Manual' ) +Reconnect timing: initial=${reconnect_initial_backoff_ms}ms max=${reconnect_max_backoff_ms}ms idle=${reconnect_idle_retry_ms}ms +Clipboard: enabled=$clipboard_enabled send_local=$clipboard_send_enabled force_poll=$clipboard_force_poll poll=${clipboard_poll_ms}ms +Latency report logging: $latency_report + +Known peers: +$peer_lines" + zenity --text-info --title="$APP_NAME connection quality" --width=840 --height=520 <<<"$quality_text" +} + +diagnostics_bundle() { + if [[ ! -x "$DIAGNOSTICS_BUNDLE_SCRIPT" ]]; then + zenity --error --text="Diagnostics bundle script is not available: +$DIAGNOSTICS_BUNDLE_SCRIPT" + return 1 + fi + local output_dir result + output_dir="$(zenity --file-selection --directory --title="$APP_NAME diagnostics output folder" || true)" + [[ -n "$output_dir" ]] || return 1 + result="$("$DIAGNOSTICS_BUNDLE_SCRIPT" --config "$CONFIG_PATH" --state "$STATE_PATH" --output "$output_dir" 2>&1 || true)" + zenity --text-info --title="$APP_NAME diagnostics bundle" --width=760 --height=420 <<<"$result" +} + show_peers() { local rows=() configured_host selected_peer selected_host selected_port selected_name selected_action configured_host="$(read_config_value host)" @@ -829,6 +934,53 @@ discover_and_save_peer() { fi } +export_windows_helper() { + require_client_binary || return 1 + local output_dir position result + output_dir="$(zenity --file-selection --directory --title="$APP_NAME Windows helper output folder" || true)" + [[ -n "$output_dir" ]] || return 1 + position="$(zenity --list --radiolist --title="$APP_NAME Windows helper" --width=560 --height=260 \ + --text="Choose where the Linux desktop sits relative to the Windows host." \ + --column="Use" --column="Position" \ + TRUE "auto" \ + FALSE "top-left" \ + FALSE "top-right" \ + FALSE "bottom-left" \ + FALSE "bottom-right" || true)" + [[ -n "$position" ]] || return 1 + result="$("$APP_BIN" export-windows-pair --config "$CONFIG_PATH" --output "$output_dir" --position "$position" --force 2>&1 || true)" + zenity --text-info --title="$APP_NAME Windows pairing helper" --width=820 --height=440 <<<"$result + +Next steps: +1. Copy the exported .ps1 file to the Windows machine. +2. Start PowerToys once so Mouse Without Borders creates its settings file. +3. Run the helper in PowerShell. +4. Return here and run Health Check." +} + +guided_pairing() { + while true; do + local choice + choice="$(zenity --list --title="$APP_NAME guided pairing" --width=620 --height=360 \ + --text="Use this flow to discover Windows, save Linux settings, export the Windows helper, then verify the setup." \ + --column="Step" \ + "1. Discover Windows peer and save settings" \ + "2. Edit settings manually" \ + "3. Export Windows helper" \ + "4. Start service" \ + "5. Run health check" \ + "Back" || true)" + case "$choice" in + "1. Discover Windows peer and save settings") discover_and_save_peer ;; + "2. Edit settings manually") edit_settings ;; + "3. Export Windows helper") export_windows_helper ;; + "4. Start service") start_session ;; + "5. Run health check") health_check ;; + ""|"Back") return 0 ;; + esac + done +} + edit_settings() { local preset_host="${1:-}" local host key key_file secret_id secret_key_name machine_name port screen_width screen_height auto_connect_enabled reconnect_initial_backoff_ms reconnect_max_backoff_ms reconnect_idle_retry_ms clipboard_enabled clipboard_force_poll clipboard_poll_ms @@ -1045,7 +1197,23 @@ Terminal=false Categories=Utility;Network; Keywords=mouse;keyboard;sharing;input;controller; StartupNotify=false -Actions=OpenSettings;OpenConnectionBehavior;ShowTrayHelp;ShowStatus;StartService;RestartService;StopService; +Actions=GuidedPairing;HealthCheck;DiagnosticsBundle;ConnectionQuality;OpenSettings;OpenConnectionBehavior;ShowTrayHelp;ShowStatus;StartService;RestartService;StopService; + +[Desktop Action GuidedPairing] +Name=Guided Pairing +Exec=$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}") guided-pairing + +[Desktop Action HealthCheck] +Name=Health Check +Exec=$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}") health-check + +[Desktop Action DiagnosticsBundle] +Name=Diagnostics Bundle +Exec=$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}") diagnostics-bundle + +[Desktop Action ConnectionQuality] +Name=Connection Quality +Exec=$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}") connection-quality [Desktop Action OpenSettings] Name=Open Settings @@ -1102,6 +1270,10 @@ main_menu() { local choice choice="$(zenity --list --title="$APP_NAME" --text="$(menu_summary_text)" --width=540 --height=400 \ --column="Action" \ + "Guided Pairing" \ + "Health Check" \ + "Diagnostics Bundle" \ + "Connection Quality" \ "Settings" \ "Peers (Discovery & Known)" \ "Connection Behavior" \ @@ -1114,6 +1286,10 @@ main_menu() { "Quit" || true)" case "$choice" in + "Guided Pairing") guided_pairing ;; + "Health Check") health_check ;; + "Diagnostics Bundle") diagnostics_bundle ;; + "Connection Quality") connection_quality ;; "Settings") edit_settings ;; "Peers (Discovery & Known)") local peer_choice @@ -1138,6 +1314,10 @@ require_ui case "${1:-menu}" in ""|menu) main_menu ;; + guided-pairing|pairing|export-helper) guided_pairing ;; + health-check|doctor) health_check ;; + diagnostics-bundle|diagnostics) diagnostics_bundle ;; + connection-quality|quality) connection_quality ;; settings) edit_settings ;; connection|connection-behavior|reconnect) edit_connection_behavior ;; discover) discover_and_save_peer ;; @@ -1150,7 +1330,7 @@ case "${1:-menu}" in tray) start_tray ;; install-desktop-entry|install-desktop-entries) install_desktop_entry ;; help|-h|--help) - printf 'Usage: %s [menu|settings|connection|discover|peers|tray-help|status|start|restart|stop|tray|install-desktop-entry]\n' "$(basename "${BASH_SOURCE[0]}")" + printf 'Usage: %s [menu|guided-pairing|health-check|diagnostics-bundle|connection-quality|settings|connection|discover|peers|tray-help|status|start|restart|stop|tray|install-desktop-entry]\n' "$(basename "${BASH_SOURCE[0]}")" ;; *) zenity --error --text="Unknown action: $1" diff --git a/packaging/README.md b/packaging/README.md index 6ba13bd..2a9a4f4 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -17,6 +17,11 @@ packagers can copy files directly or map them to distro macros. - `usr/lib/systemd/user/mwb-client.service` Runs `mwb_client` from a user systemd manager with `%h/.config/mwb-client/config.ini`. +- `usr/share/applications/inputflow.desktop` + Launches the packaged desktop controller from `/usr/libexec/inputflow`. +- `../scripts/inputflow-diagnostics-bundle.sh` + Canonical diagnostics bundle helper; the RPM installs it under + `/usr/libexec/inputflow/scripts/`. - `rpm/inputflow.spec` Fedora/RPM packaging skeleton for the InputFlow package. It intentionally keeps the installed command and user service named `mwb_client` and @@ -30,6 +35,10 @@ Install the files to the standard macro destinations: - `%{_modulesloaddir}/mwb-client-uinput.conf` - `%{_udevrulesdir}/70-mwb-client-uinput.rules` - `%{_userunitdir}/mwb-client.service` +- `%{_libexecdir}/inputflow/mwb-desktop-ui.sh` +- `%{_libexecdir}/inputflow/scripts/inputflow-diagnostics-bundle.sh` +- `%{_datadir}/applications/inputflow.desktop` +- `%{_datadir}/icons/hicolor/scalable/apps/inputflow.svg` Create the group with systemd-sysusers from package scriptlets/macros. Reload udev rules after installing the rule, and load or trigger `uinput` if the package wants @@ -80,6 +89,10 @@ The packaged user unit is intentionally configuration-gated with `ConditionPathExists=%h/.config/mwb-client/config.ini`; an enabled service will be skipped until the user creates that file. +The desktop entry is a launcher only; it does not enable or start services during +package installation. The diagnostics bundle script redacts likely secret, host, +and address values before archiving config or state files. + Firewall rules are normally unnecessary for the Linux client when it only initiates outbound connections to a Windows host. SELinux policy should be kept distro-owned; do not disable SELinux or apply broad device labels for this service unless a diff --git a/packaging/rpm/inputflow.spec b/packaging/rpm/inputflow.spec index c45afa2..e8c917d 100644 --- a/packaging/rpm/inputflow.spec +++ b/packaging/rpm/inputflow.spec @@ -23,6 +23,8 @@ Requires(pre): systemd Requires(post): systemd-udev Requires(postun): systemd-udev Recommends: wl-clipboard +Recommends: zenity +Recommends: python3-gobject %description InputFlow is a Linux companion client for Microsoft PowerToys Mouse @@ -40,10 +42,14 @@ retain the mwb-client naming for compatibility. %cmake_build --target mwb_client mwb_tray install -Dpm0755 %{_vpath_builddir}/mwb_client %{buildroot}%{_bindir}/mwb_client install -Dpm0755 %{_vpath_builddir}/mwb_tray %{buildroot}%{_bindir}/mwb_tray +install -Dpm0755 mwb-desktop-ui.sh %{buildroot}%{_libexecdir}/inputflow/mwb-desktop-ui.sh +install -Dpm0755 scripts/inputflow-diagnostics-bundle.sh %{buildroot}%{_libexecdir}/inputflow/scripts/inputflow-diagnostics-bundle.sh install -Dpm0644 packaging/usr/lib/sysusers.d/mwb-client.conf %{buildroot}%{_sysusersdir}/mwb-client.conf install -Dpm0644 packaging/usr/lib/modules-load.d/mwb-client-uinput.conf %{buildroot}%{_modulesloaddir}/mwb-client-uinput.conf install -Dpm0644 packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules %{buildroot}%{_udevrulesdir}/70-mwb-client-uinput.rules install -Dpm0644 packaging/usr/lib/systemd/user/mwb-client.service %{buildroot}%{_userunitdir}/mwb-client.service +install -Dpm0644 packaging/usr/share/applications/inputflow.desktop %{buildroot}%{_datadir}/applications/inputflow.desktop +install -Dpm0644 assets/icons/inputflow-desktop.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/inputflow.svg %pre %sysusers_create %{_sysusersdir}/mwb-client.conf @@ -66,10 +72,14 @@ udevadm trigger --name-match=uinput || : %doc README.md packaging/README.md %{_bindir}/mwb_client %{_bindir}/mwb_tray +%{_libexecdir}/inputflow/mwb-desktop-ui.sh +%{_libexecdir}/inputflow/scripts/inputflow-diagnostics-bundle.sh %{_sysusersdir}/mwb-client.conf %{_modulesloaddir}/mwb-client-uinput.conf %{_udevrulesdir}/70-mwb-client-uinput.rules %{_userunitdir}/mwb-client.service +%{_datadir}/applications/inputflow.desktop +%{_datadir}/icons/hicolor/scalable/apps/inputflow.svg %changelog * Fri Apr 24 2026 InputFlow Maintainers - 0.1.0-1 diff --git a/packaging/usr/share/applications/inputflow.desktop b/packaging/usr/share/applications/inputflow.desktop new file mode 100644 index 0000000..2ec9e80 --- /dev/null +++ b/packaging/usr/share/applications/inputflow.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=InputFlow Controller +Comment=Configure and monitor InputFlow +Exec=/usr/libexec/inputflow/mwb-desktop-ui.sh menu +Icon=inputflow +Terminal=false +Categories=Utility;RemoteAccess; +StartupNotify=false diff --git a/scripts/inputflow-diagnostics-bundle.sh b/scripts/inputflow-diagnostics-bundle.sh new file mode 100755 index 0000000..60d64e4 --- /dev/null +++ b/scripts/inputflow-diagnostics-bundle.sh @@ -0,0 +1,354 @@ +#!/usr/bin/env bash +set -u +set -o pipefail + +SCRIPT_NAME="$(basename "$0")" + +usage() { + cat <&2 +} + +die() { + log "error: $*" + exit 1 +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +default_config_path() { + if [[ -n "${XDG_CONFIG_HOME:-}" ]]; then + printf '%s\n' "$XDG_CONFIG_HOME/mwb-client/config.ini" + elif [[ -n "${HOME:-}" ]]; then + printf '%s\n' "$HOME/.config/mwb-client/config.ini" + else + printf '%s\n' "mwb-client/config.ini" + fi +} + +default_state_path() { + if [[ -n "${XDG_STATE_HOME:-}" ]]; then + printf '%s\n' "$XDG_STATE_HOME/mwb-client/state.ini" + elif [[ -n "${HOME:-}" ]]; then + printf '%s\n' "$HOME/.local/state/mwb-client/state.ini" + else + printf '%s\n' "mwb-client/state.ini" + fi +} + +CONFIG_PATH="$(default_config_path)" +STATE_PATH="$(default_state_path)" +OUTPUT_DIR="." + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + [[ $# -ge 2 ]] || die "--config requires a path" + CONFIG_PATH="$2" + shift 2 + ;; + --state) + [[ $# -ge 2 ]] || die "--state requires a path" + STATE_PATH="$2" + shift 2 + ;; + --output) + [[ $# -ge 2 ]] || die "--output requires a directory" + OUTPUT_DIR="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +mkdir -p "$OUTPUT_DIR" || die "failed to create output directory: $OUTPUT_DIR" +OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd -P)" || die "failed to resolve output directory: $OUTPUT_DIR" + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +BUNDLE_NAME="inputflow-diagnostics-${TIMESTAMP}-$$" +BUNDLE_DIR="$OUTPUT_DIR/$BUNDLE_NAME" +mkdir -p "$BUNDLE_DIR" || die "failed to create bundle directory: $BUNDLE_DIR" +chmod 700 "$BUNDLE_DIR" 2>/dev/null || true + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P 2>/dev/null || pwd -P)" +CONFIG_PATH_DISPLAY="$CONFIG_PATH" +STATE_PATH_DISPLAY="$STATE_PATH" + +redact_stream() { + sed -E \ + -e 's/^([[:space:]]*[^#;[:space:]]*(key|token|secret|password|passphrase)[^=]*[[:space:]]*=[[:space:]]*).*/\1[REDACTED]/I' \ + -e 's/([[:space:]]--?[^[:space:]]*(key|token|secret|password|passphrase)[^[:space:]=]*[=[:space:]]+)[^[:space:]]+/\1[REDACTED]/Ig' \ + -e 's/([A-Za-z_][A-Za-z0-9_]*(KEY|TOKEN|SECRET|PASSWORD|PASSPHRASE)[A-Za-z0-9_]*=)[^[:space:]]+/\1[REDACTED]/Ig' \ + -e "s/((secret|key|token|password|passphrase)( service)? id[[:space:]]+'?)[^'[:space:]]+('?)/\1[REDACTED]\4/Ig" \ + -e 's/(|SECURITY_KEY|security key)[^[:space:]]*/\1[REDACTED]/Ig' \ + -e 's/[A-Fa-f0-9]{32,}/[REDACTED_HEX]/g' +} + +safe_run() { + local output_file="$1" + shift + { + printf '$' + printf ' %q' "$@" + printf '\n\n' + "$@" + local status=$? + printf '\n[exit status: %s]\n' "$status" + } 2>&1 | redact_stream >"$output_file" +} + +safe_shell() { + local output_file="$1" + local description="$2" + local script="$3" + { + printf '$ %s\n\n' "$description" + bash -c "$script" + local status=$? + printf '\n[exit status: %s]\n' "$status" + } 2>&1 | redact_stream >"$output_file" +} + +redacted_copy_or_note() { + local source_file="$1" + local output_file="$2" + { + printf 'source=%s\n' "$source_file" + if [[ -f "$source_file" && -r "$source_file" ]]; then + printf 'present=yes\n\n' + redact_stream <"$source_file" + elif [[ -e "$source_file" ]]; then + printf 'present=yes\nreadable=no\n' + else + printf 'present=no\n' + fi + } >"$output_file" +} + +write_config_summary() { + local output_file="$1" + { + printf 'config_path=%s\n' "$CONFIG_PATH_DISPLAY" + if [[ ! -e "$CONFIG_PATH" ]]; then + printf 'present=no\n' + return + fi + printf 'present=yes\n' + if [[ -f "$CONFIG_PATH" ]]; then + stat -c 'mode=%A +owner=%U:%G +size_bytes=%s +modified=%y' "$CONFIG_PATH" 2>/dev/null || true + fi + if [[ ! -r "$CONFIG_PATH" ]]; then + printf 'readable=no\n' + return + fi + printf 'readable=yes\n\n[redacted values]\n' + local line trimmed key value + while IFS= read -r line || [[ -n "$line" ]]; do + trimmed="${line#"${line%%[![:space:]]*}"}" + trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" + [[ -z "$trimmed" || "$trimmed" == \#* || "$trimmed" == \;* ]] && continue + if [[ "$trimmed" != *"="* ]]; then + printf '%s\n' "$trimmed" | redact_stream + continue + fi + key="${trimmed%%=*}" + value="${trimmed#*=}" + key="${key%"${key##*[![:space:]]}"}" + value="${value#"${value%%[![:space:]]*}"}" + if [[ "$key" =~ [Kk][Ee][Yy]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss] ]]; then + printf '%s=[REDACTED]\n' "$key" + else + printf '%s=%s\n' "$key" "$value" | redact_stream + fi + done <"$CONFIG_PATH" + } >"$output_file" +} + +write_state_summary() { + local output_file="$1" + { + printf 'state_path=%s\n' "$STATE_PATH_DISPLAY" + if [[ ! -e "$STATE_PATH" ]]; then + printf 'present=no\n' + return + fi + printf 'present=yes\n' + stat -c 'mode=%A +owner=%U:%G +size_bytes=%s +modified=%y' "$STATE_PATH" 2>/dev/null || true + if [[ ! -r "$STATE_PATH" ]]; then + printf 'readable=no\n' + return + fi + printf 'readable=yes\n' + printf 'peer_lines=%s\n' "$(grep -c '^peer=' "$STATE_PATH" 2>/dev/null || printf '0')" + printf '\n[redacted state]\n' + redact_stream <"$STATE_PATH" + } >"$output_file" +} + +write_manifest() { + cat >"$BUNDLE_DIR/README.txt" </dev/null || date) +Host: $(hostname 2>/dev/null || printf 'unknown') +User: $(id -un 2>/dev/null || printf 'unknown') +Repository: $REPO_ROOT + +This bundle is redacted by best effort before files are written. Security keys, +tokens, passwords, key file values, secret IDs, and long hex strings are replaced. +Review before sharing outside trusted support channels. +EOF +} + +write_manifest +write_config_summary "$BUNDLE_DIR/config-summary.txt" +write_state_summary "$BUNDLE_DIR/app-state.txt" + +safe_shell "$BUNDLE_DIR/os-session.txt" "collect OS and session info" ' +set +e +date -Is 2>/dev/null || date +uname -a +printf "\n[/etc/os-release]\n" +test -r /etc/os-release && cat /etc/os-release +printf "\n[session]\n" +id +printf "SHELL=%s\nUSER=%s\nLOGNAME=%s\nXDG_SESSION_TYPE=%s\nXDG_CURRENT_DESKTOP=%s\nDESKTOP_SESSION=%s\nDISPLAY=%s\nWAYLAND_DISPLAY=%s\n" "${SHELL:-}" "${USER:-}" "${LOGNAME:-}" "${XDG_SESSION_TYPE:-}" "${XDG_CURRENT_DESKTOP:-}" "${DESKTOP_SESSION:-}" "${DISPLAY:-}" "${WAYLAND_DISPLAY:-}" +if command -v loginctl >/dev/null 2>&1 && test -n "${XDG_SESSION_ID:-}"; then + loginctl show-session "$XDG_SESSION_ID" --no-pager 2>/dev/null +fi +' + +safe_shell "$BUNDLE_DIR/systemd-user-status.txt" "collect systemd user service status" ' +set +e +if ! command -v systemctl >/dev/null 2>&1; then + echo "systemctl not found" + exit 0 +fi +systemctl --user --no-pager status mwb-client.service inputflow.service 2>&1 +printf "\n[known matching units]\n" +systemctl --user --no-pager list-units "mwb*" "inputflow*" 2>&1 +printf "\n[unit files]\n" +systemctl --user --no-pager list-unit-files "mwb*" "inputflow*" 2>&1 +' + +safe_shell "$BUNDLE_DIR/journal-user-recent.txt" "collect recent user journal logs" ' +set +e +if ! command -v journalctl >/dev/null 2>&1; then + echo "journalctl not found" + exit 0 +fi +journalctl --user --no-pager --since "2 hours ago" -u mwb-client.service -u inputflow.service -n 300 2>&1 +' + +safe_shell "$BUNDLE_DIR/uinput-state.txt" "collect uinput state" ' +set +e +printf "[device]\n" +ls -l /dev/uinput 2>&1 +stat /dev/uinput 2>&1 +printf "\n[user groups]\n" +id +printf "\n[modules]\n" +grep "^uinput " /proc/modules 2>/dev/null || true +lsmod 2>/dev/null | grep -E "^uinput|uinput" || true +printf "\n[udev rules]\n" +find /etc/udev/rules.d /usr/lib/udev/rules.d /lib/udev/rules.d -maxdepth 1 \( -iname "*uinput*" -o -iname "*inputflow*" -o -iname "*mwb*" \) -print -exec sh -c '"'"'for f; do echo "--- $f"; sed -n "1,120p" "$f"; done'"'"' sh {} + 2>/dev/null +' + +safe_shell "$BUNDLE_DIR/network-hints.txt" "collect network hints" ' +set +e +hostnamectl 2>/dev/null || hostname 2>/dev/null +printf "\n[addresses]\n" +ip -brief address 2>&1 +printf "\n[links]\n" +ip -brief link 2>&1 +printf "\n[routes]\n" +ip route 2>&1 +printf "\n[listening/inputflow sockets]\n" +if command -v ss >/dev/null 2>&1; then + ss -lntup 2>/dev/null | grep -Ei "15101|mwb|inputflow|State|Netid" || true +else + echo "ss not found" +fi +printf "\n[host resolution]\n" +getent hosts "$(hostname)" 2>/dev/null || true +' + +safe_shell "$BUNDLE_DIR/package-build-info.txt" "collect package and build info" " +set +e +printf '[repository]\\n' +if command -v git >/dev/null 2>&1 && git -C '$REPO_ROOT' rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C '$REPO_ROOT' rev-parse --show-toplevel + git -C '$REPO_ROOT' rev-parse --short HEAD + git -C '$REPO_ROOT' status --short +fi +printf '\\n[build artifacts]\\n' +for f in '$REPO_ROOT/build/mwb_client' '$REPO_ROOT/build/mwb_tray'; do + if test -e \"\$f\"; then + ls -l \"\$f\" + file \"\$f\" 2>/dev/null || true + \"\$f\" --version 2>&1 || true + else + echo \"missing: \$f\" + fi +done +printf '\\n[cmake cache summary]\\n' +if test -r '$REPO_ROOT/build/CMakeCache.txt'; then + grep -E '^(CMAKE_BUILD_TYPE|CMAKE_PROJECT_VERSION|MWB_|CMAKE_CXX_COMPILER|CMAKE_CXX_FLAGS)' '$REPO_ROOT/build/CMakeCache.txt' 2>/dev/null || true +fi +printf '\\n[installed packages]\\n' +if command -v dpkg-query >/dev/null 2>&1; then + dpkg-query -W 'mwb*' 'inputflow*' 2>/dev/null || true +fi +if command -v rpm >/dev/null 2>&1; then + rpm -qa 2>/dev/null | grep -Ei 'mwb|inputflow' || true +fi +" + +if [[ -x "$REPO_ROOT/build/mwb_client" ]]; then + safe_run "$BUNDLE_DIR/mwb-client-doctor.txt" "$REPO_ROOT/build/mwb_client" doctor --config "$CONFIG_PATH" --state "$STATE_PATH" +elif command -v mwb_client >/dev/null 2>&1; then + safe_run "$BUNDLE_DIR/mwb-client-doctor.txt" mwb_client doctor --config "$CONFIG_PATH" --state "$STATE_PATH" +else + printf 'mwb_client executable not found at %s or in PATH\n' "$REPO_ROOT/build/mwb_client" >"$BUNDLE_DIR/mwb-client-doctor.txt" +fi + +FINAL_PATH="$BUNDLE_DIR" +if have tar; then + ARCHIVE_PATH="$OUTPUT_DIR/$BUNDLE_NAME.tar.gz" + if tar -czf "$ARCHIVE_PATH" -C "$OUTPUT_DIR" "$BUNDLE_NAME" >/dev/null 2>&1; then + rm -rf "$BUNDLE_DIR" + FINAL_PATH="$ARCHIVE_PATH" + else + log "warning: tar failed; leaving bundle directory unarchived" + fi +else + log "warning: tar not found; leaving bundle directory unarchived" +fi + +printf '%s\n' "$FINAL_PATH" diff --git a/scripts/validate-rpm-packaging.sh b/scripts/validate-rpm-packaging.sh index 9f95739..1a0bb09 100755 --- a/scripts/validate-rpm-packaging.sh +++ b/scripts/validate-rpm-packaging.sh @@ -6,11 +6,15 @@ cleanup_paths=() required_files=( "README.md" "LICENSE" + "mwb-desktop-ui.sh" + "assets/icons/inputflow-desktop.svg" "packaging/README.md" "packaging/usr/lib/sysusers.d/mwb-client.conf" "packaging/usr/lib/modules-load.d/mwb-client-uinput.conf" "packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules" "packaging/usr/lib/systemd/user/mwb-client.service" + "packaging/usr/share/applications/inputflow.desktop" + "scripts/inputflow-diagnostics-bundle.sh" ) fail() { @@ -67,14 +71,43 @@ grep -Eq 'KERNEL=="uinput"' packaging/usr/lib/udev/rules.d/70-mwb-client-uinput. grep -Eq 'GROUP="inputflow"' packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules || fail "udev rule must use inputflow group" grep -Eq 'MODE="0660"' packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules || fail "udev rule must set mode 0660" grep -Eq 'static_node=uinput' packaging/usr/lib/udev/rules.d/70-mwb-client-uinput.rules || fail "udev rule must keep a stable uinput node" +grep -Eq '^uinput$' packaging/usr/lib/modules-load.d/mwb-client-uinput.conf || fail "modules-load file must load uinput" +grep -Eq '^ConditionPathExists=%h/\.config/mwb-client/config\.ini$' packaging/usr/lib/systemd/user/mwb-client.service || fail "user service must be gated by config presence" +grep -Eq '^ExecStart=/usr/bin/mwb_client run --config %h/\.config/mwb-client/config\.ini$' packaging/usr/lib/systemd/user/mwb-client.service || fail "user service must run packaged mwb_client with the gated config" +grep -Eq '^NoNewPrivileges=true$' packaging/usr/lib/systemd/user/mwb-client.service || fail "user service must keep NoNewPrivileges hardening" +grep -Eq '^Type=Application$' packaging/usr/share/applications/inputflow.desktop || fail "desktop entry must be an application" +grep -Eq '^Name=InputFlow Controller$' packaging/usr/share/applications/inputflow.desktop || fail "desktop entry must expose InputFlow Controller" +grep -Eq '^Exec=/usr/libexec/inputflow/mwb-desktop-ui\.sh menu$' packaging/usr/share/applications/inputflow.desktop || fail "desktop entry must launch the packaged controller" +grep -Eq '^Icon=inputflow$' packaging/usr/share/applications/inputflow.desktop || fail "desktop entry must use the packaged inputflow icon name" +grep -Eq '^Terminal=false$' packaging/usr/share/applications/inputflow.desktop || fail "desktop entry must not require a terminal" +grep -Eq '^DIAGNOSTICS_BUNDLE_SCRIPT="\$SCRIPT_DIR/scripts/inputflow-diagnostics-bundle\.sh"$' mwb-desktop-ui.sh || fail "desktop UI must resolve diagnostics bundle next to the packaged controller" +bash -n scripts/inputflow-diagnostics-bundle.sh || fail "diagnostics bundle script must pass bash syntax checks" +grep -Eq 'doctor --config' scripts/inputflow-diagnostics-bundle.sh || fail "diagnostics bundle must collect client doctor output" +grep -Eq 'systemctl --user --no-pager status mwb-client\.service' scripts/inputflow-diagnostics-bundle.sh || fail "diagnostics bundle must collect user service status" +grep -Eq 'redacted' scripts/inputflow-diagnostics-bundle.sh || fail "diagnostics bundle must redact config/state content" grep -Eq '%\{_bindir\}/mwb_client' "$spec_file" || fail "spec must package mwb_client" +grep -Eq '%\{_bindir\}/mwb_tray' "$spec_file" || fail "spec must package mwb_tray" +grep -Eq '%\{_libexecdir\}/inputflow/mwb-desktop-ui\.sh' "$spec_file" || fail "spec must package the desktop controller" +grep -Eq '%\{_libexecdir\}/inputflow/scripts/inputflow-diagnostics-bundle\.sh' "$spec_file" || fail "spec must package diagnostics bundle script" grep -Eq '%\{_userunitdir\}/mwb-client\.service' "$spec_file" || fail "spec must package mwb-client.service" grep -Eq '%\{_sysusersdir\}/mwb-client\.conf' "$spec_file" || fail "spec must package sysusers integration" grep -Eq '%\{_udevrulesdir\}/70-mwb-client-uinput\.rules' "$spec_file" || fail "spec must package udev rule" +grep -Eq '%\{_datadir\}/applications/inputflow\.desktop' "$spec_file" || fail "spec must package desktop entry" +grep -Eq '%\{_datadir\}/icons/hicolor/scalable/apps/inputflow\.svg' "$spec_file" || fail "spec must package application icon" grep -Eq '%sysusers_create[[:space:]]+%\{_sysusersdir\}/mwb-client\.conf' "$spec_file" || fail "spec must create the inputflow group via systemd-sysusers" grep -Eq '%systemd_user_post[[:space:]]+mwb-client\.service' "$spec_file" || fail "spec must run user-unit post macro" +grep -Eq '%systemd_user_preun[[:space:]]+mwb-client\.service' "$spec_file" || fail "spec must run user-unit preun macro" +grep -Eq '%systemd_user_postun_with_restart[[:space:]]+mwb-client\.service' "$spec_file" || fail "spec must run user-unit postun macro" grep -Eq 'udevadm control --reload-rules' "$spec_file" || fail "spec must reload udev rules" grep -Eq 'udevadm trigger --name-match=uinput' "$spec_file" || fail "spec must retrigger /dev/uinput" +grep -Eq 'install -Dpm0755 mwb-desktop-ui\.sh .+%\{_libexecdir\}/inputflow/mwb-desktop-ui\.sh' "$spec_file" || fail "spec must install the desktop controller executable" +grep -Eq 'install -Dpm0755 scripts/inputflow-diagnostics-bundle\.sh .+%\{_libexecdir\}/inputflow/scripts/inputflow-diagnostics-bundle\.sh' "$spec_file" || fail "spec must install the diagnostics bundle executable" +grep -Eq 'install -Dpm0644 packaging/usr/share/applications/inputflow\.desktop .+%\{_datadir\}/applications/inputflow\.desktop' "$spec_file" || fail "spec must install the desktop entry" +grep -Eq 'install -Dpm0644 assets/icons/inputflow-desktop\.svg .+%\{_datadir\}/icons/hicolor/scalable/apps/inputflow\.svg' "$spec_file" || fail "spec must install the application icon" + +if command -v desktop-file-validate >/dev/null 2>&1; then + run_quiet "desktop-file-validate" desktop-file-validate packaging/usr/share/applications/inputflow.desktop +fi if grep -Eq '/usr/bin/inputflow|inputflow-client\.service|inputflow\.service' "$spec_file"; then fail "spec introduced an inputflow command or service alias before the documented compatibility pass" diff --git a/src/TrayController.cpp b/src/TrayController.cpp index 4334e23..5615158 100644 --- a/src/TrayController.cpp +++ b/src/TrayController.cpp @@ -31,6 +31,10 @@ struct TrayContext { GtkWidget* openControllerItem{nullptr}; GtkWidget* editSettingsItem{nullptr}; GtkWidget* editConnectionItem{nullptr}; + GtkWidget* healthCheckItem{nullptr}; + GtkWidget* diagnosticsBundleItem{nullptr}; + GtkWidget* connectionQualityItem{nullptr}; + GtkWidget* guidedPairingItem{nullptr}; GtkWidget* discoverPeersItem{nullptr}; GtkWidget* showPeersItem{nullptr}; GtkWidget* trayHelpItem{nullptr}; @@ -392,6 +396,10 @@ void UpdateIndicatorVisuals(TrayContext* context, const std::string& state) { const bool controllerAvailable = !context->controllerPath.empty(); gtk_widget_set_sensitive(context->editSettingsItem, controllerAvailable); gtk_widget_set_sensitive(context->editConnectionItem, controllerAvailable); + gtk_widget_set_sensitive(context->healthCheckItem, controllerAvailable); + gtk_widget_set_sensitive(context->diagnosticsBundleItem, controllerAvailable); + gtk_widget_set_sensitive(context->connectionQualityItem, controllerAvailable); + gtk_widget_set_sensitive(context->guidedPairingItem, controllerAvailable); gtk_widget_set_sensitive(context->discoverPeersItem, controllerAvailable); gtk_widget_set_sensitive(context->showPeersItem, controllerAvailable); gtk_widget_set_sensitive(context->trayHelpItem, TRUE); @@ -477,6 +485,26 @@ void OnEditConnectionBehavior(GtkMenuItem*, gpointer userData) { (void)LaunchController(context, {"connection"}); } +void OnHealthCheck(GtkMenuItem*, gpointer userData) { + auto* context = static_cast(userData); + (void)LaunchController(context, {"health-check"}); +} + +void OnDiagnosticsBundle(GtkMenuItem*, gpointer userData) { + auto* context = static_cast(userData); + (void)LaunchController(context, {"diagnostics-bundle"}); +} + +void OnConnectionQuality(GtkMenuItem*, gpointer userData) { + auto* context = static_cast(userData); + (void)LaunchController(context, {"connection-quality"}); +} + +void OnGuidedPairing(GtkMenuItem*, gpointer userData) { + auto* context = static_cast(userData); + (void)LaunchController(context, {"guided-pairing"}); +} + void OnDiscoverPeers(GtkMenuItem*, gpointer userData) { auto* context = static_cast(userData); (void)LaunchController(context, {"discover"}); @@ -553,6 +581,10 @@ int main(int argc, char** argv) { context.editSettingsItem = AddMenuItem(menu, "Settings", G_CALLBACK(OnEditSettings), &context); context.editConnectionItem = AddMenuItem(menu, "Connection Behavior", G_CALLBACK(OnEditConnectionBehavior), &context); + context.healthCheckItem = AddMenuItem(menu, "Health Check", G_CALLBACK(OnHealthCheck), &context); + context.diagnosticsBundleItem = AddMenuItem(menu, "Diagnostics Bundle", G_CALLBACK(OnDiagnosticsBundle), &context); + context.connectionQualityItem = AddMenuItem(menu, "Connection Quality", G_CALLBACK(OnConnectionQuality), &context); + context.guidedPairingItem = AddMenuItem(menu, "Guided Pairing", G_CALLBACK(OnGuidedPairing), &context); context.discoverPeersItem = AddMenuItem(menu, "Discover Peers", G_CALLBACK(OnDiscoverPeers), &context); context.showPeersItem = AddMenuItem(menu, "Known Peers", G_CALLBACK(OnShowPeers), &context); diff --git a/src/main.cpp b/src/main.cpp index 8cd7e29..8566222 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,7 +57,7 @@ void PrintGeneralUsage(std::ostream& out, const char* argv0) { << " [--reconnect-initial-backoff-ms MS] [--reconnect-max-backoff-ms MS] [--reconnect-idle-retry-ms MS]" << " [--latency-report]\n"; out << " " << binary << " discover [--state PATH] [--port PORT] [--timeout-ms MS] [--max-hosts N]\n"; - out << " " << binary << " doctor [--config PATH]\n"; + out << " " << binary << " doctor [--config PATH] [--state PATH]\n"; out << " " << binary << " init-config [--config PATH] [--force] [--host IP] [--key KEY | --key-file PATH | --key-secret-id ID] [--name NAME] [--port PORT]\n"; out << " " << binary << " export-windows-pair [--config PATH] [--output PATH] [--force] [--linux-ip IP] [--position auto|top-left|top-right|bottom-left|bottom-right] [--key KEY | --key-file PATH | --key-secret-id ID] [--name NAME]\n"; out << " " << binary << " install-user-service [--config PATH] [--unit PATH] [--force]\n"; @@ -949,6 +949,18 @@ std::int64_t CurrentEpochSeconds() { .count(); } +std::filesystem::path ResolveRelativeTo(const std::filesystem::path& path, + const std::filesystem::path& baseDirectory) { + if (path.empty() || path.is_absolute()) { + return path; + } + return baseDirectory / path; +} + +std::string BoolToken(bool value) { + return value ? "yes" : "no"; +} + bool LoadOptionalState(const std::filesystem::path& statePath, mwb::AppState& state) { if (!std::filesystem::exists(statePath)) { return true; @@ -971,6 +983,116 @@ bool SaveStateOrReport(const std::filesystem::path& statePath, const mwb::AppSta return true; } +bool FileContainsNonWhitespace(const std::filesystem::path& path) { + std::ifstream file(path); + if (!file) { + return false; + } + + char ch = '\0'; + while (file.get(ch)) { + if (std::isspace(static_cast(ch)) == 0) { + return true; + } + } + return false; +} + +void PrintDoctorAuthKeyPresence(const mwb::AppConfig& config, + const std::filesystem::path& configPath) { + if (!config.keySecretId.empty()) { + PrintDoctorLine("OK", "auth key", "key_secret_id configured"); + return; + } + if (!config.keyFile.empty()) { + const std::filesystem::path keyPath = ResolveRelativeTo(config.keyFile, configPath.parent_path()); + if (!std::filesystem::exists(keyPath)) { + PrintDoctorLine("WARN", "auth key", "key_file missing: " + keyPath.string()); + } else if (!FileContainsNonWhitespace(keyPath)) { + PrintDoctorLine("WARN", "auth key", "key_file empty or unreadable: " + keyPath.string()); + } else { + PrintDoctorLine("OK", "auth key", "key_file present: " + keyPath.string()); + } + return; + } + if (!config.key.empty()) { + PrintDoctorLine("OK", "auth key", "inline key configured"); + return; + } + PrintDoctorLine("WARN", "auth key", "not configured"); +} + +void PrintDoctorHostProbe(const mwb::AppConfig& config, int port) { + const std::string label = "host " + std::to_string(port); + if (config.host.empty()) { + PrintDoctorLine("INFO", label, "not checked; host not configured"); + return; + } + + const auto reachable = ProbeReachableIpv4Host(config.host, port, 500); + PrintDoctorLine(reachable.has_value() ? "OK" : "WARN", label, + reachable.has_value() + ? "tcp reachable ip=" + *reachable + : "tcp unreachable host=" + config.host); +} + +const mwb::PeerState* FindPeerForQuality(const mwb::AppState& state, + const std::string& host, + int port) { + const mwb::PeerState* hostOnlyMatch = nullptr; + for (const auto& peer : state.peers) { + if (peer.host != host) { + continue; + } + if (peer.port == port) { + return &peer; + } + if (hostOnlyMatch == nullptr) { + hostOnlyMatch = &peer; + } + } + return hostOnlyMatch; +} + +void PrintDoctorConnectionQuality(const mwb::AppConfig& config, + const std::filesystem::path& statePath) { + if (!std::filesystem::exists(statePath)) { + PrintDoctorLine("INFO", "state", statePath.string() + " does not exist"); + PrintDoctorLine("INFO", "connection quality", "state=missing"); + return; + } + + mwb::AppState state; + std::string error; + if (!mwb::LoadAppState(statePath, state, error)) { + PrintDoctorLine("WARN", "state", error); + PrintDoctorLine("WARN", "connection quality", "state=unreadable"); + return; + } + + PrintDoctorLine("OK", "state", statePath.string() + " peers=" + std::to_string(state.peers.size())); + if (config.host.empty()) { + PrintDoctorLine("INFO", "connection quality", "host=missing peers=" + std::to_string(state.peers.size())); + return; + } + + const mwb::PeerState* peer = FindPeerForQuality(state, config.host, config.port); + if (peer == nullptr) { + PrintDoctorLine("INFO", "connection quality", + "host=" + config.host + " port=" + std::to_string(config.port) + " peer_state=missing"); + return; + } + + const std::string detail = + "host=" + peer->host + + " port=" + std::to_string(peer->port) + + " approved=" + BoolToken(peer->approved) + + " connected_now=" + BoolToken(peer->connectedNow) + + " last_seen_epoch=" + std::to_string(peer->lastSeenEpochSeconds) + + " last_connected_epoch=" + std::to_string(peer->lastConnectedEpochSeconds); + PrintDoctorLine(peer->lastConnectedEpochSeconds > 0 ? "OK" : "INFO", "connection quality", detail); +} + int RunClient(const mwb::AppConfig& config, const std::filesystem::path& statePath) { mwb::AppState state; @@ -1436,6 +1558,7 @@ int HandleDiscoverCommand(const std::string& binary, const std::vector& args) { std::filesystem::path configPath = mwb::DefaultConfigPath(); + std::filesystem::path statePath = mwb::DefaultStatePath(); for (std::size_t index = 0; index < args.size(); ++index) { const std::string& arg = args[index]; @@ -1453,6 +1576,12 @@ int HandleDoctorCommand(const std::vector& args) { return 1; } configPath = *value; + } else if (arg == "--state") { + const auto value = requireValue("--state"); + if (!value) { + return 1; + } + statePath = *value; } else { std::cerr << "ERR: Unknown doctor option: " << arg << std::endl; return 1; @@ -1491,16 +1620,17 @@ int HandleDoctorCommand(const std::vector& args) { if (!config.keySecretId.empty()) { PrintDoctorLine("OK", "key source", "Secret Service id '" + config.keySecretId + "'"); } else if (!config.keyFile.empty()) { - std::filesystem::path keyPath = config.keyFile; - if (keyPath.is_relative()) { - keyPath = configPath.parent_path() / keyPath; - } + std::filesystem::path keyPath = ResolveRelativeTo(config.keyFile, configPath.parent_path()); PrintDoctorLine(std::filesystem::exists(keyPath) ? "OK" : "WARN", "key file", keyPath.string()); } else if (!config.key.empty()) { PrintDoctorLine("WARN", "key source", "inline key configured; prefer key_file or key_secret_id"); } else { PrintDoctorLine("WARN", "key source", "not configured"); } + PrintDoctorAuthKeyPresence(config, configPath); + PrintDoctorHostProbe(config, 15100); + PrintDoctorHostProbe(config, 15101); + PrintDoctorConnectionQuality(config, statePath); if (config.screenWidth && config.screenHeight) { PrintDoctorLine("OK", "screen override", std::to_string(*config.screenWidth) + "x" + std::to_string(*config.screenHeight)); @@ -1508,6 +1638,12 @@ int HandleDoctorCommand(const std::vector& args) { PrintDoctorLine("INFO", "screen override", "not configured; /sys/class/drm autodetection will be used"); } } + if (!configExists || hasError) { + PrintDoctorLine("WARN", "auth key", "not checked; config unavailable"); + PrintDoctorLine("INFO", "host 15100", "not checked; config unavailable"); + PrintDoctorLine("INFO", "host 15101", "not checked; config unavailable"); + PrintDoctorConnectionQuality(config, statePath); + } PrintDoctorLine("INFO", "drm", DrmSummary()); From 4d3f9db76fa18be46694697db452e0298aebe39b Mon Sep 17 00:00:00 2001 From: daredoole Date: Tue, 28 Apr 2026 19:14:40 -0400 Subject: [PATCH 3/4] Remove internal planning docs from beta hardening PR --- docs/feedback-inspired-roadmap.md | 63 ------------------- .../cross-platform-kvm-feedback-prompt.md | 51 --------------- 2 files changed, 114 deletions(-) delete mode 100644 docs/feedback-inspired-roadmap.md delete mode 100644 docs/research/cross-platform-kvm-feedback-prompt.md diff --git a/docs/feedback-inspired-roadmap.md b/docs/feedback-inspired-roadmap.md deleted file mode 100644 index b5243da..0000000 --- a/docs/feedback-inspired-roadmap.md +++ /dev/null @@ -1,63 +0,0 @@ -# Feedback-Inspired Roadmap - -This plan turns user feedback from Linux/Windows keyboard-mouse sharing discussions into concrete InputFlow work. The recurring signal is that users do not just want another Synergy-style KVM. They want Mouse Without Borders behavior on Linux: reliable Windows interoperability, safe setup, shared-key pairing, sane multi-monitor traversal, and recoverable diagnostics. - -## Product Goals - -- Preserve PowerToys Mouse Without Borders protocol compatibility. -- Make Windows plus Linux setup safer and easier than Synergy-family tools. -- Treat multi-monitor geometry as a first-class feature, not a side effect. -- Make beta failures diagnosable without asking users to hand-copy logs. -- Avoid invasive installers or helpers that stop unrelated Windows software. - -## Compatibility Targets - -- Windows side: Microsoft PowerToys Mouse Without Borders settings and protocol behavior. -- Linux side: X11 and Wayland sessions where input injection is available through `/dev/uinput`. -- Network: trusted LAN operation over the existing MWB-compatible ports `15101` for input and `15100` for clipboard. -- Auth: existing shared security key model, including inline config, key file, and Secret Service-backed key references. -- Packaging: user-scoped systemd service gated by config presence, distro package install must not auto-enable remote-control behavior. -- Existing names: keep `mwb_client`, `mwb_tray`, `mwb-client.service`, and `~/.config/mwb-client/config.ini` until an alias migration is explicitly designed. - -## Phase 1: Stabilize Current Beta Flow - -- Keep the health check, diagnostics bundle, connection quality panel, and guided pairing flow. -- Add beta issue templates that request diagnostics bundle output, distro, desktop session, Windows version, PowerToys version, monitor layout, and whether clipboard is enabled. -- Add release notes that explain the trust boundary: InputFlow is for trusted LANs, not internet exposure. - -## Phase 2: Screen Topology And Wrap - -- Add a topology model that represents machines and individual displays separately. -- Add explicit wrap policies: `none`, `horizontal`, `vertical`, and `both`. -- Support edge-transition layouts that users call out as broken elsewhere: `AAB`, `BAA`, `ABA`, `BAB`, stacked displays, and asymmetric resolutions. -- Validate impossible layouts before saving them. -- Add tests for edge traversal, release/press preservation across transitions, and monitor-boundary ambiguity. - -## Phase 3: Layout Wizard - -- Add a visual layout wizard in the controller for Linux and Windows displays. -- Provide presets for common two-machine and three-screen layouts. -- Surface warnings for known protocol limitations before users start the service. -- Export the selected layout into the Windows helper flow when PowerToys settings need to be seeded. - -## Phase 4: Safe Windows Helper - -- Add dry-run output that shows exact PowerToys settings changes. -- Back up the PowerToys Mouse Without Borders settings file before writing. -- Provide a restore command or restore instructions in the generated helper. -- Do not stop browsers, VPNs, endpoint security, UPS utilities, or unrelated Windows processes. -- Keep the helper idempotent so users can rerun it after changing display topology. - -## Phase 5: Migration And Positioning - -- Add docs for users coming from Barrier, Input Leap, Deskflow, Synergy, Cursr, and Wine/MWB attempts. -- Explain that InputFlow is MWB-compatible, not Synergy-protocol compatible. -- Call out the design difference: InputFlow should prioritize MWB-style peer behavior, shared key setup, and wrap/topology handling over generic client-server KVM behavior. - -## Decision Gates - -- Do not claim competitor status or maintenance state without fresh primary-source verification. -- Do not add a new protocol mode unless MWB compatibility remains the default path. -- Do not ship topology changes without regression tests for the existing absolute cursor and reconnect behavior. -- Do not package auto-start behavior that activates remote input before a user creates config and opts in. - diff --git a/docs/research/cross-platform-kvm-feedback-prompt.md b/docs/research/cross-platform-kvm-feedback-prompt.md deleted file mode 100644 index f4cf715..0000000 --- a/docs/research/cross-platform-kvm-feedback-prompt.md +++ /dev/null @@ -1,51 +0,0 @@ -# Research Prompt: Cross-Platform KVM Feedback And Compatibility - -Use this prompt for a focused research pass before implementing feedback-inspired features. - -## Objective - -Identify the user-visible failures and winning features across Linux/Windows keyboard-mouse sharing tools, then convert them into InputFlow requirements that preserve Microsoft PowerToys Mouse Without Borders compatibility. - -## Sources To Review - -- Official documentation and release notes for PowerToys Mouse Without Borders. -- Official repositories, docs, and issue trackers for Barrier, Input Leap, Deskflow, Synergy, and Cursr. -- Recent user feedback threads from Reddit, GitHub issues, forums, and distro communities. -- InputFlow local docs and source, especially `README.md`, `SECURITY.md`, `src/InputManager.cpp`, `src/ScreenGeometry.h`, `src/main.cpp`, and `mwb-desktop-ui.sh`. - -## Questions To Answer - -- Which tools currently support Windows plus Linux, and what protocol or architecture do they use? -- Which projects are maintained, deprecated, forked, or commercially supported as of the research date? -- What setup steps users praise or complain about? -- What multi-monitor layouts fail most often? -- How do tools model displays: per-machine rectangle, per-monitor topology, or explicit edge graph? -- Which tools support edge wrap, bidirectional traversal, and layouts like `B A B`? -- What clipboard formats are supported, and where do users report reliability problems? -- What installer or helper behaviors are considered unsafe or invasive? -- What security model is used: shared key, TLS/certificates, accounts/cloud, or unauthenticated LAN? - -## InputFlow Compatibility Requirements - -- Must remain compatible with PowerToys Mouse Without Borders on Windows. -- Must keep the existing AES-256-CBC MWB-compatible protocol unless a separate opt-in mode is designed. -- Must use ports `15101` for input and `15100` for clipboard unless PowerToys compatibility changes. -- Must support existing config keys and auth sources. -- Must preserve Linux input injection through `/dev/uinput`. -- Must work with the existing user systemd service and tray/controller workflow. -- Must not require installing a Windows service or stopping unrelated Windows applications. -- Must keep diagnostics redacted by default. - -## Deliverables - -- A dated evidence table with source links, project status, supported platforms, topology behavior, security model, and install friction. -- A ranked list of InputFlow feature candidates with implementation risk. -- A topology requirements document covering `AAB`, `BAA`, `ABA`, `BAB`, stacked displays, asymmetric resolutions, and wrap modes. -- A compatibility risk assessment for any feature that touches protocol, encryption, clipboard, input mapping, or Windows helper behavior. - -## Output Format - -- Keep direct quotes short and cite each source. -- Separate verified facts from inferred product recommendations. -- Flag unstable facts that need re-checking before release notes or marketing copy. - From e7f6047fa033b973031ee4ff68d68654f71d5a56 Mon Sep 17 00:00:00 2001 From: daredoole Date: Tue, 28 Apr 2026 19:17:46 -0400 Subject: [PATCH 4/4] Fix Fedora CI RPM build dependencies --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0fa197..e3669c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,8 @@ jobs: dnf install -y \ cmake \ gcc-c++ \ + gtk3-devel \ + libayatana-appindicator3-devel \ make \ openssl-devel \ pkgconf-pkg-config \ @@ -123,6 +125,7 @@ jobs: - name: Check shell syntax run: | bash -n mwb-desktop-ui.sh + bash -n scripts/inputflow-diagnostics-bundle.sh bash -n scripts/validate-rpm-packaging.sh - name: Validate RPM packaging skeleton