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)
|
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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 "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()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user