diff --git a/CMakeLists.txt b/CMakeLists.txt index bb9d19f..96fcc2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,18 @@ project(turnbinds LANGUAGES CXX) add_executable(turnbinds) -set_source_files_properties( - "${CMAKE_CURRENT_SOURCE_DIR}/src/turnbinds.rc" - PROPERTIES OBJECT_DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/src/turnbinds.ico" -) +if(WIN32) + set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/src/turnbinds.rc" + PROPERTIES OBJECT_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/turnbinds.ico" + ) + + file(GLOB_RECURSE src CONFIGURE_DEPENDS src/*.cpp src/*.rc) +else() + file(GLOB_RECURSE src CONFIGURE_DEPENDS src/*.cpp) +endif() -file(GLOB_RECURSE src CONFIGURE_DEPENDS src/*.cpp src/*.rc) target_sources(turnbinds PRIVATE ${src}) target_include_directories(turnbinds PRIVATE src) @@ -22,14 +27,32 @@ set_property(TARGET turnbinds PROPERTY CXX_STANDARD 26) target_compile_options(turnbinds PRIVATE -Wall -Werror -Wno-switch - -Wno-vla-cxx-extension + $<$:-Wno-vla-cxx-extension> + $<$:-Wno-vla> + $<$:-Wno-sign-compare> + $<$:-Wno-subobject-linkage> ) -target_link_libraries(turnbinds PRIVATE version pathcch) - -target_link_options(turnbinds PRIVATE - -static - $<$:-Wl,--strip-all> -) +if(WIN32) + target_link_libraries(turnbinds PRIVATE version pathcch) + + target_link_options(turnbinds PRIVATE + -static + $<$:-Wl,--strip-all> + ) +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(XLIBS REQUIRED IMPORTED_TARGET x11 xtst xfixes) + target_link_libraries(turnbinds PRIVATE PkgConfig::XLIBS) + + target_compile_definitions(turnbinds PRIVATE + VERSION_STRING="2.2.0" + COPYRIGHT_STRING="https://github.com/t5mat/turnbinds" + ) + + target_link_options(turnbinds PRIVATE + $<$:-Wl,--strip-all> + ) +endif() install(TARGETS turnbinds RUNTIME DESTINATION .) diff --git a/src/common.hpp b/src/common.hpp index 24e70c3..c81d358 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -1,7 +1,69 @@ #pragma once #include +#include +#include +#include +#include +#include + +#ifdef _WIN32 #include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef COUNT +#undef COUNT +#endif + +#undef KEY_ENTER +#undef KEY_UP +#undef KEY_DOWN +#undef KEY_LEFT +#undef KEY_RIGHT + +using SHORT = short; +using WORD = unsigned short; +using DWORD = unsigned long; + +struct COORD { SHORT X, Y; }; +struct SMALL_RECT { SHORT Left, Top, Right, Bottom; }; + +struct CONSOLE_SCREEN_BUFFER_INFOEX { + COORD dwSize; + COORD dwCursorPosition; + WORD wAttributes; +}; + +struct CONSOLE_CURSOR_INFO { + DWORD dwSize; + bool bVisible; +}; + +constexpr WORD FOREGROUND_BLUE = 0x0001; +constexpr WORD FOREGROUND_GREEN = 0x0002; +constexpr WORD FOREGROUND_RED = 0x0004; +constexpr WORD FOREGROUND_INTENSITY = 0x0008; +constexpr WORD BACKGROUND_BLUE = 0x0010; +constexpr WORD BACKGROUND_GREEN = 0x0020; +constexpr WORD BACKGROUND_RED = 0x0040; +constexpr WORD BACKGROUND_INTENSITY = 0x0080; +#endif namespace common { @@ -43,6 +105,8 @@ inline const wchar_t *parse_double(const wchar_t *s, double &value) return end; } +#ifdef _WIN32 + namespace win32 { namespace { @@ -385,4 +449,462 @@ struct CtrlSignalHandler } +namespace platform = win32; + +#else + +namespace platform { + +constexpr int MOUSE_BUTTON_BASE = 0x10000; + +constexpr int DEFAULT_BIND_LEFT = MOUSE_BUTTON_BASE | 1; +constexpr int DEFAULT_BIND_RIGHT = MOUSE_BUTTON_BASE | 3; +constexpr int DEFAULT_BIND_SPEED = XK_Shift_L; +constexpr int DEFAULT_BIND_CYCLE = MOUSE_BUTTON_BASE | 8; + +constexpr auto VIRTUAL_KEYS = 256; + +inline Display *get_display() +{ + static Display *dpy = XOpenDisplay(nullptr); + return dpy; +} + +inline long long performance_counter_frequency() +{ + return 1000000000LL; +} + +inline long long performance_counter() +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +inline void set_timer_resolution(unsigned long) +{ +} + +inline void delay_execution_by(long long hns) +{ + struct timespec ts; + ts.tv_sec = hns / 10000000; + ts.tv_nsec = (hns % 10000000) * 100; + nanosleep(&ts, nullptr); +} + +inline void move_mouse_by(int x, int y) +{ + auto *dpy = get_display(); + if (!dpy) return; + XTestFakeRelativeMotionEvent(dpy, x, y, 0); + XFlush(dpy); +} + +struct EvdevState +{ + static constexpr int MAX_DEVICES = 32; + int fds[MAX_DEVICES]; + int count = 0; + bool initialized = false; + + void init() + { + if (initialized) return; + initialized = true; + + DIR *dir = opendir("/dev/input"); + if (!dir) return; + + struct dirent *ent; + while ((ent = readdir(dir)) && count < MAX_DEVICES) { + if (std::strncmp(ent->d_name, "event", 5) != 0) continue; + char path[280]; + std::snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); + int fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd >= 0) { + fds[count++] = fd; + } + } + closedir(dir); + } + + bool is_code_down(unsigned int code) + { + init(); + unsigned char key_states[KEY_MAX / 8 + 1]; + for (int i = 0; i < count; ++i) { + std::memset(key_states, 0, sizeof(key_states)); + if (ioctl(fds[i], EVIOCGKEY(sizeof(key_states)), key_states) >= 0) { + if (key_states[code / 8] & (1 << (code % 8))) { + return true; + } + } + } + return false; + } +}; + +inline EvdevState &get_evdev() +{ + static EvdevState state; + return state; +} + +inline int x11_button_to_evdev(int button) +{ + switch (button) { + case 1: return BTN_LEFT; + case 2: return BTN_MIDDLE; + case 3: return BTN_RIGHT; + case 8: return BTN_SIDE; + case 9: return BTN_EXTRA; + default: return -1; + } +} + +inline bool is_key_down(int key) +{ + auto &evdev = get_evdev(); + + if (key >= MOUSE_BUTTON_BASE) { + int button = key & 0xFFFF; + int code = x11_button_to_evdev(button); + if (code < 0) return false; + return evdev.is_code_down(code); + } + + auto *dpy = get_display(); + if (!dpy) return false; + KeyCode kc = XKeysymToKeycode(dpy, key); + if (kc == 0) return false; + unsigned int evdev_code = kc - 8; + return evdev.is_code_down(evdev_code); +} + +inline bool get_vk_string(int vk, wchar_t *result, size_t count) +{ + if (vk >= MOUSE_BUTTON_BASE) { + int button = vk & 0xFFFF; + switch (button) { + case 1: std::swprintf(result, count, L"Left Mouse Button"); return true; + case 2: std::swprintf(result, count, L"Middle Mouse Button"); return true; + case 3: std::swprintf(result, count, L"Right Mouse Button"); return true; + case 8: std::swprintf(result, count, L"X1 Mouse Button"); return true; + case 9: std::swprintf(result, count, L"X2 Mouse Button"); return true; + default: std::swprintf(result, count, L"Mouse Button %d", button); return true; + } + } + + const char *name = XKeysymToString(vk); + if (!name) return false; + + size_t i; + for (i = 0; name[i] && i < count - 1; ++i) { + result[i] = static_cast(name[i]); + } + result[i] = L'\0'; + return true; +} + +struct ConsoleOutput +{ + COORD buffer_size = {64, 20}; + COORD cursor_pos = {0, 0}; + WORD current_attr = 0; + bool cursor_visible_ = false; + + ConsoleOutput() + { + std::printf("\033[?1049h"); + std::printf("\033[?25l"); + std::printf("\033[0m"); + std::fflush(stdout); + } + + ~ConsoleOutput() + { + std::printf("\033[0m"); + std::printf("\033[?25h"); + std::printf("\033[?1049l"); + std::fflush(stdout); + } + + void set_as_active_screen_buffer() {} + + CONSOLE_CURSOR_INFO get_cursor_info() + { + return {100, cursor_visible_}; + } + + void set_cursor_info(const CONSOLE_CURSOR_INFO &info) + { + cursor_visible_ = info.bVisible; + std::printf(info.bVisible ? "\033[?25h" : "\033[?25l"); + std::fflush(stdout); + } + + CONSOLE_SCREEN_BUFFER_INFOEX get_screen_buffer_info() + { + return {buffer_size, cursor_pos, current_attr}; + } + + void set_window_info(bool, const SMALL_RECT &) {} + + void set_buffer_size(COORD size) + { + buffer_size = size; + } + + void set_cursor_position(COORD pos) + { + cursor_pos = pos; + std::printf("\033[%d;%dH", pos.Y + 1, pos.X + 1); + } + + DWORD get_mode() { return 0; } + void set_mode(DWORD) {} + + void set_text_attributes(WORD attr) + { + current_attr = attr; + if (attr == 0) { + std::printf("\033[0m"); + } else { + int fg = 30; + if (attr & FOREGROUND_RED) fg += 1; + if (attr & FOREGROUND_GREEN) fg += 2; + if (attr & FOREGROUND_BLUE) fg += 4; + if (attr & FOREGROUND_INTENSITY) fg += 60; + + int bg = 40; + if (attr & BACKGROUND_RED) bg += 1; + if (attr & BACKGROUND_GREEN) bg += 2; + if (attr & BACKGROUND_BLUE) bg += 4; + if (attr & BACKGROUND_INTENSITY) bg += 60; + + std::printf("\033[0;%d;%dm", fg, bg); + } + } + + void fill(wchar_t c, COORD pos, size_t count) + { + set_cursor_position(pos); + if (c == L' ') { + if (pos.X == 0 && count >= (size_t)buffer_size.X) { + std::printf("\033[J"); + } else { + std::printf("\033[K"); + } + } else { + size_t max = buffer_size.X - pos.X; + size_t to_write = std::min(count, max); + char ch = static_cast(c); + for (size_t i = 0; i < to_write; ++i) { + std::putchar(ch); + } + cursor_pos.X += to_write; + } + } + + void fill_attribute(WORD, COORD, size_t) {} + + static void write_wchar(wchar_t c) + { + if (static_cast(c) < 0x80) { + std::putchar(static_cast(c)); + } else if (static_cast(c) < 0x800) { + std::putchar(0xC0 | (c >> 6)); + std::putchar(0x80 | (c & 0x3F)); + } else { + std::putchar(0xE0 | (c >> 12)); + std::putchar(0x80 | ((c >> 6) & 0x3F)); + std::putchar(0x80 | (c & 0x3F)); + } + } + + void write(const wchar_t *s, size_t count) + { + for (size_t i = 0; i < count; ++i) { + write_wchar(s[i]); + } + cursor_pos.X += count; + } + + void write_text(const wchar_t *s) + { + write(s, std::wcslen(s)); + } + + void write_info(const wchar_t *s, size_t count, CONSOLE_SCREEN_BUFFER_INFOEX &info) + { + count = std::min(count, size_t(info.dwSize.X - info.dwCursorPosition.X)); + for (size_t i = 0; i < count; ++i) { + write_wchar(s[i]); + } + info.dwCursorPosition.X += count; + cursor_pos = info.dwCursorPosition; + } + + void write_text_info(const wchar_t *s, CONSOLE_SCREEN_BUFFER_INFOEX &info) + { + size_t count = std::min(std::wcslen(s), size_t(info.dwSize.X - info.dwCursorPosition.X)); + for (size_t i = 0; i < count; ++i) { + write_wchar(s[i]); + } + info.dwCursorPosition.X += count; + cursor_pos = info.dwCursorPosition; + } + + void flush() + { + std::fflush(stdout); + } +}; + +struct ConsoleInput +{ + struct termios orig_termios; + int handle; + + enum Key { + KEY_NONE = -1, + KEY_ESCAPE = 0x1B, + KEY_ENTER = 0x0D, + KEY_UP = 0x100, + KEY_DOWN, + KEY_LEFT, + KEY_RIGHT, + }; + + ConsoleInput() : handle(STDIN_FILENO) + { + tcgetattr(STDIN_FILENO, &orig_termios); + } + + ~ConsoleInput() + { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + } + + void enable_raw() + { + struct termios raw = orig_termios; + raw.c_lflag &= ~(ECHO | ICANON | ISIG); + raw.c_iflag &= ~(IXON | ICRNL); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + } + + void enable_cooked() + { + struct termios cooked = orig_termios; + cooked.c_lflag |= ECHO | ICANON; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &cooked); + } + + DWORD get_mode() { return 0; } + void set_mode(DWORD) {} + + int read_key() + { + char c; + if (::read(STDIN_FILENO, &c, 1) != 1) return KEY_NONE; + + if (c == '\033') { + char seq[2]; + if (::read(STDIN_FILENO, &seq[0], 1) != 1) return KEY_ESCAPE; + if (::read(STDIN_FILENO, &seq[1], 1) != 1) return KEY_ESCAPE; + if (seq[0] == '[') { + switch (seq[1]) { + case 'A': return KEY_UP; + case 'B': return KEY_DOWN; + case 'C': return KEY_RIGHT; + case 'D': return KEY_LEFT; + } + } + return KEY_ESCAPE; + } + + if (c == '\r' || c == '\n') return KEY_ENTER; + if (c == 'q' || c == 'Q') return KEY_ESCAPE; + + return KEY_NONE; + } + + bool wait_for_input(int timeout_ms) + { + struct pollfd pfd; + pfd.fd = STDIN_FILENO; + pfd.events = POLLIN; + return poll(&pfd, 1, timeout_ms) > 0; + } + + void read_text(wchar_t *buffer, size_t count) + { + enable_cooked(); + std::printf("\033[?25h"); + std::fflush(stdout); + + char line[count]; + if (std::fgets(line, count, stdin)) { + size_t len = std::strlen(line); + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + for (size_t i = 0; i <= std::strlen(line); ++i) { + buffer[i] = static_cast(line[i]); + } + } else { + buffer[0] = L'\0'; + } + + enable_raw(); + std::printf("\033[?25l"); + std::fflush(stdout); + } + + void write_text(const wchar_t *) {} +}; + +struct CursorVisibilityObserver +{ + bool visible() + { + auto *dpy = get_display(); + if (!dpy) return true; + + auto *img = XFixesGetCursorImage(dpy); + if (!img) return true; + + bool vis = (img->width > 1 || img->height > 1); + XFree(img); + return vis; + } +}; + +struct CtrlSignalHandler +{ + CtrlSignalHandler() + { + struct sigaction sa = {}; + sa.sa_handler = [](int) { quit = true; }; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + struct sigaction sa_winch = {}; + sa_winch.sa_handler = [](int) { resize_flag = true; }; + sigaction(SIGWINCH, &sa_winch, nullptr); + } + + void done() {} + + static inline volatile sig_atomic_t quit = 0; + static inline volatile sig_atomic_t resize_flag = 0; +}; + +} + +#endif + } diff --git a/src/config.hpp b/src/config.hpp index 6bccd74..a192a54 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -3,6 +3,8 @@ #include #include "common.hpp" +#ifdef _WIN32 + struct Config { bool developer; @@ -64,3 +66,115 @@ struct Config WritePrivateProfileStructW(section, L"placement", &(*placement), sizeof(*placement), path); } }; + +#else + +struct Config +{ + bool developer; + int bind_left; + int bind_right; + int bind_speed; + int bind_cycle; + wchar_t rate[128]; + wchar_t sleep[128]; + wchar_t cl_yawspeed[128]; + wchar_t sensitivity[128]; + wchar_t cl_anglespeedkey[128]; + wchar_t m_yaw[128]; + bool enabled; + int current; + int selected; + + Config(const char *path) + { + developer = false; + bind_left = common::platform::DEFAULT_BIND_LEFT; + bind_right = common::platform::DEFAULT_BIND_RIGHT; + bind_speed = common::platform::DEFAULT_BIND_SPEED; + bind_cycle = common::platform::DEFAULT_BIND_CYCLE; + std::wcscpy(rate, L"1000"); + std::wcscpy(sleep, L"3500"); + std::wcscpy(cl_yawspeed, L"75 120 210"); + std::wcscpy(sensitivity, L"1.0"); + std::wcscpy(cl_anglespeedkey, L"0.67"); + std::wcscpy(m_yaw, L"0.022"); + enabled = true; + current = 0; + selected = 0; + + FILE *f = std::fopen(path, "r"); + if (!f) return; + + char line[512]; + while (std::fgets(line, sizeof(line), f)) { + char *eq = std::strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + char *value = eq + 1; + size_t len = std::strlen(value); + if (len > 0 && value[len - 1] == '\n') value[len - 1] = '\0'; + + if (std::strcmp(line, "developer") == 0) developer = std::atoi(value) == 1; + else if (std::strcmp(line, "bind_left") == 0) bind_left = std::atoi(value); + else if (std::strcmp(line, "bind_right") == 0) bind_right = std::atoi(value); + else if (std::strcmp(line, "bind_speed") == 0) bind_speed = std::atoi(value); + else if (std::strcmp(line, "bind_cycle") == 0) bind_cycle = std::atoi(value); + else if (std::strcmp(line, "rate") == 0) str_to_wcs(value, rate, std::size(rate)); + else if (std::strcmp(line, "sleep") == 0) str_to_wcs(value, sleep, std::size(sleep)); + else if (std::strcmp(line, "cl_yawspeed") == 0) str_to_wcs(value, cl_yawspeed, std::size(cl_yawspeed)); + else if (std::strcmp(line, "sensitivity") == 0) str_to_wcs(value, sensitivity, std::size(sensitivity)); + else if (std::strcmp(line, "cl_anglespeedkey") == 0) str_to_wcs(value, cl_anglespeedkey, std::size(cl_anglespeedkey)); + else if (std::strcmp(line, "m_yaw") == 0) str_to_wcs(value, m_yaw, std::size(m_yaw)); + else if (std::strcmp(line, "enabled") == 0) enabled = std::atoi(value) == 1; + else if (std::strcmp(line, "current") == 0) current = std::atoi(value); + else if (std::strcmp(line, "selected") == 0) selected = std::atoi(value); + } + + std::fclose(f); + } + + void save(const char *path) + { + FILE *f = std::fopen(path, "w"); + if (!f) return; + + std::fprintf(f, "developer=%d\n", developer ? 1 : 0); + std::fprintf(f, "bind_left=%d\n", bind_left); + std::fprintf(f, "bind_right=%d\n", bind_right); + std::fprintf(f, "bind_speed=%d\n", bind_speed); + std::fprintf(f, "bind_cycle=%d\n", bind_cycle); + write_wcs_value(f, "rate", rate); + write_wcs_value(f, "sleep", sleep); + write_wcs_value(f, "cl_yawspeed", cl_yawspeed); + write_wcs_value(f, "sensitivity", sensitivity); + write_wcs_value(f, "cl_anglespeedkey", cl_anglespeedkey); + write_wcs_value(f, "m_yaw", m_yaw); + std::fprintf(f, "enabled=%d\n", enabled ? 1 : 0); + std::fprintf(f, "current=%d\n", current); + std::fprintf(f, "selected=%d\n", selected); + + std::fclose(f); + } + +private: + static void str_to_wcs(const char *src, wchar_t *dst, size_t count) + { + size_t i; + for (i = 0; src[i] && i < count - 1; ++i) { + dst[i] = static_cast(src[i]); + } + dst[i] = L'\0'; + } + + static void write_wcs_value(FILE *f, const char *key, const wchar_t *value) + { + std::fprintf(f, "%s=", key); + for (size_t i = 0; value[i]; ++i) { + std::fputc(static_cast(value[i]), f); + } + std::fputc('\n', f); + } +}; + +#endif diff --git a/src/mouse_move_state.hpp b/src/mouse_move_state.hpp index a344b86..6ba224c 100644 --- a/src/mouse_move_state.hpp +++ b/src/mouse_move_state.hpp @@ -6,7 +6,7 @@ struct MouseMoveState { long long update(bool reset, bool in_left, bool in_right, bool in_speed, double rate, double yawspeed, double anglespeedkey, double sensitivity, double yaw) { - auto time = common::win32::performance_counter(); + auto time = common::platform::performance_counter(); if (reset) { prev_in_left = false; @@ -21,7 +21,7 @@ struct MouseMoveState prev_in_left = in_left; prev_in_right = in_right; - if (!(in_left ^ in_right) || (time - last_time < common::win32::performance_counter_frequency() / rate)) { + if (!(in_left ^ in_right) || (time - last_time < common::platform::performance_counter_frequency() / rate)) { return 0; } @@ -31,7 +31,7 @@ struct MouseMoveState (yawspeed / (sensitivity * yaw)) * (in_speed ? anglespeedkey : 1.0) * (time - last_time) - ) / common::win32::performance_counter_frequency(); + ) / common::platform::performance_counter_frequency(); auto amount = static_cast(remaining); remaining -= amount; diff --git a/src/resources.hpp b/src/resources.hpp index 4cc87e1..d6135ff 100644 --- a/src/resources.hpp +++ b/src/resources.hpp @@ -2,6 +2,8 @@ #include "common.hpp" +#ifdef _WIN32 + struct VersionInfo { const wchar_t *name; @@ -32,3 +34,17 @@ inline auto load_icon_resource(HINSTANCE instance) { return static_cast(::LoadImageW(instance, MAKEINTRESOURCEW(1), IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR)); } + +#else + +struct VersionInfo +{ + const wchar_t *name = L"turnbinds"; + const wchar_t *title = L"turnbinds"; + const wchar_t *version = L"" VERSION_STRING; + const wchar_t *copyright = L"" COPYRIGHT_STRING; + + VersionInfo() {} +}; + +#endif diff --git a/src/turnbinds.cpp b/src/turnbinds.cpp index e93a324..053a050 100644 --- a/src/turnbinds.cpp +++ b/src/turnbinds.cpp @@ -1,7 +1,13 @@ #include #include #include + +#ifdef _WIN32 #include +#else +#include +#endif + #include "common.hpp" #include "resources.hpp" #include "config.hpp" @@ -182,7 +188,9 @@ struct State wchar_t cycle_vars_text[std::to_underlying(CycleVar::COUNT)][128]; ConsoleItem selected; +#ifdef _WIN32 std::optional placement; +#endif void on_config_loaded(Config &config); void on_config_save(Config &config); @@ -201,10 +209,11 @@ struct State struct Console { - win32::ConsoleOutput out; - win32::ConsoleInput in; + platform::ConsoleOutput out; + platform::ConsoleInput in; bool editing = false; +#ifdef _WIN32 Console(const VersionInfo &version_info) : out(CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr)), in(GetStdHandle(STD_INPUT_HANDLE)), @@ -222,6 +231,13 @@ struct Console { in.set_mode(initial_in_mode); } +#else + Console(const VersionInfo &version_info) : + version_info(version_info) + { + in.enable_raw(); + } +#endif void resize() { @@ -230,6 +246,9 @@ struct Console out.set_window_info(true, {0, 0, BUFFER_WIDTH - 1, static_cast(BUFFER_HEIGHT[g_state.developer] - 1)}); out.set_buffer_size({BUFFER_WIDTH, static_cast(BUFFER_HEIGHT[g_state.developer])}); out.set_window_info(true, {0, 0, BUFFER_WIDTH - 1, static_cast(BUFFER_HEIGHT[g_state.developer] - 1)}); +#ifndef _WIN32 + redraw_full(); +#endif } void on_developer_changed() @@ -260,13 +279,14 @@ struct Console } private: +#ifdef _WIN32 struct { - std::array down; + std::array down; void start() { for (int vk = 0; vk < std::size(down); ++vk) { - down[vk] = win32::is_key_down(vk); + down[vk] = platform::is_key_down(vk); } } @@ -278,7 +298,7 @@ struct Console } auto prev_down = down[vk]; - down[vk] = win32::is_key_down(vk); + down[vk] = platform::is_key_down(vk); if (!prev_down && down[vk]) { auto scan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC_EX); if (scan != 0 && vk != MapVirtualKeyW(scan, MAPVK_VSC_TO_VK_EX)) { @@ -291,8 +311,58 @@ struct Console return std::nullopt; } } bind_capturer; +#else + struct { + bool active = false; + + void start() + { + auto *dpy = platform::get_display(); + if (!dpy) return; + Window root = DefaultRootWindow(dpy); + XGrabKeyboard(dpy, root, True, GrabModeAsync, GrabModeAsync, CurrentTime); + XGrabPointer(dpy, root, True, ButtonPressMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + XFlush(dpy); + active = true; + } + + std::optional run() + { + if (!active) return std::nullopt; + auto *dpy = platform::get_display(); + if (!dpy) return std::nullopt; + + XEvent ev; + while (XPending(dpy)) { + XNextEvent(dpy, &ev); + if (ev.type == KeyPress) { + auto ks = XkbKeycodeToKeysym(dpy, ev.xkey.keycode, 0, 0); + stop(); + return static_cast(ks); + } + if (ev.type == ButtonPress) { + stop(); + return platform::MOUSE_BUTTON_BASE | ev.xbutton.button; + } + } + return std::nullopt; + } + + void stop() + { + if (!active) return; + auto *dpy = platform::get_display(); + if (!dpy) return; + XUngrabKeyboard(dpy, CurrentTime); + XUngrabPointer(dpy, CurrentTime); + XFlush(dpy); + active = false; + } + } bind_capturer; +#endif public: +#ifdef _WIN32 bool run() { bool ignore_input_events = false; @@ -416,6 +486,104 @@ struct Console return true; } +#else + bool run() + { + if (platform::CtrlSignalHandler::resize_flag) { + platform::CtrlSignalHandler::resize_flag = 0; + redraw_full(); + } + + if (editing && console_item_type(g_state.selected) == ConsoleItemType::BIND) { + auto captured = bind_capturer.run(); + if (captured) { + if (*captured != static_cast(XK_Escape)) { + g_state.binds[std::to_underlying(to_bind(g_state.selected))] = *captured; + } + editing = false; + redraw_item_value(g_state.selected); + } + return true; + } + + int key; + while ((key = in.read_key()) != platform::ConsoleInput::KEY_NONE) { + switch (key) { + case platform::ConsoleInput::KEY_ESCAPE: + return false; + case platform::ConsoleInput::KEY_UP: + case platform::ConsoleInput::KEY_DOWN: + { + bool down = (key == platform::ConsoleInput::KEY_DOWN); + auto prev = g_state.selected; + do { + g_state.selected = static_cast((down ? (std::to_underlying(g_state.selected) + 1) : (std::to_underlying(g_state.selected) - 1 + std::to_underlying(ConsoleItem::COUNT) + 1)) % (std::to_underlying(ConsoleItem::COUNT) + 1)); + } while (!g_state.developer && is_developer_console_item(g_state.selected)); + g_state.on_selected_changed(prev); + } + break; + case platform::ConsoleInput::KEY_LEFT: + case platform::ConsoleInput::KEY_RIGHT: + { + bool right = (key == platform::ConsoleInput::KEY_RIGHT); + switch (console_item_type(g_state.selected)) { + case ConsoleItemType::CYCLE_VAR: + case ConsoleItemType::COUNT: + if (g_state.count > 0) { + g_state.current = (right ? (g_state.current + 1) : (g_state.current - 1 + g_state.count)) % g_state.count; + g_state.on_current_changed(); + } + break; + case ConsoleItemType::SWITCH: + { + auto switch_ = to_switch(g_state.selected); + g_state.switches[std::to_underlying(switch_)] = right; + g_state.on_switch_changed(switch_); + } + break; + } + } + break; + case platform::ConsoleInput::KEY_ENTER: + switch (console_item_type(g_state.selected)) { + case ConsoleItemType::BIND: + editing = true; + redraw_item_value(g_state.selected); + bind_capturer.start(); + break; + case ConsoleItemType::VAR: + editing = true; + read_input_value(g_state.selected); + editing = false; + g_state.on_var_changed(to_var(g_state.selected)); + redraw_full(); + break; + case ConsoleItemType::CYCLE_VAR: + editing = true; + for (int i = 0; i < std::to_underlying(ConsoleItem::COUNT); ++i) { + switch (console_item_type(static_cast(i))) { + case ConsoleItemType::CYCLE_VAR: + redraw_item_value(static_cast(i)); + break; + } + } + read_input_value(g_state.selected); + editing = false; + g_state.on_cycle_vars_changed(); + redraw_full(); + break; + case ConsoleItemType::COUNT: + g_state.developer = !g_state.developer; + g_state.on_developer_changed(); + break; + } + break; + } + } + + return true; + } +#endif private: static constexpr const wchar_t *CONSOLE_ITEM_NAMES[] = { @@ -431,18 +599,21 @@ struct Console L"rate", L"sleep" }; - static constexpr const wchar_t *SELECTOR[] = {L" ", L" ยป "}; + static constexpr const wchar_t *SELECTOR[] = {L" ", L" \u00BB "}; static constexpr SHORT BUFFER_WIDTH = 64; static constexpr SHORT BUFFER_HEIGHT[] = {17, 20}; static constexpr auto INPUT_PADDING = 18; const VersionInfo &version_info; +#ifdef _WIN32 DWORD initial_in_mode; +#endif COORD positions_selector[std::to_underlying(ConsoleItem::COUNT)]; COORD positions_value[std::to_underlying(ConsoleItem::COUNT)]; +#ifdef _WIN32 void read_input_value(ConsoleItem item) { auto prev_out_mode = out.get_mode(); @@ -485,6 +656,45 @@ struct Console out.set_cursor_position(positions_value[std::to_underlying(item)]); } +#else + void read_input_value(ConsoleItem item) + { + auto prev_cursor_info = out.get_cursor_info(); + + { + auto info = prev_cursor_info; + info.bVisible = true; + out.set_cursor_info(info); + } + + out.set_cursor_position(positions_value[std::to_underlying(item)]); + out.flush(); + + wchar_t *text; + size_t count; + switch (console_item_type(item)) { + case ConsoleItemType::VAR: + text = g_state.vars_text[std::to_underlying(to_var(item))]; + count = std::size(g_state.vars_text[0]); + break; + case ConsoleItemType::CYCLE_VAR: + text = g_state.cycle_vars_text[std::to_underlying(to_cycle_var(item))]; + count = std::size(g_state.cycle_vars_text[0]); + break; + } + + wchar_t buffer[count]; + in.read_text(buffer, count); + + auto *stripped = wcsstrip(buffer); + if (*stripped != L'\0') { + std::wcscpy(text, stripped); + } + + out.set_cursor_info(prev_cursor_info); + out.set_cursor_position(positions_value[std::to_underlying(item)]); + } +#endif void redraw_full() { @@ -534,7 +744,7 @@ struct Console if (!valid) { out.set_text_attributes(FOREGROUND_INTENSITY | FOREGROUND_RED); } - std::swprintf(buffer, count, L"%-*s", INPUT_PADDING, CONSOLE_ITEM_NAMES[i]); + std::swprintf(buffer, count, L"%-*ls", INPUT_PADDING, CONSOLE_ITEM_NAMES[i]); out.write_text_info(buffer, info); if (!valid) { out.set_text_attributes(info.wAttributes); @@ -557,14 +767,14 @@ struct Console out.set_text_attributes(FOREGROUND_RED | FOREGROUND_GREEN); wchar_t title[BUFFER_WIDTH + 1]; - std::swprintf(title, std::size(title), L"%s %s", version_info.title, version_info.version); - std::swprintf(buffer, count, L"%*s", info.dwSize.X - 2, title); + std::swprintf(title, std::size(title), L"%ls %ls", version_info.title, version_info.version); + std::swprintf(buffer, count, L"%*ls", info.dwSize.X - 2, title); out.set_cursor_position(info.dwCursorPosition = {0, static_cast(info.dwCursorPosition.Y + 1)}); out.write_text_info(buffer, info); out.fill(L' ', info.dwCursorPosition, info.dwSize.X - info.dwCursorPosition.X); - std::swprintf(buffer, count, L"%*s", info.dwSize.X - 2, version_info.copyright); + std::swprintf(buffer, count, L"%*ls", info.dwSize.X - 2, version_info.copyright); out.set_cursor_position(info.dwCursorPosition = {0, static_cast(info.dwCursorPosition.Y + 1)}); out.write_text_info(buffer, info); @@ -575,6 +785,10 @@ struct Console out.fill(L' ', info.dwCursorPosition, info.dwSize.X * info.dwSize.Y); out.set_cursor_position(initial_cursor); + +#ifndef _WIN32 + out.flush(); +#endif } void redraw_item_value(ConsoleItem item) @@ -583,6 +797,9 @@ struct Console auto info = out.get_screen_buffer_info(); draw_item_value(item, info); out.fill(L' ', info.dwCursorPosition, info.dwSize.X - info.dwCursorPosition.X); +#ifndef _WIN32 + out.flush(); +#endif } void redraw_selector(ConsoleItem prev) @@ -596,6 +813,9 @@ struct Console out.set_cursor_position(positions_selector[std::to_underlying(g_state.selected)]); out.write_text(SELECTOR[true]); } +#ifndef _WIN32 + out.flush(); +#endif } void draw_item_value(ConsoleItem item, CONSOLE_SCREEN_BUFFER_INFOEX &info) @@ -611,7 +831,7 @@ struct Console auto bind = to_bind(item); wchar_t buffer[BUFFER_WIDTH + 1]; - if (!win32::get_vk_string(g_state.binds[std::to_underlying(bind)], buffer, std::size(buffer))) { + if (!platform::get_vk_string(g_state.binds[std::to_underlying(bind)], buffer, std::size(buffer))) { buffer[0] = L'\0'; } @@ -696,6 +916,7 @@ struct App inline static std::optional version_info; inline static std::optional console; +#ifdef _WIN32 static void run() { auto instance = GetModuleHandle(nullptr); @@ -707,7 +928,7 @@ struct App SetEnvironmentVariableW(L"__TURNBINDS_CONHOST", L""); wchar_t command_line[MAX_PATH + 1 + std::size(image_path)]; - std::swprintf(command_line, std::size(command_line), L"conhost.exe %s", image_path); + std::swprintf(command_line, std::size(command_line), L"conhost.exe %ls", image_path); STARTUPINFOW startup_info = {}; startup_info.cb = sizeof(STARTUPINFOW); @@ -755,8 +976,8 @@ struct App SendMessage(console_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(icon)); SetWindowLong(console_hwnd, GWL_STYLE, GetWindowLong(console_hwnd, GWL_STYLE) & ~WS_SIZEBOX & ~WS_MAXIMIZEBOX); - win32::CursorVisibilityObserver cursor_visibility_observer; - win32::CtrlSignalHandler ctrl_signal_handler(hwnd); + platform::CursorVisibilityObserver cursor_visibility_observer; + platform::CtrlSignalHandler ctrl_signal_handler(hwnd); Config config(ini_path, version_info->name); g_state.on_config_loaded(config); @@ -769,7 +990,7 @@ struct App ShowWindow(console_hwnd, SW_SHOWNORMAL); SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); - win32::set_timer_resolution(1); + platform::set_timer_resolution(1); bool active = false; std::array binds_down; @@ -806,13 +1027,13 @@ struct App decltype(binds_down) prev_binds_down; if (!prev_active) { for (int i = 0; i < std::size(g_state.binds); ++i) { - binds_down[i] = win32::is_key_down(g_state.binds[i]); + binds_down[i] = platform::is_key_down(g_state.binds[i]); } prev_binds_down = binds_down; } else { prev_binds_down = binds_down; for (int i = 0; i < std::size(g_state.binds); ++i) { - binds_down[i] = win32::is_key_down(g_state.binds[i]); + binds_down[i] = platform::is_key_down(g_state.binds[i]); } } @@ -832,10 +1053,10 @@ struct App g_state.cycle_vars[std::to_underlying(CycleVar::SENSITIVITY)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::SENSITIVITY)].size()], g_state.cycle_vars[std::to_underlying(CycleVar::YAW)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::YAW)].size()]); if (amount != 0) { - win32::move_mouse_by(amount, 0); + platform::move_mouse_by(amount, 0); } - win32::delay_execution_by(*g_state.vars[std::to_underlying(Var::SLEEP)]); + platform::delay_execution_by(*g_state.vars[std::to_underlying(Var::SLEEP)]); } while (true); console.reset(); @@ -848,12 +1069,119 @@ struct App ctrl_signal_handler.done(); } +#else + static void run() + { + version_info.emplace(); + + char exe_path[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len <= 0) { + std::strcpy(exe_path, "turnbinds"); + } else { + exe_path[len] = '\0'; + } + + char ini_path[PATH_MAX]; + std::strcpy(ini_path, exe_path); + char *dot = std::strrchr(ini_path, '.'); + if (dot && std::strchr(dot, '/') == nullptr) { + std::strcpy(dot, ".ini"); + } else { + std::strcat(ini_path, ".ini"); + } + + console.emplace(*version_info); + + platform::CursorVisibilityObserver cursor_visibility_observer; + platform::CtrlSignalHandler ctrl_signal_handler; + + Config config(ini_path); + g_state.on_config_loaded(config); + + console->resize(); + reset_console_window_title(); + + (void)nice(-5); + platform::set_timer_resolution(1); + + bool active = false; + std::array binds_down; + MouseMoveState mouse_move_state; + + while (!platform::CtrlSignalHandler::quit) { + if (!console->run()) { + break; + } + + auto prev_active = active; + active = + !console->editing && + g_state.switches[std::to_underlying(Switch::ENABLED)] && + g_state.valid && + !cursor_visibility_observer.visible(); + + if (!active) { + console->in.wait_for_input(100); + continue; + } + + decltype(binds_down) prev_binds_down; + if (!prev_active) { + for (int i = 0; i < std::size(g_state.binds); ++i) { + binds_down[i] = platform::is_key_down(g_state.binds[i]); + } + prev_binds_down = binds_down; + } else { + prev_binds_down = binds_down; + for (int i = 0; i < std::size(g_state.binds); ++i) { + binds_down[i] = platform::is_key_down(g_state.binds[i]); + } + } + + if (!prev_binds_down[std::to_underlying(Bind::CYCLE)] && binds_down[std::to_underlying(Bind::CYCLE)]) { + g_state.current = (g_state.current + 1) % g_state.count; + g_state.on_current_changed(); + } + + auto amount = mouse_move_state.update( + !prev_active, + binds_down[std::to_underlying(Bind::LEFT)], + binds_down[std::to_underlying(Bind::RIGHT)], + binds_down[std::to_underlying(Bind::SPEED)], + *g_state.vars[std::to_underlying(Var::RATE)], + g_state.cycle_vars[std::to_underlying(CycleVar::YAWSPEED)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::YAWSPEED)].size()], + g_state.cycle_vars[std::to_underlying(CycleVar::ANGLESPEEDKEY)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::ANGLESPEEDKEY)].size()], + g_state.cycle_vars[std::to_underlying(CycleVar::SENSITIVITY)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::SENSITIVITY)].size()], + g_state.cycle_vars[std::to_underlying(CycleVar::YAW)][g_state.current % g_state.cycle_vars[std::to_underlying(CycleVar::YAW)].size()]); + if (amount != 0) { + platform::move_mouse_by(amount, 0); + } + + platform::delay_execution_by(*g_state.vars[std::to_underlying(Var::SLEEP)]); + } + + console.reset(); + + g_state.on_config_save(config); + config.save(ini_path); + + ctrl_signal_handler.done(); + } +#endif static void reset_console_window_title() { wchar_t buffer[128]; - std::swprintf(buffer, std::size(buffer), L"%s%s", version_info->title, (!g_state.valid ? L" (error)" : (g_state.switches[std::to_underlying(Switch::ENABLED)] ? L"" : L" (disabled)"))); + std::swprintf(buffer, std::size(buffer), L"%ls%ls", version_info->title, (!g_state.valid ? L" (error)" : (g_state.switches[std::to_underlying(Switch::ENABLED)] ? L"" : L" (disabled)"))); +#ifdef _WIN32 SetConsoleTitleW(buffer); +#else + std::printf("\033]0;"); + for (int i = 0; buffer[i]; ++i) std::putchar(static_cast(buffer[i])); + std::printf("\007"); + std::fflush(stdout); +#endif } }; @@ -873,7 +1201,9 @@ void State::on_config_loaded(Config &config) g_state.switches[std::to_underlying(Switch::ENABLED)] = config.enabled; g_state.current = config.current; g_state.selected = static_cast(config.selected); +#ifdef _WIN32 g_state.placement = config.placement; +#endif for (int i = 0; i < std::to_underlying(Var::COUNT); ++i) { parse_var(static_cast(i)); @@ -910,7 +1240,9 @@ void State::on_config_save(Config &config) config.enabled = g_state.switches[std::to_underlying(Switch::ENABLED)]; config.current = g_state.current; config.selected = std::to_underlying(g_state.selected); +#ifdef _WIN32 config.placement = g_state.placement; +#endif } void State::on_developer_changed()