changes
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "options.hpp"
|
||||
|
||||
namespace wayviewer {
|
||||
|
||||
void showOptionsWindow(Options& options);
|
||||
bool shouldUseOptionsWindow();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user