From df8a59d03bcdbffcc7c5ea5d2b7ae3f2e325c3da Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Sun, 28 Jun 2026 09:57:06 -0500 Subject: [PATCH] installer and packaging --- CMakeLists.txt | 11 ++ CMakePresets.json | 2 + README.md | 52 ++++++++++ install.sh | 146 +++++++++++++++++++++++++++ packaging/wayviewer.desktop.in | 7 ++ src/options.cpp | 178 +++++++++++++++++++++++++++++++++ 6 files changed, 396 insertions(+) create mode 100755 install.sh create mode 100644 packaging/wayviewer.desktop.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cbbb7b..555ea3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(wayviewer VERSION 0.1.0 LANGUAGES CXX) +include(GNUInstallDirs) + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() @@ -33,3 +35,12 @@ target_link_libraries(wayviewer PRIVATE PkgConfig::SDL2 PkgConfig::LIBVNCCLIENT ) + +configure_file( + packaging/wayviewer.desktop.in + ${CMAKE_CURRENT_BINARY_DIR}/wayviewer.desktop + @ONLY +) + +install(TARGETS wayviewer RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/wayviewer.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/CMakePresets.json b/CMakePresets.json index 1f8c3cc..753cd63 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -8,6 +8,7 @@ "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "$env{HOME}/.local", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } }, @@ -18,6 +19,7 @@ "binaryDir": "${sourceDir}/build-debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_INSTALL_PREFIX": "$env{HOME}/.local", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } } diff --git a/README.md b/README.md index 46da59c..eaf4406 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,32 @@ Dependencies: - libvncclient development files - pkg-config +## Install + +```sh +./install.sh +``` + +Uninstall: + +```sh +./install.sh uninstall +``` + +System install: + +```sh +./install.sh --system +``` + +Custom prefix: + +```sh +./install.sh --prefix /path/to/prefix +``` + +## Build Manually + ```sh cmake --preset default cmake --build --preset default @@ -32,6 +58,32 @@ cmake --build --preset debug Use `./build/wayviewer --help` for all options. +## Launcher + +Launching `wayviewer` without arguments from wofi opens graphical prompts for: + +- server address +- username +- password +- quality + +Install the binary and desktop entry into your user prefix: + +```sh +cmake --install build +``` + +Then open wofi in `drun` mode and choose `wayviewer`. + +## Source Layout + +- `src/main.cpp`: process entry point +- `src/options.*`: command-line parsing and wofi or terminal prompts +- `src/vnc_client.*`: VNC client setup and callbacks +- `src/viewer_state.*`: framebuffer state and dirty rectangles +- `src/input.*`: SDL input translation +- `src/sdl_app.*`: SDL window, rendering, and event loop + ## Why? Because I needed a VNC viewer that didnt suck and so here it is. Provided with no warranty to anyone who wants to build off of this hunk of complete trash diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9a74333 --- /dev/null +++ b/install.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <&2; exit 2; } + prefix=$1 + ;; + --system) + prefix=/usr/local + ;; + --debug) + build_type=Debug + ;; + --build-dir) + shift + [ "$#" -gt 0 ] || { echo "install.sh: missing value for --build-dir" >&2; exit 2; } + build_dir=$1 + ;; + --destdir) + shift + [ "$#" -gt 0 ] || { echo "install.sh: missing value for --destdir" >&2; exit 2; } + destdir=$1 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "install.sh: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ -z "$build_dir" ]; then + if [ "$build_type" = Debug ]; then + build_dir=$script_dir/build-debug + else + build_dir=$script_dir/build + fi +fi + +run_install() { + command -v cmake >/dev/null 2>&1 || { echo "install.sh: cmake is required" >&2; exit 1; } + command -v pkg-config >/dev/null 2>&1 || { echo "install.sh: pkg-config is required" >&2; exit 1; } + + cmake -S "$script_dir" -B "$build_dir" \ + -DCMAKE_BUILD_TYPE="$build_type" \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + cmake --build "$build_dir" + + if [ -n "$destdir" ]; then + DESTDIR=$destdir cmake --install "$build_dir" + elif mkdir -p "$prefix" 2>/dev/null && [ -w "$prefix" ]; then + cmake --install "$build_dir" + elif command -v sudo >/dev/null 2>&1; then + sudo cmake --install "$build_dir" + else + echo "install.sh: $prefix is not writable and sudo was not found" >&2 + exit 1 + fi + + if command -v update-desktop-database >/dev/null 2>&1; then + applications_dir=${destdir}${prefix}/share/applications + if [ -d "$applications_dir" ]; then + update-desktop-database "$applications_dir" >/dev/null 2>&1 || true + fi + fi +} + +run_uninstall() { + manifest=$build_dir/install_manifest.txt + if [ ! -f "$manifest" ]; then + echo "install.sh: no install manifest found at $manifest" >&2 + echo "install.sh: run install first or pass the matching --build-dir" >&2 + exit 1 + fi + + while IFS= read -r path || [ -n "$path" ]; do + target=${destdir}${path} + if [ -e "$target" ] || [ -L "$target" ]; then + if [ -w "$(dirname "$target")" ]; then + rm -f "$target" + elif command -v sudo >/dev/null 2>&1; then + sudo rm -f "$target" + else + echo "install.sh: cannot remove $target and sudo was not found" >&2 + exit 1 + fi + echo "Removed $target" + fi + done < "$manifest" + + if command -v update-desktop-database >/dev/null 2>&1; then + applications_dir=${destdir}${prefix}/share/applications + if [ -d "$applications_dir" ]; then + update-desktop-database "$applications_dir" >/dev/null 2>&1 || true + fi + fi +} + +case "$action" in + install) + run_install + ;; + uninstall) + run_uninstall + ;; +esac diff --git a/packaging/wayviewer.desktop.in b/packaging/wayviewer.desktop.in new file mode 100644 index 0000000..20b7d50 --- /dev/null +++ b/packaging/wayviewer.desktop.in @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=wayviewer +Exec=@CMAKE_INSTALL_FULL_BINDIR@/wayviewer +Terminal=false +Categories=Network;RemoteAccess; +Keywords=VNC;Remote;Desktop;Wayland; diff --git a/src/options.cpp b/src/options.cpp index 9fae3a8..8ddbbd8 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -1,13 +1,18 @@ #include "options.hpp" #include +#include #include +#include #include #include #include #include +#include +#include #include #include +#include namespace wayviewer { namespace { @@ -143,6 +148,158 @@ std::string promptPassword(const std::string& label) return value; } +bool hasGraphicalSession() +{ + return std::getenv("WAYLAND_DISPLAY") || std::getenv("DISPLAY"); +} + +bool commandExists(const char* command) +{ + const char* path = std::getenv("PATH"); + if (!path) { + return false; + } + + std::string paths = path; + std::size_t start = 0; + while (start <= paths.size()) { + const std::size_t end = paths.find(':', start); + const std::string directory = paths.substr(start, end == std::string::npos ? std::string::npos : end - start); + const std::string candidate = (directory.empty() ? "." : directory) + "/" + command; + if (access(candidate.c_str(), X_OK) == 0) { + return true; + } + if (end == std::string::npos) { + break; + } + start = end + 1; + } + return false; +} + +void writeAll(int fd, std::string_view data) +{ + const char* current = data.data(); + std::size_t remaining = data.size(); + while (remaining > 0) { + const ssize_t written = write(fd, current, remaining); + if (written < 0) { + if (errno == EINTR) { + continue; + } + return; + } + current += written; + remaining -= static_cast(written); + } +} + +std::string readAll(int fd) +{ + std::string output; + std::array buffer {}; + while (true) { + const ssize_t count = read(fd, buffer.data(), buffer.size()); + if (count < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (count == 0) { + break; + } + output.append(buffer.data(), static_cast(count)); + } + return output; +} + +std::string trimLineEnd(std::string value) +{ + while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) { + value.pop_back(); + } + return value; +} + +std::string runWofi(const std::vector& args, const std::string& input) +{ + int stdinPipe[2] {}; + int stdoutPipe[2] {}; + if (pipe(stdinPipe) != 0 || pipe(stdoutPipe) != 0) { + throw std::runtime_error("failed to open prompt pipes"); + } + + const pid_t pid = fork(); + if (pid < 0) { + close(stdinPipe[0]); + close(stdinPipe[1]); + close(stdoutPipe[0]); + close(stdoutPipe[1]); + throw std::runtime_error("failed to launch wofi"); + } + + if (pid == 0) { + dup2(stdinPipe[0], STDIN_FILENO); + dup2(stdoutPipe[1], STDOUT_FILENO); + close(stdinPipe[0]); + close(stdinPipe[1]); + close(stdoutPipe[0]); + close(stdoutPipe[1]); + + std::vector argv; + argv.push_back(const_cast("wofi")); + for (const std::string& arg : args) { + argv.push_back(const_cast(arg.c_str())); + } + argv.push_back(nullptr); + execvp("wofi", argv.data()); + _exit(127); + } + + close(stdinPipe[0]); + close(stdoutPipe[1]); + writeAll(stdinPipe[1], input); + close(stdinPipe[1]); + + std::string output = readAll(stdoutPipe[0]); + close(stdoutPipe[0]); + + int status = 0; + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) { + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + throw std::runtime_error("prompt cancelled"); + } + + return trimLineEnd(output); +} + +std::string wofiTextPrompt(const std::string& label) +{ + return runWofi({"--dmenu", "--exec-search", "--prompt", label, "--lines", "1", "--width", "520"}, "\n"); +} + +std::string wofiPasswordPrompt(const std::string& label) +{ + return runWofi({"--dmenu", "--password", "--exec-search", "--prompt", label, "--lines", "1", "--width", "520"}, "\n"); +} + +Quality wofiQualityPrompt(Quality current) +{ + const char* selected = current == Quality::Low ? "low\nmedium\nhigh\n" : current == Quality::High ? "high\nmedium\nlow\n" : "medium\nhigh\nlow\n"; + return parseQuality(runWofi({"--dmenu", "--no-custom-entry", "--prompt", "Quality", "--lines", "3", "--width", "320"}, selected)); +} + +bool shouldUseWofi() +{ + if (const char* ui = std::getenv("WAYVIEWER_UI")) { + return std::string(ui) == "wofi"; + } + return hasGraphicalSession() && commandExists("wofi") && !isatty(STDIN_FILENO); +} + } Options parseArgs(int argc, char** argv) @@ -215,6 +372,27 @@ void promptForMissingOptions(Options& options) return; } + if (shouldUseWofi()) { + if (options.host.empty()) { + const std::string target = wofiTextPrompt("VNC host or host:port"); + if (target.empty()) { + throw std::runtime_error("missing VNC host"); + } + parseTarget(options, target); + } + + if (options.username.empty()) { + options.username = wofiTextPrompt("Username"); + } + + if (options.password.empty()) { + options.password = wofiPasswordPrompt("Password"); + } + + options.quality = wofiQualityPrompt(options.quality); + return; + } + if (options.host.empty()) { const std::string target = promptLine("VNC host or host:port"); if (target.empty()) {