Files
prosperon/input/router.cm
2026-02-17 09:15:15 -06:00

343 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(cfg) {
var o = cfg || {}
var min_swipe = o.swipe_min_dist || 30
var max_time = o.swipe_max_time || 500
var pinch_th = o.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 = []
var i = 0
var ev = null
var fingerId = null
var count = 0
var fingers = null
var currentDist = 0
var d = 0
var touch = null
var dt = 0
var dx = 0
var dy = 0
var absX = 0
var absY = 0
var dir = null
for (i = 0; i < length(events); i++) {
ev = events[i]
// Only process gamepad touchpad events
if (ev.control != 'gamepad_touchpad') {
push(output, ev)
continue
}
fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0)
if (ev.pressed) {
touches[fingerId] = {
pos: ev.pos,
startPos: ev.pos,
startTime: time.number()
}
count = length(array(touches))
if (count == 1) gesture_state = 'single'
else if (count == 2) {
gesture_state = 'multi'
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
count = length(array(touches))
if (count == 2 && gesture_state == 'multi') {
fingers = Object.values(touches)
currentDist = dist(fingers[0].pos, fingers[1].pos)
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]) {
touch = touches[fingerId]
count = length(array(touches))
if (count == 1 && gesture_state == 'single') {
dt = (time.number() - touch.startTime) * 1000
dx = ev.pos[0] - touch.startPos[0]
dy = ev.pos[1] - touch.startPos[1]
if (dt < max_time) {
absX = Math.abs(dx)
absY = Math.abs(dy)
if (absX > min_swipe / 100 || absY > min_swipe / 100) {
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 = []
var i = 0
var ev = null
var notation = null
var chord = null
for (i = 0; i < length(events); i++) {
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
}
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) {
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 = []
var i = 0
var ev = null
var actions = null
var j = 0
var action = null
for (i = 0; i < length(events); i++) {
ev = events[i]
// Pass through non-button events
if (ev.kind != 'button' && ev.kind != 'chord' && ev.kind != 'gesture') {
push(output, ev)
continue
}
actions = bindings.get_actions(ev.control)
if (length(actions) == 0) {
push(output, ev)
continue
}
for (j = 0; j < length(actions); j++) {
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) {
var i = 0
var ev = null
for (i = 0; i < length(events); i++) {
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, cfg) {
var o = cfg || {}
var stages = []
var action = null
if (o.gestures != false) {
push(stages, gesture_stage(o))
}
if (o.emacs != false) {
push(stages, emacs_stage())
}
action = action_stage(user.bindings)
push(stages, action)
push(stages, delivery_stage(user))
return {
stages: stages,
down() { return action.down },
handle: function(canon) {
var events = [canon]
var i = 0
for (i = 0; i < length(this.stages); i++) {
events = this.stages[i].process(events)
}
return events
}
}
}
return {
make,
gesture_stage,
emacs_stage,
action_stage,
delivery_stage
}