Files
prosperon/input/router.cm
2026-01-21 09:05:02 -06:00

315 lines
8.2 KiB
Plaintext

// Router - Pipeline for processing input through stages
var time = use('time')
// Valid emacs keys
var valid_emacs_keys = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'return', 'enter', 'space', 'escape', 'tab', 'backspace', 'delete',
'up', 'down', 'left', 'right', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
]
var emacs_special = {
'return': 'RET', 'enter': 'RET', 'space': 'SPC', 'escape': 'ESC',
'tab': 'TAB', 'backspace': 'DEL', 'delete': 'delete'
}
// Gesture stage - detects swipes and pinches from touchpad
function gesture_stage(config) {
config = config || {}
var min_swipe = config.swipe_min_dist || 30
var max_time = config.swipe_max_time || 500
var pinch_th = config.pinch_threshold || 10
var touches = {}
var gesture_state = null
var start_dist = 0
function dist(p1, p2) {
var dx = p2[0] - p1[0]
var dy = p2[1] - p1[1]
return Math.sqrt(dx * dx + dy * dy)
}
return {
process: function(events) {
var output = []
for (var i = 0; i < length(events); i++) {
var ev = events[i]
// Only process gamepad touchpad events
if (ev.control != 'gamepad_touchpad') {
push(output, ev)
continue
}
var fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0)
if (ev.pressed) {
touches[fingerId] = {
pos: ev.pos,
startPos: ev.pos,
startTime: time.number()
}
var count = length(array(touches))
if (count == 1) gesture_state = 'single'
else if (count == 2) {
gesture_state = 'multi'
var fingers = Object.values(touches)
start_dist = dist(fingers[0].pos, fingers[1].pos)
}
}
else if (ev.kind == 'axis') {
if (touches[fingerId]) {
touches[fingerId].pos = ev.pos
var count = length(array(touches))
if (count == 2 && gesture_state == 'multi') {
var fingers = Object.values(touches)
var currentDist = dist(fingers[0].pos, fingers[1].pos)
var d = currentDist - start_dist
if (Math.abs(d) >= pinch_th / 100) {
push(output, {
kind: 'gesture',
device_id: ev.device_id,
control: d > 0 ? 'pinch_out' : 'pinch_in',
pressed: true,
released: false,
delta: d,
time: ev.time
})
start_dist = currentDist
}
}
}
}
else if (ev.released) {
if (touches[fingerId]) {
var touch = touches[fingerId]
var count = length(array(touches))
if (count == 1 && gesture_state == 'single') {
var dt = (time.number() - touch.startTime) * 1000
var dx = ev.pos[0] - touch.startPos[0]
var dy = ev.pos[1] - touch.startPos[1]
if (dt < max_time) {
var absX = Math.abs(dx), absY = Math.abs(dy)
if (absX > min_swipe / 100 || absY > min_swipe / 100) {
var dir = absX > absY
? (dx > 0 ? 'swipe_right' : 'swipe_left')
: (dy > 0 ? 'swipe_down' : 'swipe_up')
push(output, {
kind: 'gesture',
device_id: ev.device_id,
control: dir,
pressed: true,
released: false,
time: ev.time
})
}
}
}
delete touches[fingerId]
if (length(array(touches)) == 0) gesture_state = null
}
}
}
return output
}
}
}
// Emacs stage - converts keyboard input to emacs chords
function emacs_stage() {
var prefix = null
return {
process: function(events) {
var output = []
for (var i = 0; i < length(events); i++) {
var ev = events[i]
// Only process keyboard button events
if (ev.device_id != 'kbm' || ev.kind != 'button' || !ev.pressed) {
push(output, ev)
continue
}
if (find(valid_emacs_keys, ev.control) == null) {
push(output, ev)
continue
}
// Only process if we have modifiers OR waiting for chord
if (!ev.mods?.ctrl && !ev.mods?.alt && !prefix) {
push(output, ev)
continue
}
var notation = ""
if (ev.mods?.ctrl) notation += "C-"
if (ev.mods?.alt) notation += "M-"
if (length(ev.control) == 1) {
notation += lower(ev.control)
} else {
notation += emacs_special[ev.control] || ev.control
}
// Handle prefix keys
if (notation == "C-x" || notation == "C-c") {
prefix = notation
continue // Consume, don't output
}
// Complete chord if we have prefix
if (prefix) {
var chord = prefix + " " + notation
prefix = null
push(output, {
kind: 'chord',
device_id: ev.device_id,
control: chord,
pressed: true,
released: false,
time: ev.time
})
} else {
push(output, {
kind: 'chord',
device_id: ev.device_id,
control: notation,
pressed: true,
released: false,
time: ev.time
})
}
}
return output
}
}
}
// Action mapping stage - converts controls to named actions
function action_stage(bindings) {
var down = {}
return {
down: down,
process: function(events) {
var output = []
for (var i = 0; i < length(events); i++) {
var ev = events[i]
// Pass through non-button events
if (ev.kind != 'button' && ev.kind != 'chord' && ev.kind != 'gesture') {
push(output, ev)
continue
}
var actions = bindings.get_actions(ev.control)
if (length(actions) == 0) {
push(output, ev)
continue
}
for (var j = 0; j < length(actions); j++) {
var action = actions[j]
if (ev.pressed) down[action] = true
else if (ev.released) down[action] = false
push(output, {
kind: 'action',
device_id: ev.device_id,
control: action,
pressed: ev.pressed,
released: ev.released,
time: ev.time,
original: ev
})
}
}
return output
}
}
}
// Delivery stage - dispatches to possessed entity
function delivery_stage(user) {
return {
process: function(events) {
for (var i = 0; i < length(events); i++) {
var ev = events[i]
// Only deliver actions
if (ev.kind != 'action') continue
user.dispatch(ev.control, {
pressed: ev.pressed,
released: ev.released,
time: ev.time
})
}
return events
}
}
}
// Create router with pipeline
function make(user, config) {
config = config || {}
var stages = []
var action = null
if (config.gestures != false) {
push(stages, gesture_stage(config))
}
if (config.emacs != false) {
push(stages, emacs_stage())
}
action = action_stage(user.bindings)
push(stages, action)
push(stages, delivery_stage(user))
return {
stages: stages,
get down() { return action.down },
handle: function(canon) {
var events = [canon]
for (var i = 0; i < length(this.stages); i++) {
events = this.stages[i].process(events)
}
return events
}
}
}
return {
make: make,
gesture_stage: gesture_stage,
emacs_stage: emacs_stage,
action_stage: action_stage,
delivery_stage: delivery_stage
}