installer and packaging
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+146
@@ -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
|
||||
@@ -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
@@ -1,13 +1,18 @@
|
||||
#include "options.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
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<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)
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user