initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.cpp text eol=lf
|
||||||
|
*.hpp text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
cmake-build-*/
|
||||||
|
CMakeFiles/
|
||||||
|
CMakeCache.txt
|
||||||
|
cmake_install.cmake
|
||||||
|
compile_commands.json
|
||||||
|
CTestTestfile.cmake
|
||||||
|
Testing/
|
||||||
|
|
||||||
|
wayviewer
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
|
||||||
|
.cache/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
project(wayviewer VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
|
||||||
|
pkg_check_modules(LIBVNCCLIENT REQUIRED IMPORTED_TARGET libvncclient)
|
||||||
|
|
||||||
|
add_executable(wayviewer
|
||||||
|
src/input.cpp
|
||||||
|
src/main.cpp
|
||||||
|
src/options.cpp
|
||||||
|
src/sdl_app.cpp
|
||||||
|
src/viewer_state.cpp
|
||||||
|
src/vnc_client.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(wayviewer PRIVATE
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
-Wpedantic
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(wayviewer PRIVATE
|
||||||
|
PkgConfig::SDL2
|
||||||
|
PkgConfig::LIBVNCCLIENT
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"displayName": "Default",
|
||||||
|
"generator": "Unix Makefiles",
|
||||||
|
"binaryDir": "${sourceDir}/build",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"displayName": "Debug",
|
||||||
|
"generator": "Unix Makefiles",
|
||||||
|
"binaryDir": "${sourceDir}/build-debug",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"configurePreset": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"configurePreset": "debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# wayviewer
|
||||||
|
|
||||||
|
A small Wayland-friendly VNC viewer written in C++.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
|
||||||
|
- CMake 3.16+
|
||||||
|
- C++17 compiler
|
||||||
|
- SDL2 development files
|
||||||
|
- libvncclient development files
|
||||||
|
- pkg-config
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --preset default
|
||||||
|
cmake --build --preset default
|
||||||
|
```
|
||||||
|
|
||||||
|
For a debug build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --preset debug
|
||||||
|
cmake --build --preset debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build/wayviewer HOST:5900
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `./build/wayviewer --help` for all options.
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#include "input.hpp"
|
||||||
|
|
||||||
|
#include <rfb/keysym.h>
|
||||||
|
#include <rfb/rfbclient.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
std::uint32_t mapSdlKey(SDL_Keycode key)
|
||||||
|
{
|
||||||
|
if (key >= 0x20 && key <= 0x7e) {
|
||||||
|
return static_cast<std::uint32_t>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case SDLK_BACKSPACE: return XK_BackSpace;
|
||||||
|
case SDLK_TAB: return XK_Tab;
|
||||||
|
case SDLK_RETURN: return XK_Return;
|
||||||
|
case SDLK_ESCAPE: return XK_Escape;
|
||||||
|
case SDLK_DELETE: return XK_Delete;
|
||||||
|
case SDLK_HOME: return XK_Home;
|
||||||
|
case SDLK_END: return XK_End;
|
||||||
|
case SDLK_PAGEUP: return XK_Page_Up;
|
||||||
|
case SDLK_PAGEDOWN: return XK_Page_Down;
|
||||||
|
case SDLK_LEFT: return XK_Left;
|
||||||
|
case SDLK_UP: return XK_Up;
|
||||||
|
case SDLK_RIGHT: return XK_Right;
|
||||||
|
case SDLK_DOWN: return XK_Down;
|
||||||
|
case SDLK_INSERT: return XK_Insert;
|
||||||
|
case SDLK_F1: return XK_F1;
|
||||||
|
case SDLK_F2: return XK_F2;
|
||||||
|
case SDLK_F3: return XK_F3;
|
||||||
|
case SDLK_F4: return XK_F4;
|
||||||
|
case SDLK_F5: return XK_F5;
|
||||||
|
case SDLK_F6: return XK_F6;
|
||||||
|
case SDLK_F7: return XK_F7;
|
||||||
|
case SDLK_F8: return XK_F8;
|
||||||
|
case SDLK_F9: return XK_F9;
|
||||||
|
case SDLK_F10: return XK_F10;
|
||||||
|
case SDLK_F11: return XK_F11;
|
||||||
|
case SDLK_F12: return XK_F12;
|
||||||
|
case SDLK_LSHIFT: return XK_Shift_L;
|
||||||
|
case SDLK_RSHIFT: return XK_Shift_R;
|
||||||
|
case SDLK_LCTRL: return XK_Control_L;
|
||||||
|
case SDLK_RCTRL: return XK_Control_R;
|
||||||
|
case SDLK_LALT: return XK_Alt_L;
|
||||||
|
case SDLK_RALT: return XK_Alt_R;
|
||||||
|
case SDLK_LGUI: return XK_Super_L;
|
||||||
|
case SDLK_RGUI: return XK_Super_R;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int buttonMaskFromMouseState(std::uint32_t buttons)
|
||||||
|
{
|
||||||
|
int mask = 0;
|
||||||
|
if (buttons & SDL_BUTTON_LMASK) {
|
||||||
|
mask |= rfbButton1Mask;
|
||||||
|
}
|
||||||
|
if (buttons & SDL_BUTTON_MMASK) {
|
||||||
|
mask |= rfbButton2Mask;
|
||||||
|
}
|
||||||
|
if (buttons & SDL_BUTTON_RMASK) {
|
||||||
|
mask |= rfbButton3Mask;
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> localToRemote(int localX, int localY, int windowW, int windowH, int remoteW, int remoteH)
|
||||||
|
{
|
||||||
|
if (windowW <= 0 || windowH <= 0 || remoteW <= 0 || remoteH <= 0) {
|
||||||
|
return {0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = localX * remoteW / windowW;
|
||||||
|
int y = localY * remoteH / windowH;
|
||||||
|
x = std::max(0, std::min(remoteW - 1, x));
|
||||||
|
y = std::max(0, std::min(remoteH - 1, y));
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
std::uint32_t mapSdlKey(SDL_Keycode key);
|
||||||
|
int buttonMaskFromMouseState(std::uint32_t buttons);
|
||||||
|
std::pair<int, int> localToRemote(int localX, int localY, int windowW, int windowH, int remoteW, int remoteH);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include "sdl_app.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return wayviewer::runViewer(argc, argv);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "wayviewer: " << e.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
#include "options.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[noreturn]] void usage(const char* program, int code)
|
||||||
|
{
|
||||||
|
std::ostream& out = code == 0 ? std::cout : std::cerr;
|
||||||
|
out << "Usage: " << program << " [options]\n"
|
||||||
|
<< " " << program << " --host HOST [--port PORT] [options]\n"
|
||||||
|
<< " " << program << " HOST[:PORT] [options]\n\n"
|
||||||
|
<< "Options:\n"
|
||||||
|
<< " --host HOST VNC server host\n"
|
||||||
|
<< " --port PORT VNC server port, default 5900\n"
|
||||||
|
<< " --username USER VNC username for servers that request one\n"
|
||||||
|
<< " --password PASS VNC password\n"
|
||||||
|
<< " --password-file PATH Read the first line of PATH as the VNC password\n"
|
||||||
|
<< " --quality LEVEL low, medium, or high; default medium\n"
|
||||||
|
<< " --server-scale N Ask server for 1/N framebuffer scaling if supported\n"
|
||||||
|
<< " --no-prompt Fail instead of prompting for missing values\n"
|
||||||
|
<< " --view-only Do not send keyboard or pointer input\n"
|
||||||
|
<< " --shared Ask the server for a shared session\n"
|
||||||
|
<< " --title TITLE Window title\n"
|
||||||
|
<< " --help Show this help\n";
|
||||||
|
std::exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
int parsePort(const std::string& value)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const int port = std::stoi(value);
|
||||||
|
if (port <= 0 || port > 65535) {
|
||||||
|
throw std::out_of_range("port");
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
throw std::runtime_error("invalid port: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int parseScale(const std::string& value)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const int scale = std::stoi(value);
|
||||||
|
if (scale < 1 || scale > 16) {
|
||||||
|
throw std::out_of_range("scale");
|
||||||
|
}
|
||||||
|
return scale;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
throw std::runtime_error("invalid server scale: " + value + " (expected 1 through 16)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lower(std::string value)
|
||||||
|
{
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quality parseQuality(const std::string& value)
|
||||||
|
{
|
||||||
|
const std::string normalized = lower(value);
|
||||||
|
if (normalized == "low") {
|
||||||
|
return Quality::Low;
|
||||||
|
}
|
||||||
|
if (normalized == "medium" || normalized == "med") {
|
||||||
|
return Quality::Medium;
|
||||||
|
}
|
||||||
|
if (normalized == "high") {
|
||||||
|
return Quality::High;
|
||||||
|
}
|
||||||
|
throw std::runtime_error("invalid quality: " + value + " (expected low, medium, or high)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readPasswordFile(const std::string& path)
|
||||||
|
{
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("failed to open password file: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string password;
|
||||||
|
std::getline(file, password);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseTarget(Options& options, const std::string& target)
|
||||||
|
{
|
||||||
|
const auto colon = target.rfind(':');
|
||||||
|
if (colon == std::string::npos || colon == 0 || colon == target.size() - 1) {
|
||||||
|
options.host = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.host = target.substr(0, colon);
|
||||||
|
options.port = parsePort(target.substr(colon + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string promptLine(const std::string& label, const std::string& defaultValue = {})
|
||||||
|
{
|
||||||
|
std::cout << label;
|
||||||
|
if (!defaultValue.empty()) {
|
||||||
|
std::cout << " [" << defaultValue << "]";
|
||||||
|
}
|
||||||
|
std::cout << ": " << std::flush;
|
||||||
|
|
||||||
|
std::string value;
|
||||||
|
std::getline(std::cin, value);
|
||||||
|
return value.empty() ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string promptPassword(const std::string& label)
|
||||||
|
{
|
||||||
|
std::cout << label << ": " << std::flush;
|
||||||
|
|
||||||
|
termios oldTerm {};
|
||||||
|
const bool canHide = isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &oldTerm) == 0;
|
||||||
|
if (canHide) {
|
||||||
|
termios newTerm = oldTerm;
|
||||||
|
newTerm.c_lflag &= ~ECHO;
|
||||||
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string value;
|
||||||
|
std::getline(std::cin, value);
|
||||||
|
|
||||||
|
if (canHide) {
|
||||||
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &oldTerm);
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Options parseArgs(int argc, char** argv)
|
||||||
|
{
|
||||||
|
Options options;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string arg = argv[i];
|
||||||
|
const auto requireValue = [&](const char* name) -> std::string {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
throw std::runtime_error(std::string("missing value for ") + name);
|
||||||
|
}
|
||||||
|
return argv[++i];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (arg == "--help" || arg == "-h") {
|
||||||
|
usage(argv[0], 0);
|
||||||
|
} else if (arg == "--host") {
|
||||||
|
options.host = requireValue("--host");
|
||||||
|
} else if (arg == "--port") {
|
||||||
|
options.port = parsePort(requireValue("--port"));
|
||||||
|
} else if (arg == "--username") {
|
||||||
|
options.username = requireValue("--username");
|
||||||
|
} else if (arg == "--password") {
|
||||||
|
options.password = requireValue("--password");
|
||||||
|
} else if (arg == "--password-file") {
|
||||||
|
options.password = readPasswordFile(requireValue("--password-file"));
|
||||||
|
} else if (arg == "--quality") {
|
||||||
|
options.quality = parseQuality(requireValue("--quality"));
|
||||||
|
} else if (arg == "--server-scale") {
|
||||||
|
options.serverScale = parseScale(requireValue("--server-scale"));
|
||||||
|
} else if (arg == "--no-prompt") {
|
||||||
|
options.prompt = false;
|
||||||
|
} else if (arg == "--view-only") {
|
||||||
|
options.viewOnly = true;
|
||||||
|
} else if (arg == "--shared") {
|
||||||
|
options.shared = true;
|
||||||
|
} else if (arg == "--title") {
|
||||||
|
options.title = requireValue("--title");
|
||||||
|
} else if (!arg.empty() && arg[0] == '-') {
|
||||||
|
throw std::runtime_error("unknown option: " + arg);
|
||||||
|
} else if (options.host.empty()) {
|
||||||
|
parseTarget(options, arg);
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("unexpected argument: " + arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const char* envUsername = std::getenv("WAYVIEWER_USERNAME")) {
|
||||||
|
if (options.username.empty()) {
|
||||||
|
options.username = envUsername;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const char* envPassword = std::getenv("WAYVIEWER_PASSWORD")) {
|
||||||
|
if (options.password.empty()) {
|
||||||
|
options.password = envPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
void promptForMissingOptions(Options& options)
|
||||||
|
{
|
||||||
|
if (!options.prompt) {
|
||||||
|
if (options.host.empty()) {
|
||||||
|
throw std::runtime_error("missing VNC host");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.host.empty()) {
|
||||||
|
const std::string target = promptLine("VNC host or host:port");
|
||||||
|
if (target.empty()) {
|
||||||
|
throw std::runtime_error("missing VNC host");
|
||||||
|
}
|
||||||
|
parseTarget(options, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.username.empty()) {
|
||||||
|
options.username = promptLine("Username (blank if not required)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.password.empty()) {
|
||||||
|
options.password = promptPassword("Password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
enum class Quality {
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
std::string host;
|
||||||
|
int port = 5900;
|
||||||
|
std::string username;
|
||||||
|
std::string password;
|
||||||
|
Quality quality = Quality::Medium;
|
||||||
|
int serverScale = 0;
|
||||||
|
bool prompt = true;
|
||||||
|
bool viewOnly = false;
|
||||||
|
bool shared = false;
|
||||||
|
std::string title = "wayviewer";
|
||||||
|
};
|
||||||
|
|
||||||
|
Options parseArgs(int argc, char** argv);
|
||||||
|
void promptForMissingOptions(Options& options);
|
||||||
|
|
||||||
|
}
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
#include "sdl_app.hpp"
|
||||||
|
|
||||||
|
#include "input.hpp"
|
||||||
|
#include "options.hpp"
|
||||||
|
#include "viewer_state.hpp"
|
||||||
|
#include "vnc_client.hpp"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <rfb/rfbclient.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct SdlSession {
|
||||||
|
SdlSession()
|
||||||
|
{
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||||
|
throw std::runtime_error(std::string("SDL_Init failed: ") + SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~SdlSession()
|
||||||
|
{
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using WindowPtr = std::unique_ptr<SDL_Window, decltype(&SDL_DestroyWindow)>;
|
||||||
|
using RendererPtr = std::unique_ptr<SDL_Renderer, decltype(&SDL_DestroyRenderer)>;
|
||||||
|
using TexturePtr = std::unique_ptr<SDL_Texture, decltype(&SDL_DestroyTexture)>;
|
||||||
|
|
||||||
|
WindowPtr createWindow(const Options& options)
|
||||||
|
{
|
||||||
|
WindowPtr window(
|
||||||
|
SDL_CreateWindow(options.title.c_str(),
|
||||||
|
SDL_WINDOWPOS_CENTERED,
|
||||||
|
SDL_WINDOWPOS_CENTERED,
|
||||||
|
1024,
|
||||||
|
768,
|
||||||
|
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI),
|
||||||
|
SDL_DestroyWindow);
|
||||||
|
if (!window) {
|
||||||
|
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
|
||||||
|
}
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
RendererPtr createRenderer(SDL_Window* window)
|
||||||
|
{
|
||||||
|
RendererPtr renderer(SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED), SDL_DestroyRenderer);
|
||||||
|
if (!renderer) {
|
||||||
|
renderer.reset(SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE));
|
||||||
|
}
|
||||||
|
if (!renderer) {
|
||||||
|
throw std::runtime_error(std::string("SDL_CreateRenderer failed: ") + SDL_GetError());
|
||||||
|
}
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectClient(rfbClient* client, const Options& options)
|
||||||
|
{
|
||||||
|
int initArgc = 1;
|
||||||
|
char programName[] = "wayviewer";
|
||||||
|
char* initArgv[] = {programName, nullptr};
|
||||||
|
if (!rfbInitClient(client, &initArgc, initArgv)) {
|
||||||
|
throw std::runtime_error("failed to connect or authenticate to VNC server");
|
||||||
|
}
|
||||||
|
if (options.serverScale > 1 && !SendScaleSetting(client, options.serverScale)) {
|
||||||
|
std::cerr << "wayviewer: server did not accept framebuffer scale request\n";
|
||||||
|
}
|
||||||
|
SendIncrementalFramebufferUpdateRequest(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendPendingMotion(rfbClient* client, bool pendingMotion, int x, int y, int mask)
|
||||||
|
{
|
||||||
|
if (pendingMotion) {
|
||||||
|
SendPointerEvent(client, x, y, mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int handleServerMessages(rfbClient* client, ViewerState& state)
|
||||||
|
{
|
||||||
|
int handledMessages = 0;
|
||||||
|
while (handledMessages < 256) {
|
||||||
|
const int ready = WaitForMessage(client, 0);
|
||||||
|
if (ready < 0) {
|
||||||
|
state.connected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ready == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!HandleRFBServerMessage(client)) {
|
||||||
|
state.connected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++handledMessages;
|
||||||
|
}
|
||||||
|
return handledMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTexture(TexturePtr& texture,
|
||||||
|
SDL_Renderer* renderer,
|
||||||
|
ViewerState& state,
|
||||||
|
int& textureW,
|
||||||
|
int& textureH,
|
||||||
|
int& textureBytesPerPixel,
|
||||||
|
bool& needsRender)
|
||||||
|
{
|
||||||
|
const int remoteW = state.width;
|
||||||
|
const int remoteH = state.height;
|
||||||
|
const int bytesPerPixel = state.bytesPerPixel;
|
||||||
|
if (remoteW <= 0 || remoteH <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!texture || textureW != remoteW || textureH != remoteH || textureBytesPerPixel != bytesPerPixel) {
|
||||||
|
const Uint32 textureFormat = bytesPerPixel == 2 ? SDL_PIXELFORMAT_RGB565 : SDL_PIXELFORMAT_XRGB8888;
|
||||||
|
texture.reset(SDL_CreateTexture(renderer, textureFormat, SDL_TEXTUREACCESS_STREAMING, remoteW, remoteH));
|
||||||
|
textureW = remoteW;
|
||||||
|
textureH = remoteH;
|
||||||
|
textureBytesPerPixel = bytesPerPixel;
|
||||||
|
state.dirtyRects.clear();
|
||||||
|
markDirty(state, 0, 0, remoteW, remoteH);
|
||||||
|
state.resized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!texture || !state.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int pitch = remoteW * bytesPerPixel;
|
||||||
|
if (state.dirtyRects.empty()) {
|
||||||
|
SDL_UpdateTexture(texture.get(), nullptr, state.framebuffer.data(), pitch);
|
||||||
|
} else {
|
||||||
|
for (const SDL_Rect& rect : state.dirtyRects) {
|
||||||
|
const auto offset = (static_cast<std::size_t>(rect.y) * remoteW + rect.x) * bytesPerPixel;
|
||||||
|
SDL_UpdateTexture(texture.get(), &rect, state.framebuffer.data() + offset, pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.dirtyRects.clear();
|
||||||
|
state.dirty = false;
|
||||||
|
needsRender = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderFrame(SDL_Renderer* renderer, SDL_Texture* texture, bool& needsRender)
|
||||||
|
{
|
||||||
|
if (!needsRender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 16, 16, 16, 255);
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
if (texture) {
|
||||||
|
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
SDL_RenderPresent(renderer);
|
||||||
|
needsRender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int runViewer(int argc, char** argv)
|
||||||
|
{
|
||||||
|
Options options = parseArgs(argc, argv);
|
||||||
|
promptForMissingOptions(options);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, sdlScaleFilter(options.quality));
|
||||||
|
SdlSession sdl;
|
||||||
|
|
||||||
|
WindowPtr window = createWindow(options);
|
||||||
|
RendererPtr renderer = createRenderer(window.get());
|
||||||
|
|
||||||
|
ViewerState state;
|
||||||
|
ClientContext context;
|
||||||
|
RfbClientPtr client = createClient(options, state, context);
|
||||||
|
connectClient(client.get(), options);
|
||||||
|
|
||||||
|
state.connected = true;
|
||||||
|
TexturePtr texture(nullptr, SDL_DestroyTexture);
|
||||||
|
int textureW = 0;
|
||||||
|
int textureH = 0;
|
||||||
|
int textureBytesPerPixel = 0;
|
||||||
|
int buttonMask = 0;
|
||||||
|
bool running = true;
|
||||||
|
bool needsRender = true;
|
||||||
|
|
||||||
|
while (running && state.connected) {
|
||||||
|
const int inputRemoteW = state.width;
|
||||||
|
const int inputRemoteH = state.height;
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
bool handledEvent = false;
|
||||||
|
bool pendingMotion = false;
|
||||||
|
int pendingMotionX = 0;
|
||||||
|
int pendingMotionY = 0;
|
||||||
|
int pendingMotionMask = 0;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
handledEvent = true;
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_QUIT:
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT:
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_RESIZED ||
|
||||||
|
event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ||
|
||||||
|
event.window.event == SDL_WINDOWEVENT_EXPOSED) {
|
||||||
|
needsRender = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_KEYDOWN:
|
||||||
|
case SDL_KEYUP:
|
||||||
|
if (!options.viewOnly) {
|
||||||
|
const auto key = mapSdlKey(event.key.keysym.sym);
|
||||||
|
if (key != 0) {
|
||||||
|
SendKeyEvent(client.get(), key, event.type == SDL_KEYDOWN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
if (!options.viewOnly) {
|
||||||
|
int windowW = 0;
|
||||||
|
int windowH = 0;
|
||||||
|
SDL_GetWindowSize(window.get(), &windowW, &windowH);
|
||||||
|
const auto [remoteX, remoteY] = localToRemote(event.motion.x,
|
||||||
|
event.motion.y,
|
||||||
|
windowW,
|
||||||
|
windowH,
|
||||||
|
inputRemoteW,
|
||||||
|
inputRemoteH);
|
||||||
|
buttonMask = buttonMaskFromMouseState(event.motion.state);
|
||||||
|
pendingMotion = true;
|
||||||
|
pendingMotionX = remoteX;
|
||||||
|
pendingMotionY = remoteY;
|
||||||
|
pendingMotionMask = buttonMask;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
|
case SDL_MOUSEBUTTONUP:
|
||||||
|
if (!options.viewOnly) {
|
||||||
|
int windowW = 0;
|
||||||
|
int windowH = 0;
|
||||||
|
SDL_GetWindowSize(window.get(), &windowW, &windowH);
|
||||||
|
const auto [remoteX, remoteY] = localToRemote(event.button.x,
|
||||||
|
event.button.y,
|
||||||
|
windowW,
|
||||||
|
windowH,
|
||||||
|
inputRemoteW,
|
||||||
|
inputRemoteH);
|
||||||
|
const int bit = 1 << (event.button.button - 1);
|
||||||
|
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
||||||
|
buttonMask |= bit;
|
||||||
|
} else {
|
||||||
|
buttonMask &= ~bit;
|
||||||
|
}
|
||||||
|
SendPointerEvent(client.get(), remoteX, remoteY, buttonMask);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_MOUSEWHEEL:
|
||||||
|
if (!options.viewOnly) {
|
||||||
|
int localX = 0;
|
||||||
|
int localY = 0;
|
||||||
|
SDL_GetMouseState(&localX, &localY);
|
||||||
|
int windowW = 0;
|
||||||
|
int windowH = 0;
|
||||||
|
SDL_GetWindowSize(window.get(), &windowW, &windowH);
|
||||||
|
const auto [remoteX, remoteY] = localToRemote(localX, localY, windowW, windowH, inputRemoteW, inputRemoteH);
|
||||||
|
const int wheelMask = event.wheel.y > 0 ? rfbButton4Mask : rfbButton5Mask;
|
||||||
|
SendPointerEvent(client.get(), remoteX, remoteY, wheelMask);
|
||||||
|
SendPointerEvent(client.get(), remoteX, remoteY, 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPendingMotion(client.get(), pendingMotion, pendingMotionX, pendingMotionY, pendingMotionMask);
|
||||||
|
const int handledMessages = handleServerMessages(client.get(), state);
|
||||||
|
updateTexture(texture, renderer.get(), state, textureW, textureH, textureBytesPerPixel, needsRender);
|
||||||
|
renderFrame(renderer.get(), texture.get(), needsRender);
|
||||||
|
|
||||||
|
if (!handledEvent && handledMessages == 0 && !needsRender) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
int runViewer(int argc, char** argv);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include "viewer_state.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
void markDirty(ViewerState& state, int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
if (state.width <= 0 || state.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int left = std::max(0, x);
|
||||||
|
const int top = std::max(0, y);
|
||||||
|
const int right = std::min(state.width, x + w);
|
||||||
|
const int bottom = std::min(state.height, y + h);
|
||||||
|
if (left >= right || top >= bottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.dirty = true;
|
||||||
|
if (state.dirtyRects.empty()) {
|
||||||
|
state.dirtyRects.push_back(SDL_Rect{left, top, right - left, bottom - top});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Rect& merged = state.dirtyRects.front();
|
||||||
|
const int mergedLeft = std::min(merged.x, left);
|
||||||
|
const int mergedTop = std::min(merged.y, top);
|
||||||
|
const int mergedRight = std::max(merged.x + merged.w, right);
|
||||||
|
const int mergedBottom = std::max(merged.y + merged.h, bottom);
|
||||||
|
merged = SDL_Rect{mergedLeft, mergedTop, mergedRight - mergedLeft, mergedBottom - mergedTop};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
struct ViewerState {
|
||||||
|
std::vector<std::uint8_t> framebuffer;
|
||||||
|
std::vector<SDL_Rect> dirtyRects;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int bytesPerPixel = 4;
|
||||||
|
bool dirty = false;
|
||||||
|
bool resized = false;
|
||||||
|
bool connected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void markDirty(ViewerState& state, int x, int y, int w, int h);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#include "vnc_client.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int clientContextTag = 0;
|
||||||
|
|
||||||
|
void applyQuality(rfbClient* client, Quality quality)
|
||||||
|
{
|
||||||
|
client->appData.forceTrueColour = TRUE;
|
||||||
|
client->appData.encodingsString = "copyrect tight zrle hextile raw";
|
||||||
|
|
||||||
|
switch (quality) {
|
||||||
|
case Quality::Low:
|
||||||
|
client->appData.requestedDepth = 16;
|
||||||
|
client->appData.compressLevel = 1;
|
||||||
|
client->appData.qualityLevel = 3;
|
||||||
|
client->appData.enableJPEG = TRUE;
|
||||||
|
break;
|
||||||
|
case Quality::Medium:
|
||||||
|
client->appData.requestedDepth = 24;
|
||||||
|
client->appData.compressLevel = 5;
|
||||||
|
client->appData.qualityLevel = 7;
|
||||||
|
client->appData.enableJPEG = TRUE;
|
||||||
|
break;
|
||||||
|
case Quality::High:
|
||||||
|
client->appData.requestedDepth = 24;
|
||||||
|
client->appData.compressLevel = 9;
|
||||||
|
client->appData.qualityLevel = 9;
|
||||||
|
client->appData.enableJPEG = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void configurePixelFormat(rfbClient* client, Quality quality)
|
||||||
|
{
|
||||||
|
if (quality == Quality::Low) {
|
||||||
|
client->format.bitsPerPixel = 16;
|
||||||
|
client->format.depth = 16;
|
||||||
|
client->format.bigEndian = 0;
|
||||||
|
client->format.trueColour = TRUE;
|
||||||
|
client->format.redMax = 31;
|
||||||
|
client->format.greenMax = 63;
|
||||||
|
client->format.blueMax = 31;
|
||||||
|
client->format.redShift = 11;
|
||||||
|
client->format.greenShift = 5;
|
||||||
|
client->format.blueShift = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client->format.bitsPerPixel = 32;
|
||||||
|
client->format.depth = 24;
|
||||||
|
client->format.bigEndian = 0;
|
||||||
|
client->format.trueColour = TRUE;
|
||||||
|
client->format.redMax = 255;
|
||||||
|
client->format.greenMax = 255;
|
||||||
|
client->format.blueMax = 255;
|
||||||
|
client->format.redShift = 16;
|
||||||
|
client->format.greenShift = 8;
|
||||||
|
client->format.blueShift = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* getPassword(rfbClient* client)
|
||||||
|
{
|
||||||
|
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
|
||||||
|
const std::string& password = context->options->password;
|
||||||
|
auto* copy = static_cast<char*>(std::malloc(password.size() + 1));
|
||||||
|
if (!copy) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::memcpy(copy, password.c_str(), password.size() + 1);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
rfbCredential* getCredential(rfbClient* client, int credentialType)
|
||||||
|
{
|
||||||
|
if (credentialType != rfbCredentialTypeUser) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
|
||||||
|
auto* credential = static_cast<rfbCredential*>(std::calloc(1, sizeof(rfbCredential)));
|
||||||
|
if (!credential) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
credential->userCredential.username = strdup(context->options->username.c_str());
|
||||||
|
credential->userCredential.password = strdup(context->options->password.c_str());
|
||||||
|
if (!credential->userCredential.username || !credential->userCredential.password) {
|
||||||
|
std::free(credential->userCredential.username);
|
||||||
|
std::free(credential->userCredential.password);
|
||||||
|
std::free(credential);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
rfbBool allocateFramebuffer(rfbClient* client)
|
||||||
|
{
|
||||||
|
auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
|
||||||
|
auto& state = *context->state;
|
||||||
|
const int width = client->width;
|
||||||
|
const int height = client->height;
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.width = width;
|
||||||
|
state.height = height;
|
||||||
|
state.bytesPerPixel = std::max(1, client->format.bitsPerPixel / 8);
|
||||||
|
state.framebuffer.assign(static_cast<std::size_t>(width) * height * state.bytesPerPixel, 0);
|
||||||
|
client->frameBuffer = state.framebuffer.data();
|
||||||
|
state.dirtyRects.clear();
|
||||||
|
markDirty(state, 0, 0, width, height);
|
||||||
|
state.resized = true;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void framebufferUpdate(rfbClient* client, int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
|
||||||
|
markDirty(*context->state, x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishedFramebufferUpdate(rfbClient* client)
|
||||||
|
{
|
||||||
|
SendIncrementalFramebufferUpdateRequest(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sdlScaleFilter(Quality quality)
|
||||||
|
{
|
||||||
|
switch (quality) {
|
||||||
|
case Quality::Low: return "nearest";
|
||||||
|
case Quality::Medium: return "linear";
|
||||||
|
case Quality::High: return "linear";
|
||||||
|
}
|
||||||
|
return "linear";
|
||||||
|
}
|
||||||
|
|
||||||
|
RfbClientPtr createClient(Options& options, ViewerState& state, ClientContext& context)
|
||||||
|
{
|
||||||
|
const int clientBytesPerPixel = options.quality == Quality::Low ? 2 : 4;
|
||||||
|
const int bitsPerSample = options.quality == Quality::Low ? 5 : 8;
|
||||||
|
RfbClientPtr client(rfbGetClient(bitsPerSample, 3, clientBytesPerPixel), rfbClientCleanup);
|
||||||
|
if (!client) {
|
||||||
|
throw std::runtime_error("failed to allocate VNC client");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.options = &options;
|
||||||
|
context.state = &state;
|
||||||
|
|
||||||
|
rfbClientSetClientData(client.get(), &clientContextTag, &context);
|
||||||
|
client->serverHost = strdup(options.host.c_str());
|
||||||
|
client->serverPort = options.port;
|
||||||
|
client->appData.shareDesktop = options.shared ? TRUE : FALSE;
|
||||||
|
client->appData.viewOnly = options.viewOnly ? TRUE : FALSE;
|
||||||
|
client->appData.scaleSetting = options.serverScale;
|
||||||
|
applyQuality(client.get(), options.quality);
|
||||||
|
client->MallocFrameBuffer = allocateFramebuffer;
|
||||||
|
client->GotFrameBufferUpdate = framebufferUpdate;
|
||||||
|
client->FinishedFrameBufferUpdate = finishedFramebufferUpdate;
|
||||||
|
client->GetPassword = getPassword;
|
||||||
|
client->GetCredential = getCredential;
|
||||||
|
configurePixelFormat(client.get(), options.quality);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "options.hpp"
|
||||||
|
#include "viewer_state.hpp"
|
||||||
|
|
||||||
|
#include <rfb/rfbclient.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace wayviewer {
|
||||||
|
|
||||||
|
struct ClientContext {
|
||||||
|
Options* options = nullptr;
|
||||||
|
ViewerState* state = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
using RfbClientPtr = std::unique_ptr<rfbClient, decltype(&rfbClientCleanup)>;
|
||||||
|
|
||||||
|
const char* sdlScaleFilter(Quality quality);
|
||||||
|
RfbClientPtr createClient(Options& options, ViewerState& state, ClientContext& context);
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user