installer and packaging

This commit is contained in:
2026-06-28 09:57:06 -05:00
parent b860b031bb
commit df8a59d03b
6 changed files with 396 additions and 0 deletions
+11
View File
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16)
project(wayviewer VERSION 0.1.0 LANGUAGES CXX) project(wayviewer VERSION 0.1.0 LANGUAGES CXX)
include(GNUInstallDirs)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif() endif()
@@ -33,3 +35,12 @@ target_link_libraries(wayviewer PRIVATE
PkgConfig::SDL2 PkgConfig::SDL2
PkgConfig::LIBVNCCLIENT 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)
+2
View File
@@ -8,6 +8,7 @@
"binaryDir": "${sourceDir}/build", "binaryDir": "${sourceDir}/build",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Release", "CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "$env{HOME}/.local",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON" "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
} }
}, },
@@ -18,6 +19,7 @@
"binaryDir": "${sourceDir}/build-debug", "binaryDir": "${sourceDir}/build-debug",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug", "CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_PREFIX": "$env{HOME}/.local",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON" "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
} }
} }
+52
View File
@@ -12,6 +12,32 @@ Dependencies:
- libvncclient development files - libvncclient development files
- pkg-config - 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 ```sh
cmake --preset default cmake --preset default
cmake --build --preset default cmake --build --preset default
@@ -32,6 +58,32 @@ cmake --build --preset debug
Use `./build/wayviewer --help` for all options. 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? ## 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 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
Executable
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env sh
set -eu
usage() {
cat <<EOF
Usage: ./install.sh [install|uninstall] [options]
Commands:
install Build and install wayviewer
uninstall Remove files installed by this script
Options:
--prefix PATH Install prefix, default: \$HOME/.local
--system Install to /usr/local
--debug Build Debug instead of Release
--build-dir PATH Build directory, default: build or build-debug
--destdir PATH Stage install or uninstall under DESTDIR
--help Show this help
EOF
}
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
action=install
build_type=Release
prefix=${WAYVIEWER_PREFIX:-"$HOME/.local"}
build_dir=
destdir=
while [ "$#" -gt 0 ]; do
case "$1" in
install)
action=install
;;
uninstall|remove)
action=uninstall
;;
--prefix)
shift
[ "$#" -gt 0 ] || { echo "install.sh: missing value for --prefix" >&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
+7
View File
@@ -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;
+178
View File
@@ -1,13 +1,18 @@
#include "options.hpp" #include "options.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <cctype> #include <cctype>
#include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string_view>
#include <sys/wait.h>
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include <vector>
namespace wayviewer { namespace wayviewer {
namespace { namespace {
@@ -143,6 +148,158 @@ std::string promptPassword(const std::string& label)
return value; 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<std::size_t>(written);
}
}
std::string readAll(int fd)
{
std::string output;
std::array<char, 256> 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<std::size_t>(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<std::string>& 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<char*> argv;
argv.push_back(const_cast<char*>("wofi"));
for (const std::string& arg : args) {
argv.push_back(const_cast<char*>(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) Options parseArgs(int argc, char** argv)
@@ -215,6 +372,27 @@ void promptForMissingOptions(Options& options)
return; 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()) { if (options.host.empty()) {
const std::string target = promptLine("VNC host or host:port"); const std::string target = promptLine("VNC host or host:port");
if (target.empty()) { if (target.empty()) {