This commit is contained in:
2026-06-28 10:37:29 -05:00
parent df8a59d03b
commit 6524ccca56
10 changed files with 685 additions and 191 deletions
+16
View File
@@ -15,12 +15,14 @@ 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)
include(CTest)
add_executable(wayviewer
src/input.cpp
src/main.cpp
src/options.cpp
src/sdl_app.cpp
src/ui_prompt.cpp
src/viewer_state.cpp
src/vnc_client.cpp
)
@@ -44,3 +46,17 @@ configure_file(
install(TARGETS wayviewer RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/wayviewer.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
if(BUILD_TESTING)
add_executable(wayviewer_input_tests
tests/input_tests.cpp
src/input.cpp
)
target_link_libraries(wayviewer_input_tests PRIVATE
PkgConfig::SDL2
PkgConfig::LIBVNCCLIENT
)
add_test(NAME input_mapping COMMAND wayviewer_input_tests)
endif()
+9 -6
View File
@@ -60,12 +60,14 @@ Use `./build/wayviewer --help` for all options.
## Launcher
Launching `wayviewer` without arguments from wofi opens graphical prompts for:
Launching `wayviewer` without arguments from wofi opens a small floating-friendly window with one column of fields:
- server address
- username
- password
- quality
- Host
- Username
- Password
- Quality
Use `Tab` or the arrow keys to move between fields, `Enter` to connect, and `Escape`, `Ctrl+Q`, the window close button, or `Quit` to exit.
Install the binary and desktop entry into your user prefix:
@@ -78,7 +80,8 @@ 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/options.*`: command-line parsing and terminal prompts
- `src/ui_prompt.*`: graphical connection form
- `src/vnc_client.*`: VNC client setup and callbacks
- `src/viewer_state.*`: framebuffer state and dirty rectangles
- `src/input.*`: SDL input translation
+61 -1
View File
@@ -7,12 +7,72 @@
namespace wayviewer {
std::uint32_t mapSdlKey(SDL_Keycode key)
namespace {
char shiftedAscii(SDL_Keycode key)
{
switch (key) {
case SDLK_1: return '!';
case SDLK_2: return '@';
case SDLK_3: return '#';
case SDLK_4: return '$';
case SDLK_5: return '%';
case SDLK_6: return '^';
case SDLK_7: return '&';
case SDLK_8: return '*';
case SDLK_9: return '(';
case SDLK_0: return ')';
case SDLK_MINUS: return '_';
case SDLK_EQUALS: return '+';
case SDLK_LEFTBRACKET: return '{';
case SDLK_RIGHTBRACKET: return '}';
case SDLK_BACKSLASH: return '|';
case SDLK_SEMICOLON: return ':';
case SDLK_QUOTE: return '"';
case SDLK_COMMA: return '<';
case SDLK_PERIOD: return '>';
case SDLK_SLASH: return '?';
case SDLK_BACKQUOTE: return '~';
default: return 0;
}
}
std::uint32_t printableKeySym(const SDL_Keysym& keysym)
{
const SDL_Keycode key = keysym.sym;
const bool shift = (keysym.mod & KMOD_SHIFT) != 0;
const bool caps = (keysym.mod & KMOD_CAPS) != 0;
if (key >= SDLK_a && key <= SDLK_z) {
if (shift != caps) {
return static_cast<std::uint32_t>('A' + (key - SDLK_a));
}
return static_cast<std::uint32_t>('a' + (key - SDLK_a));
}
if (shift) {
const char shifted = shiftedAscii(key);
if (shifted != 0) {
return static_cast<std::uint32_t>(shifted);
}
}
if (key >= 0x20 && key <= 0x7e) {
return static_cast<std::uint32_t>(key);
}
return 0;
}
}
std::uint32_t mapSdlKey(const SDL_Keysym& keysym)
{
if (const std::uint32_t printable = printableKeySym(keysym)) {
return printable;
}
const SDL_Keycode key = keysym.sym;
switch (key) {
case SDLK_BACKSPACE: return XK_BackSpace;
case SDLK_TAB: return XK_Tab;
+1 -1
View File
@@ -7,7 +7,7 @@
namespace wayviewer {
std::uint32_t mapSdlKey(SDL_Keycode key);
std::uint32_t mapSdlKey(const SDL_Keysym& keysym);
int buttonMaskFromMouseState(std::uint32_t buttons);
std::pair<int, int> localToRemote(int localX, int localY, int windowW, int windowH, int remoteW, int remoteH);
+11 -182
View File
@@ -1,18 +1,15 @@
#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>
#include "ui_prompt.hpp"
namespace wayviewer {
namespace {
@@ -100,18 +97,6 @@ std::string readPasswordFile(const std::string& path)
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;
@@ -148,158 +133,18 @@ std::string promptPassword(const std::string& label)
return value;
}
bool hasGraphicalSession()
{
return std::getenv("WAYLAND_DISPLAY") || std::getenv("DISPLAY");
}
bool commandExists(const char* command)
void parseTarget(Options& options, const std::string& target)
{
const char* path = std::getenv("PATH");
if (!path) {
return false;
const auto colon = target.rfind(':');
if (colon == std::string::npos || colon == 0 || colon == target.size() - 1) {
options.host = target;
return;
}
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.host = target.substr(0, colon);
options.port = parsePort(target.substr(colon + 1));
}
Options parseArgs(int argc, char** argv)
@@ -372,24 +217,8 @@ 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);
if (shouldUseOptionsWindow()) {
showOptionsWindow(options);
return;
}
+1
View File
@@ -25,5 +25,6 @@ struct Options {
Options parseArgs(int argc, char** argv);
void promptForMissingOptions(Options& options);
void parseTarget(Options& options, const std::string& target);
}
+17 -1
View File
@@ -12,6 +12,7 @@
#include <iostream>
#include <memory>
#include <stdexcept>
#include <unordered_map>
namespace wayviewer {
namespace {
@@ -189,6 +190,7 @@ int runViewer(int argc, char** argv)
int buttonMask = 0;
bool running = true;
bool needsRender = true;
std::unordered_map<SDL_Scancode, std::uint32_t> pressedKeys;
while (running && state.connected) {
const int inputRemoteW = state.width;
@@ -216,7 +218,21 @@ int runViewer(int argc, char** argv)
case SDL_KEYDOWN:
case SDL_KEYUP:
if (!options.viewOnly) {
const auto key = mapSdlKey(event.key.keysym.sym);
std::uint32_t key = 0;
if (event.type == SDL_KEYDOWN) {
key = mapSdlKey(event.key.keysym);
if (!event.key.repeat && key != 0) {
pressedKeys[event.key.keysym.scancode] = key;
}
} else {
const auto found = pressedKeys.find(event.key.keysym.scancode);
if (found != pressedKeys.end()) {
key = found->second;
pressedKeys.erase(found);
} else {
key = mapSdlKey(event.key.keysym);
}
}
if (key != 0) {
SendKeyEvent(client.get(), key, event.type == SDL_KEYDOWN);
}
+528
View File
@@ -0,0 +1,528 @@
#include "ui_prompt.hpp"
#include <SDL.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <unistd.h>
#include <vector>
namespace wayviewer {
namespace {
struct Rect {
int x = 0;
int y = 0;
int w = 0;
int h = 0;
};
enum class Focus {
Host,
Username,
Password,
Quality,
};
struct FormState {
std::string host;
std::string username;
std::string password;
Quality quality = Quality::Medium;
Focus focus = Focus::Host;
std::string error;
bool submitted = false;
bool cancelled = false;
};
constexpr int scale = 3;
constexpr int glyphW = 5;
constexpr int glyphH = 7;
constexpr int advance = 6 * scale;
const std::array<const char*, 7>& glyph(char c)
{
static const std::array<const char*, 7> blank = {"00000", "00000", "00000", "00000", "00000", "00000", "00000"};
static const std::array<const char*, 7> unknown = {"11111", "10001", "00001", "00010", "00100", "00000", "00100"};
static const std::array<std::array<const char*, 7>, 96> table = {{
{{"00000", "00000", "00000", "00000", "00000", "00000", "00000"}},
{{"00100", "00100", "00100", "00100", "00100", "00000", "00100"}},
{{"01010", "01010", "01010", "00000", "00000", "00000", "00000"}},
{{"01010", "01010", "11111", "01010", "11111", "01010", "01010"}},
{{"00100", "01111", "10100", "01110", "00101", "11110", "00100"}},
{{"11000", "11001", "00010", "00100", "01000", "10011", "00011"}},
{{"01100", "10010", "10100", "01000", "10101", "10010", "01101"}},
{{"00100", "00100", "01000", "00000", "00000", "00000", "00000"}},
{{"00010", "00100", "01000", "01000", "01000", "00100", "00010"}},
{{"01000", "00100", "00010", "00010", "00010", "00100", "01000"}},
{{"00000", "00100", "10101", "01110", "10101", "00100", "00000"}},
{{"00000", "00100", "00100", "11111", "00100", "00100", "00000"}},
{{"00000", "00000", "00000", "00000", "00100", "00100", "01000"}},
{{"00000", "00000", "00000", "11111", "00000", "00000", "00000"}},
{{"00000", "00000", "00000", "00000", "00000", "01100", "01100"}},
{{"00001", "00010", "00100", "01000", "10000", "00000", "00000"}},
{{"01110", "10001", "10011", "10101", "11001", "10001", "01110"}},
{{"00100", "01100", "00100", "00100", "00100", "00100", "01110"}},
{{"01110", "10001", "00001", "00010", "00100", "01000", "11111"}},
{{"11110", "00001", "00001", "01110", "00001", "00001", "11110"}},
{{"00010", "00110", "01010", "10010", "11111", "00010", "00010"}},
{{"11111", "10000", "10000", "11110", "00001", "00001", "11110"}},
{{"01110", "10000", "10000", "11110", "10001", "10001", "01110"}},
{{"11111", "00001", "00010", "00100", "01000", "01000", "01000"}},
{{"01110", "10001", "10001", "01110", "10001", "10001", "01110"}},
{{"01110", "10001", "10001", "01111", "00001", "00001", "01110"}},
{{"00000", "01100", "01100", "00000", "01100", "01100", "00000"}},
{{"00000", "01100", "01100", "00000", "01100", "00100", "01000"}},
{{"00010", "00100", "01000", "10000", "01000", "00100", "00010"}},
{{"00000", "00000", "11111", "00000", "11111", "00000", "00000"}},
{{"01000", "00100", "00010", "00001", "00010", "00100", "01000"}},
{{"01110", "10001", "00001", "00010", "00100", "00000", "00100"}},
{{"01110", "10001", "10111", "10101", "10111", "10000", "01110"}},
{{"01110", "10001", "10001", "11111", "10001", "10001", "10001"}},
{{"11110", "10001", "10001", "11110", "10001", "10001", "11110"}},
{{"01110", "10001", "10000", "10000", "10000", "10001", "01110"}},
{{"11110", "10001", "10001", "10001", "10001", "10001", "11110"}},
{{"11111", "10000", "10000", "11110", "10000", "10000", "11111"}},
{{"11111", "10000", "10000", "11110", "10000", "10000", "10000"}},
{{"01110", "10001", "10000", "10111", "10001", "10001", "01110"}},
{{"10001", "10001", "10001", "11111", "10001", "10001", "10001"}},
{{"01110", "00100", "00100", "00100", "00100", "00100", "01110"}},
{{"00001", "00001", "00001", "00001", "10001", "10001", "01110"}},
{{"10001", "10010", "10100", "11000", "10100", "10010", "10001"}},
{{"10000", "10000", "10000", "10000", "10000", "10000", "11111"}},
{{"10001", "11011", "10101", "10101", "10001", "10001", "10001"}},
{{"10001", "11001", "10101", "10011", "10001", "10001", "10001"}},
{{"01110", "10001", "10001", "10001", "10001", "10001", "01110"}},
{{"11110", "10001", "10001", "11110", "10000", "10000", "10000"}},
{{"01110", "10001", "10001", "10001", "10101", "10010", "01101"}},
{{"11110", "10001", "10001", "11110", "10100", "10010", "10001"}},
{{"01111", "10000", "10000", "01110", "00001", "00001", "11110"}},
{{"11111", "00100", "00100", "00100", "00100", "00100", "00100"}},
{{"10001", "10001", "10001", "10001", "10001", "10001", "01110"}},
{{"10001", "10001", "10001", "10001", "10001", "01010", "00100"}},
{{"10001", "10001", "10001", "10101", "10101", "10101", "01010"}},
{{"10001", "10001", "01010", "00100", "01010", "10001", "10001"}},
{{"10001", "10001", "01010", "00100", "00100", "00100", "00100"}},
{{"11111", "00001", "00010", "00100", "01000", "10000", "11111"}},
{{"01110", "01000", "01000", "01000", "01000", "01000", "01110"}},
{{"10000", "01000", "00100", "00010", "00001", "00000", "00000"}},
{{"01110", "00010", "00010", "00010", "00010", "00010", "01110"}},
{{"00100", "01010", "10001", "00000", "00000", "00000", "00000"}},
{{"00000", "00000", "00000", "00000", "00000", "00000", "11111"}},
{{"01000", "00100", "00010", "00000", "00000", "00000", "00000"}},
{{"00000", "00000", "01110", "00001", "01111", "10001", "01111"}},
{{"10000", "10000", "11110", "10001", "10001", "10001", "11110"}},
{{"00000", "00000", "01110", "10000", "10000", "10000", "01110"}},
{{"00001", "00001", "01111", "10001", "10001", "10001", "01111"}},
{{"00000", "00000", "01110", "10001", "11111", "10000", "01110"}},
{{"00110", "01001", "01000", "11100", "01000", "01000", "01000"}},
{{"00000", "00000", "01111", "10001", "10001", "01111", "00001"}},
{{"10000", "10000", "11110", "10001", "10001", "10001", "10001"}},
{{"00100", "00000", "01100", "00100", "00100", "00100", "01110"}},
{{"00010", "00000", "00110", "00010", "00010", "10010", "01100"}},
{{"10000", "10000", "10010", "10100", "11000", "10100", "10010"}},
{{"01100", "00100", "00100", "00100", "00100", "00100", "01110"}},
{{"00000", "00000", "11010", "10101", "10101", "10101", "10101"}},
{{"00000", "00000", "11110", "10001", "10001", "10001", "10001"}},
{{"00000", "00000", "01110", "10001", "10001", "10001", "01110"}},
{{"00000", "00000", "11110", "10001", "10001", "11110", "10000"}},
{{"00000", "00000", "01111", "10001", "10001", "01111", "00001"}},
{{"00000", "00000", "10110", "11001", "10000", "10000", "10000"}},
{{"00000", "00000", "01111", "10000", "01110", "00001", "11110"}},
{{"01000", "01000", "11100", "01000", "01000", "01001", "00110"}},
{{"00000", "00000", "10001", "10001", "10001", "10011", "01101"}},
{{"00000", "00000", "10001", "10001", "10001", "01010", "00100"}},
{{"00000", "00000", "10001", "10001", "10101", "10101", "01010"}},
{{"00000", "00000", "10001", "01010", "00100", "01010", "10001"}},
{{"00000", "00000", "10001", "10001", "10001", "01111", "00001"}},
{{"00000", "00000", "11111", "00010", "00100", "01000", "11111"}},
{{"00010", "00100", "00100", "01000", "00100", "00100", "00010"}},
{{"00100", "00100", "00100", "00000", "00100", "00100", "00100"}},
{{"01000", "00100", "00100", "00010", "00100", "00100", "01000"}},
{{"01000", "10101", "00010", "00000", "00000", "00000", "00000"}}
}};
if (c < 32 || c > 126) {
return unknown;
}
if (c == ' ') {
return blank;
}
return table[static_cast<std::size_t>(c - 32)];
}
void color(SDL_Renderer* renderer, int r, int g, int b)
{
SDL_SetRenderDrawColor(renderer, static_cast<Uint8>(r), static_cast<Uint8>(g), static_cast<Uint8>(b), 255);
}
void fill(SDL_Renderer* renderer, Rect rect)
{
SDL_Rect sdlRect{rect.x, rect.y, rect.w, rect.h};
SDL_RenderFillRect(renderer, &sdlRect);
}
void stroke(SDL_Renderer* renderer, Rect rect)
{
SDL_Rect sdlRect{rect.x, rect.y, rect.w, rect.h};
SDL_RenderDrawRect(renderer, &sdlRect);
}
int textWidth(const std::string& text)
{
return static_cast<int>(text.size()) * advance;
}
void drawText(SDL_Renderer* renderer, int x, int y, const std::string& text, int r, int g, int b)
{
color(renderer, r, g, b);
int cursor = x;
for (char c : text) {
const auto& rows = glyph(c);
for (int row = 0; row < glyphH; ++row) {
for (int col = 0; col < glyphW; ++col) {
if (rows[static_cast<std::size_t>(row)][col] == '1') {
fill(renderer, Rect{cursor + col * scale, y + row * scale, scale, scale});
}
}
}
cursor += advance;
}
}
bool contains(Rect rect, int x, int y)
{
return x >= rect.x && y >= rect.y && x < rect.x + rect.w && y < rect.y + rect.h;
}
std::string qualityName(Quality quality)
{
switch (quality) {
case Quality::Low: return "Low";
case Quality::Medium: return "Medium";
case Quality::High: return "High";
}
return "Medium";
}
Quality nextQuality(Quality quality)
{
switch (quality) {
case Quality::Low: return Quality::Medium;
case Quality::Medium: return Quality::High;
case Quality::High: return Quality::Low;
}
return Quality::Medium;
}
std::string visibleFieldText(const std::string& value, bool password)
{
if (!password) {
return value;
}
return std::string(value.size(), '*');
}
std::string fitText(const std::string& value, int maxWidth)
{
if (textWidth(value) <= maxWidth) {
return value;
}
std::string fitted = value;
while (!fitted.empty() && textWidth("..." + fitted) > maxWidth) {
fitted.erase(fitted.begin());
}
return "..." + fitted;
}
void drawField(SDL_Renderer* renderer, Rect rect, const std::string& label, const std::string& value, bool focused, bool password)
{
drawText(renderer, rect.x, rect.y - 28, label, 218, 222, 226);
color(renderer, focused ? 66 : 38, focused ? 122 : 42, focused ? 176 : 50);
fill(renderer, rect);
color(renderer, focused ? 124 : 82, focused ? 177 : 92, focused ? 255 : 108);
stroke(renderer, rect);
const std::string display = fitText(visibleFieldText(value, password), rect.w - 24);
drawText(renderer, rect.x + 12, rect.y + 13, display, 245, 247, 250);
}
void drawButton(SDL_Renderer* renderer, Rect rect, const std::string& label, bool primary)
{
color(renderer, primary ? 52 : 45, primary ? 119 : 48, primary ? 235 : 54);
fill(renderer, rect);
color(renderer, primary ? 117 : 83, primary ? 169 : 91, primary ? 255 : 105);
stroke(renderer, rect);
drawText(renderer, rect.x + (rect.w - textWidth(label)) / 2, rect.y + 14, label, 250, 250, 252);
}
void render(SDL_Renderer* renderer, const FormState& form)
{
color(renderer, 18, 20, 24);
SDL_RenderClear(renderer);
drawText(renderer, 32, 26, "wayviewer", 245, 247, 250);
drawText(renderer, 32, 58, "Connect to VNC", 156, 163, 175);
const Rect host{32, 120, 456, 48};
const Rect username{32, 206, 456, 48};
const Rect password{32, 292, 456, 48};
const Rect quality{32, 378, 456, 48};
const Rect connect{32, 460, 216, 52};
const Rect quit{272, 460, 216, 52};
drawField(renderer, host, "Host", form.host, form.focus == Focus::Host, false);
drawField(renderer, username, "Username", form.username, form.focus == Focus::Username, false);
drawField(renderer, password, "Password", form.password, form.focus == Focus::Password, true);
drawField(renderer, quality, "Quality", qualityName(form.quality), form.focus == Focus::Quality, false);
drawButton(renderer, connect, "Connect", true);
drawButton(renderer, quit, "Quit", false);
if (!form.error.empty()) {
drawText(renderer, 32, 530, form.error, 248, 113, 113);
}
SDL_RenderPresent(renderer);
}
void focusNext(FormState& form)
{
switch (form.focus) {
case Focus::Host: form.focus = Focus::Username; break;
case Focus::Username: form.focus = Focus::Password; break;
case Focus::Password: form.focus = Focus::Quality; break;
case Focus::Quality: form.focus = Focus::Host; break;
}
}
void focusPrevious(FormState& form)
{
switch (form.focus) {
case Focus::Host: form.focus = Focus::Quality; break;
case Focus::Username: form.focus = Focus::Host; break;
case Focus::Password: form.focus = Focus::Username; break;
case Focus::Quality: form.focus = Focus::Password; break;
}
}
std::string& focusedText(FormState& form)
{
if (form.focus == Focus::Host) {
return form.host;
}
if (form.focus == Focus::Username) {
return form.username;
}
return form.password;
}
void submit(FormState& form)
{
if (form.host.empty()) {
form.error = "Host is required";
form.focus = Focus::Host;
return;
}
form.submitted = true;
}
void applyMouse(FormState& form, int x, int y)
{
const Rect host{32, 120, 456, 48};
const Rect username{32, 206, 456, 48};
const Rect password{32, 292, 456, 48};
const Rect quality{32, 378, 456, 48};
const Rect connect{32, 460, 216, 52};
const Rect quit{272, 460, 216, 52};
if (contains(host, x, y)) {
form.focus = Focus::Host;
} else if (contains(username, x, y)) {
form.focus = Focus::Username;
} else if (contains(password, x, y)) {
form.focus = Focus::Password;
} else if (contains(quality, x, y)) {
form.focus = Focus::Quality;
form.quality = nextQuality(form.quality);
} else if (contains(connect, x, y)) {
submit(form);
} else if (contains(quit, x, y)) {
form.cancelled = true;
}
}
void applyTextInput(FormState& form, const char* text)
{
if (form.focus == Focus::Quality) {
return;
}
std::string& value = focusedText(form);
for (const char* current = text; *current != '\0'; ++current) {
const unsigned char c = static_cast<unsigned char>(*current);
if (c >= 32 && c <= 126 && value.size() < 256) {
value.push_back(static_cast<char>(c));
}
}
}
void applyKey(FormState& form, const SDL_KeyboardEvent& key)
{
const bool ctrl = (key.keysym.mod & KMOD_CTRL) != 0;
switch (key.keysym.sym) {
case SDLK_ESCAPE:
form.cancelled = true;
break;
case SDLK_q:
if (ctrl) {
form.cancelled = true;
}
break;
case SDLK_TAB:
if (key.keysym.mod & KMOD_SHIFT) {
focusPrevious(form);
} else {
focusNext(form);
}
break;
case SDLK_RETURN:
case SDLK_KP_ENTER:
if (form.focus == Focus::Quality) {
form.quality = nextQuality(form.quality);
} else {
submit(form);
}
break;
case SDLK_SPACE:
if (form.focus == Focus::Quality) {
form.quality = nextQuality(form.quality);
}
break;
case SDLK_BACKSPACE:
if (form.focus != Focus::Quality) {
std::string& value = focusedText(form);
if (!value.empty()) {
value.pop_back();
}
}
break;
case SDLK_UP:
focusPrevious(form);
break;
case SDLK_DOWN:
focusNext(form);
break;
case SDLK_LEFT:
case SDLK_RIGHT:
if (form.focus == Focus::Quality) {
form.quality = nextQuality(form.quality);
}
break;
default:
break;
}
}
bool graphicalSession()
{
return std::getenv("WAYLAND_DISPLAY") || std::getenv("DISPLAY");
}
}
bool shouldUseOptionsWindow()
{
if (const char* ui = std::getenv("WAYVIEWER_UI")) {
return std::string(ui) == "window";
}
return graphicalSession() && !isatty(STDIN_FILENO);
}
void showOptionsWindow(Options& options)
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
throw std::runtime_error(std::string("SDL_Init failed: ") + SDL_GetError());
}
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_Window* rawWindow = SDL_CreateWindow("wayviewer",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
520,
580,
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_UTILITY);
if (!rawWindow) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
SDL_Renderer* rawRenderer = SDL_CreateRenderer(rawWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!rawRenderer) {
rawRenderer = SDL_CreateRenderer(rawWindow, -1, SDL_RENDERER_SOFTWARE);
}
if (!rawRenderer) {
SDL_DestroyWindow(rawWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
throw std::runtime_error(std::string("SDL_CreateRenderer failed: ") + SDL_GetError());
}
SDL_SetWindowAlwaysOnTop(rawWindow, SDL_TRUE);
SDL_RaiseWindow(rawWindow);
FormState form;
form.host = options.host;
form.username = options.username;
form.password = options.password;
form.quality = options.quality;
SDL_StartTextInput();
while (!form.submitted && !form.cancelled) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
form.cancelled = true;
break;
case SDL_MOUSEBUTTONDOWN:
applyMouse(form, event.button.x, event.button.y);
break;
case SDL_TEXTINPUT:
applyTextInput(form, event.text.text);
break;
case SDL_KEYDOWN:
applyKey(form, event.key);
break;
default:
break;
}
}
render(rawRenderer, form);
SDL_Delay(16);
}
SDL_StopTextInput();
SDL_DestroyRenderer(rawRenderer);
SDL_DestroyWindow(rawWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (form.cancelled) {
throw std::runtime_error("cancelled");
}
options.host.clear();
options.port = 5900;
parseTarget(options, form.host);
options.username = form.username;
options.password = form.password;
options.quality = form.quality;
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "options.hpp"
namespace wayviewer {
void showOptionsWindow(Options& options);
bool shouldUseOptionsWindow();
}
+31
View File
@@ -0,0 +1,31 @@
#include "../src/input.hpp"
#include <SDL.h>
#include <rfb/keysym.h>
#include <cassert>
namespace {
SDL_Keysym keysym(SDL_Keycode key, Uint16 mod = KMOD_NONE)
{
SDL_Keysym value {};
value.sym = key;
value.mod = mod;
value.scancode = SDL_GetScancodeFromKey(key);
return value;
}
}
int main()
{
assert(wayviewer::mapSdlKey(keysym(SDLK_a)) == 'a');
assert(wayviewer::mapSdlKey(keysym(SDLK_a, KMOD_LSHIFT)) == 'A');
assert(wayviewer::mapSdlKey(keysym(SDLK_a, KMOD_CAPS)) == 'A');
assert(wayviewer::mapSdlKey(keysym(SDLK_a, KMOD_LSHIFT | KMOD_CAPS)) == 'a');
assert(wayviewer::mapSdlKey(keysym(SDLK_1, KMOD_LSHIFT)) == '!');
assert(wayviewer::mapSdlKey(keysym(SDLK_SLASH, KMOD_LSHIFT)) == '?');
assert(wayviewer::mapSdlKey(keysym(SDLK_LSHIFT)) == XK_Shift_L);
return 0;
}