Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build

.idea/
.aider*
67 changes: 67 additions & 0 deletions disk/create_disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
import os
import tempfile
import fs
from pyfatfs.PyFat import PyFat

def create_fat12_image(output_path: str, input_file_path: str) -> None:
"""
Creates a 64 KB FAT12 image identical to the original mkdosfs + dd workflow.
No sudo required.
"""
image_size_for_calc = 2 * 1024 * 1024 # 2 MB — needed for correct geometry
final_image_size = 64 * 1024 # 64 KB — what DeskHop embeds
volume_label = "DESKHOP"

# Use a named temporary file so pyfatfs can open it by path
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_path = temp_file.name
temp_file.close()

try:
# 1. Zero-fill the 2 MB temporary image
with open(temp_path, "wb") as f:
f.write(bytes(image_size_for_calc))

# 2. Format as FAT12 using pyfatfs low-level API
formatter = PyFat(encoding="utf-8")
formatter.mkfs(
temp_path,
fat_type=12,
size=image_size_for_calc,
label=volume_label.ljust(11)[:11] # Ensure exactly 11 chars
)
formatter.close()

# 3. Mount with high-level fs.py and copy config.htm
fat_url = f"fat://{temp_path}"
with fs.open_fs(fat_url) as fat_fs:
with open(input_file_path, "rb") as src:
fat_fs.writebytes("config.htm", src.read())

# 4. Truncate to final 64 KB
with open(temp_path, "rb") as src:
data = src.read(final_image_size)

os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "wb") as dst:
dst.write(data)

finally:
# Always remove temp file, even on KeyboardInterrupt
try:
os.unlink(temp_path)
except OSError:
pass


if __name__ == "__main__":
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
output_file = os.path.join(project_root, "disk", "disk.img")
input_file = os.path.join(project_root, "webconfig", "config.htm")

if not os.path.isfile(input_file):
raise FileNotFoundError(f"Config file not found: {input_file}")

create_fat12_image(output_file, input_file)
print(f"disk.img created successfully → {output_file}")
Binary file modified disk/disk.img
Binary file not shown.
2 changes: 2 additions & 0 deletions disk/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pyfatfs>=0.5.0
fs>=2.4.16
48 changes: 48 additions & 0 deletions misc/generate_disk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# Exit immediately if a command exits with a non-zero status.
set -e

# Get the directory of this script so we can use relative paths
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT_ROOT="$SCRIPT_DIR/.."
WEBCONFIG_DIR="$PROJECT_ROOT/webconfig"
DISK_DIR="$PROJECT_ROOT/disk"

# --- 0. Clean previous virtual environments ---
echo "--- Cleaning up old virtual environments ---"
rm -rf "$WEBCONFIG_DIR/venv"
rm -rf "$DISK_DIR/venv"
echo "Cleanup complete."
echo ""

# --- 1. Render the web configuration page ---
echo "--- Running Webconfig Renderer ---"
pushd "$WEBCONFIG_DIR" > /dev/null

VENV_DIR="venv"
echo "Creating new virtual environment..."
python3 -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
pip install -r requirements.txt

python3 -B render.py
deactivate
echo "Render complete."
popd > /dev/null
echo ""

# --- 2. Generate the disk image ---
echo "--- Generating Disk Image ---"
pushd "$DISK_DIR" > /dev/null

VENV_DIR="venv"
echo "Creating new virtual environment..."
python3 -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
pip install -r requirements.txt

python3 create_disk.py
deactivate
echo "Disk image generation complete."
popd > /dev/null
1 change: 1 addition & 0 deletions misc/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker-compose -f docker.yml run --rm build_container sh -c "rm -rf build && cmake -S . -B build -DDH_DEBUG=ON && cmake --build build"
38 changes: 24 additions & 14 deletions src/defaults.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@
const config_t default_config = {
.magic_header = 0xB00B1E5,
.version = CURRENT_CONFIG_VERSION,
.output[OUTPUT_A] =
{
.enforce_ports = ENFORCE_PORTS,
.force_kbd_boot_protocol = ENFORCE_KEYBOARD_BOOT_PROTOCOL,
.force_mouse_boot_mode = false,
.enable_acceleration = ENABLE_ACCELERATION,
.hotkey_toggle = HOTKEY_TOGGLE,
.kbd_led_as_indicator = KBD_LED_AS_INDICATOR,
.jump_threshold = JUMP_THRESHOLD,

.gaming_mode_on_boot = true,
.gaming_edge_enabled = false, // Global gaming edge switching disabled by default

.output = {
[OUTPUT_A] = {
.number = OUTPUT_A,
.speed_x = MOUSE_SPEED_A_FACTOR_X,
.speed_y = MOUSE_SPEED_A_FACTOR_Y,
Expand All @@ -32,10 +43,12 @@ const config_t default_config = {
.only_if_inactive = SCREENSAVER_A_ONLY_IF_INACTIVE,
.idle_time_us = (uint64_t)SCREENSAVER_A_IDLE_TIME_SEC * 1000000,
.max_time_us = (uint64_t)SCREENSAVER_A_MAX_TIME_SEC * 1000000,
}
},
.gaming_edge_threshold = GAMING_EDGE_THRESHOLD,
.gaming_edge_window_ms = GAMING_EDGE_WINDOW_MS,
.gaming_edge_max_vertical = GAMING_EDGE_MAX_VERTICAL,
},
.output[OUTPUT_B] =
{
[OUTPUT_B] = {
.number = OUTPUT_B,
.speed_x = MOUSE_SPEED_B_FACTOR_X,
.speed_y = MOUSE_SPEED_B_FACTOR_Y,
Expand All @@ -52,13 +65,10 @@ const config_t default_config = {
.only_if_inactive = SCREENSAVER_B_ONLY_IF_INACTIVE,
.idle_time_us = (uint64_t)SCREENSAVER_B_IDLE_TIME_SEC * 1000000,
.max_time_us = (uint64_t)SCREENSAVER_B_MAX_TIME_SEC * 1000000,
}
},
.enforce_ports = ENFORCE_PORTS,
.force_kbd_boot_protocol = ENFORCE_KEYBOARD_BOOT_PROTOCOL,
.force_mouse_boot_mode = false,
.enable_acceleration = ENABLE_ACCELERATION,
.hotkey_toggle = HOTKEY_TOGGLE,
.kbd_led_as_indicator = KBD_LED_AS_INDICATOR,
.jump_threshold = JUMP_THRESHOLD,
},
.gaming_edge_threshold = GAMING_EDGE_THRESHOLD,
.gaming_edge_window_ms = GAMING_EDGE_WINDOW_MS,
.gaming_edge_max_vertical = GAMING_EDGE_MAX_VERTICAL,
}
}
};
2 changes: 1 addition & 1 deletion src/include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#include "misc.h"
#include "screen.h"

#define CURRENT_CONFIG_VERSION 8
#define CURRENT_CONFIG_VERSION 9

/*==============================================================================
* Configuration Data
Expand Down
5 changes: 5 additions & 0 deletions src/include/screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ typedef struct {
uint8_t pos; // Screen position on this output
uint8_t mouse_park_pos; // Where the mouse goes after switch
screensaver_t screensaver; // Screensaver parameters for this output

// Gaming mode edge switching per output (except enabled flag which is global)
uint32_t gaming_edge_threshold;
uint32_t gaming_edge_window_ms;
uint32_t gaming_edge_max_vertical; // M2aximum allowed vertical movement
} output_t;
8 changes: 8 additions & 0 deletions src/include/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ typedef struct {
uint8_t enforce_ports;
uint16_t jump_threshold;

uint8_t gaming_mode_on_boot;
uint8_t gaming_edge_enabled; // Global enable for edge switching in gaming mode

output_t output[NUM_SCREENS];
uint32_t _reserved;

Expand Down Expand Up @@ -144,6 +147,11 @@ typedef struct {
bool config_mode_active; // True when config mode is active
bool digitizer_active; // True when digitizer Win/Mac workaround is active

/* Gaming mode edge switching state */
uint32_t gaming_edge_accum; // Accumulated horizontal movement toward edge in gaming mode
int32_t gaming_edge_vertical_accum; // Accumulated net vertical movement during edge detection (can be negative)
uint64_t gaming_edge_last_reset; // Timestamp of last accumulator reset

/* Onboard LED blinky (provide feedback when e.g. mouse connected) */
int32_t blinks_left; // How many blink transitions are left
int32_t last_led_change; // Timestamp of the last time led state transitioned
Expand Down
21 changes: 21 additions & 0 deletions src/include/user_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@
/* Mouse acceleration */
#define ENABLE_ACCELERATION 1

/**================================================== *
* =========== Gaming Edge Switching ============== *
* ================================================== *
*
* Gaming mode edge switching allows switching outputs in gaming mode
* by moving the mouse toward the edge of the screen.
*
* GAMING_EDGE_THRESHOLD: [0-4294967295], accumulated horizontal movement needed
* to trigger a switch (in mouse movement units)
* GAMING_EDGE_WINDOW_MS: [0-4294967295], time window in milliseconds for
* accumulating movement (max ~49 days)
* GAMING_EDGE_MAX_VERTICAL: [0-4294967295], maximum allowed vertical movement
* during edge detection (prevents accidental switches
* when moving mouse diagonally)
*
* */

#define GAMING_EDGE_THRESHOLD 20000
#define GAMING_EDGE_WINDOW_MS 1000
#define GAMING_EDGE_MAX_VERTICAL 2000

/**================================================== *
* ============== Screensaver Config ============== *
* ================================================== *
Expand Down
80 changes: 78 additions & 2 deletions src/mouse.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,12 @@ void switch_virtual_desktop(device_t *state, output_t *output, int new_index, in
void do_screen_switch(device_t *state, int direction) {
output_t *output = &state->config.output[state->active_output];

/* No switching allowed if explicitly disabled or in gaming mode */
if (state->switch_lock || state->gaming_mode)
/* No switching allowed if explicitly disabled */
if (state->switch_lock)
return;

/* In gaming mode, only allow switching if edge detection enabled and triggered */
if (state->gaming_mode && !state->config.gaming_edge_enabled)
return;

/* We want to jump in the direction of the other computer */
Expand Down Expand Up @@ -317,6 +321,66 @@ mouse_report_t create_mouse_report(device_t *state, mouse_values_t *values) {
return mouse_report;
}

enum screen_pos_e check_gaming_edge_switch(device_t *state, int offset_x, int offset_y) {
output_t *output = &state->config.output[state->active_output];

// Feature disabled - return early
if (!state->config.gaming_edge_enabled || !state->gaming_mode)
return NONE;

uint64_t now = time_us_64();
uint64_t window_us = (uint64_t)output->gaming_edge_window_ms * 1000;

// Determine which direction would switch screens
enum screen_pos_e switch_direction = (output->pos == LEFT) ? RIGHT : LEFT;

// Check if movement is toward the other PC
bool moving_toward_switch = (switch_direction == LEFT && offset_x < 0) ||
(switch_direction == RIGHT && offset_x > 0);

// Reset accumulators if:
// - Time window expired
// - Moving in opposite direction
// - Vertical movement exceeds maximum allowed
bool reset_needed = false;

if ((now - state->gaming_edge_last_reset) > window_us || !moving_toward_switch) {
reset_needed = true;
}

// Update vertical accumulation
state->gaming_edge_vertical_accum += offset_y;

uint32_t abs_vertical = (state->gaming_edge_vertical_accum < 0)
? -(uint32_t)state->gaming_edge_vertical_accum
: (uint32_t)state->gaming_edge_vertical_accum;
if (abs_vertical > output->gaming_edge_max_vertical) {
reset_needed = true;
}
if (reset_needed) {
state->gaming_edge_accum = 0;
state->gaming_edge_vertical_accum = 0;
state->gaming_edge_last_reset = now;

// If not moving toward switch, return early
if (!moving_toward_switch)
return NONE;
}

// Accumulate horizontal movement (use absolute value)
state->gaming_edge_accum += (uint32_t)abs(offset_x);

// Check if threshold exceeded
if (state->gaming_edge_accum >= output->gaming_edge_threshold) {
state->gaming_edge_accum = 0;
state->gaming_edge_vertical_accum = 0;
state->gaming_edge_last_reset = now;
return switch_direction;
}

return NONE;
}

void process_mouse_report(uint8_t *raw_report, int len, uint8_t itf, hid_interface_t *iface) {
mouse_values_t values = {0};
device_t *state = &global_state;
Expand All @@ -327,6 +391,18 @@ void process_mouse_report(uint8_t *raw_report, int len, uint8_t itf, hid_interfa
/* Calculate and update mouse pointer movement. */
enum screen_pos_e switch_direction = update_mouse_position(state, &values);

/* Check for gaming mode edge switching */
if (state->gaming_mode) {
// Use the acceleration-adjusted offset from update_mouse_position
output_t *current = &state->config.output[state->active_output];
uint8_t reduce_speed = state->mouse_zoom ? MOUSE_ZOOM_SCALING_FACTOR : 0;
float acceleration_factor = calculate_mouse_acceleration_factor(values.move_x, values.move_y);
int offset_x = round(values.move_x * acceleration_factor * (current->speed_x >> reduce_speed));
int offset_y = round(values.move_y * acceleration_factor * (current->speed_y >> reduce_speed));

switch_direction = check_gaming_edge_switch(state, offset_x, offset_y);
}

/* Create the report for the output PC based on the updated values */
mouse_report_t report = create_mouse_report(state, &values);

Expand Down
Loading