@@ -6,8 +6,14 @@ import CoreGraphics
66
77// MARK: - Binding Configuration
88
9+ let configPath = NSString ( " ~/.config/codecontroller.json " ) . expandingTildeInPath
10+
11+ enum BindingType : String , Codable {
12+ case key, tap, mouse, leftClick, rightClick, cycleWindows
13+ }
14+
915struct Binding : Codable {
10- let type : String // "key", "tap", "mouse", "leftClick", "rightClick", "cycleWindows"
16+ let type : BindingType
1117 var key : String ? // keyboard key name (for "key" and "tap")
1218 var modifiers : [ String ] ? // "command"/"cmd", "shift", "option"/"alt", "control"/"ctrl"
1319 var button : Int ? // mouse button number (for "mouse")
@@ -32,7 +38,7 @@ let defaultConfigJSON = """
3238 " b " : { " key " : " escape " , " type " : " key " },
3339 " x " : { " key " : " backspace " , " type " : " key " },
3440 " y " : { " key " : " tab " , " type " : " key " },
35- " back " : { " key " : " o " , " type " : " key " , " modifiers " : [ " control " ] },
41+ " back " : { " key " : " o " , " type " : " tap " , " modifiers " : [ " control " ] },
3642 " menu " : { " key " : " / " , " type " : " key " },
3743 " dpad_up " : { " key " : " up " , " type " : " key " },
3844 " dpad_down " : { " key " : " down " , " type " : " key " },
@@ -42,16 +48,16 @@ let defaultConfigJSON = """
4248 " rs " : { " type " : " rightClick " },
4349
4450 " lt+a " : { " key " : " space " , " type " : " key " },
45- " lt+y " : { " key " : " tab " , " type " : " key " , " modifiers " : [ " shift " ] },
46- " lt+back " : { " key " : " e " , " type " : " key " , " modifiers " : [ " control " ] },
51+ " lt+y " : { " key " : " tab " , " type " : " tap " , " modifiers " : [ " shift " ] },
52+ " lt+back " : { " key " : " e " , " type " : " tap " , " modifiers " : [ " control " ] },
4753 " lt+dpad_up " : { " type " : " cycleWindows " },
4854 " lt+dpad_down " : { " type " : " cycleWindows " },
4955 " lt+dpad_left " : { " key " : " [ " , " type " : " tap " , " modifiers " : [ " command " , " shift " ] },
5056 " lt+dpad_right " : { " key " : " ] " , " type " : " tap " , " modifiers " : [ " command " , " shift " ] },
5157
52- " lb+b " : { " key " : " w " , " type " : " key " , " modifiers " : [ " command " ] },
53- " lb+y " : { " key " : " t " , " type " : " key " , " modifiers " : [ " command " ] },
54- " lb+back " : { " key " : " b " , " type " : " key " , " modifiers " : [ " control " ] }
58+ " lb+b " : { " key " : " w " , " type " : " tap " , " modifiers " : [ " command " ] },
59+ " lb+y " : { " key " : " t " , " type " : " tap " , " modifiers " : [ " command " ] },
60+ " lb+back " : { " key " : " b " , " type " : " tap " , " modifiers " : [ " control " ] }
5561 }
5662}
5763"""
@@ -100,22 +106,20 @@ func resolveModifiers(_ names: [String]) -> CGEventFlags {
100106
101107func describeBinding( _ b: Binding ) -> String {
102108 switch b. type {
103- case " key " , " tap " :
109+ case . key, . tap:
104110 let mods = ( b. modifiers ?? [ ] ) . map { $0. capitalized }
105111 let key = ( b. key ?? " ? " ) . capitalized
106112 return ( mods + [ key] ) . joined ( separator: " + " )
107- case " mouse " : return " Mouse \( b. button ?? 0 ) "
108- case " leftClick " : return " Left Click "
109- case " rightClick " : return " Right Click "
110- case " cycleWindows " : return " Cycle Windows "
111- default : return b. type
113+ case . mouse: return " Mouse \( b. button ?? 0 ) "
114+ case . leftClick: return " Left Click "
115+ case . rightClick: return " Right Click "
116+ case . cycleWindows: return " Cycle Windows "
112117 }
113118}
114119
115120// Load config from file, or write defaults
116121func loadConfig( ) -> Config {
117- let configDir = NSString ( " ~/.config " ) . expandingTildeInPath
118- let configPath = " \( configDir) /codecontroller.json "
122+ let configDir = ( configPath as NSString ) . deletingLastPathComponent
119123
120124 if FileManager . default. fileExists ( atPath: configPath) {
121125 do {
@@ -151,7 +155,6 @@ func loadConfig() -> Config {
151155 return try ! JSONDecoder ( ) . decode ( Config . self, from: defaultConfigJSON. data ( using: . utf8) !)
152156}
153157
154- let validBindingTypes = Set ( [ " key " , " tap " , " mouse " , " leftClick " , " rightClick " , " cycleWindows " ] )
155158let validButtonNames = Set ( [ " a " , " b " , " x " , " y " , " rt " , " rb " , " ls " , " rs " , " dpad_up " , " dpad_down " , " dpad_left " , " dpad_right " , " back " , " menu " ] )
156159let validPrefixes = [ " lt+ " , " lb+ " ]
157160
@@ -167,24 +170,19 @@ func validateBindings(_ config: [String: Binding]) -> [String] {
167170 warnings. append ( " \" \( name) \" : unknown button name \" \( baseName) \" " )
168171 }
169172
170- // Validate binding type
171- if !validBindingTypes. contains ( binding. type) {
172- warnings. append ( " \" \( name) \" : unknown type \" \( binding. type) \" " )
173- }
174-
175173 // Validate key/tap have a key
176- if ( binding. type == " key " || binding. type == " tap " ) {
174+ if binding. type == . key || binding. type == . tap {
177175 if let key = binding. key {
178176 if resolveKeyCode ( key) == nil {
179177 warnings. append ( " \" \( name) \" : unknown key name \" \( key) \" " )
180178 }
181179 } else {
182- warnings. append ( " \" \( name) \" : type \" \( binding. type) \" requires a \" key \" field " )
180+ warnings. append ( " \" \( name) \" : type \" \( binding. type. rawValue ) \" requires a \" key \" field " )
183181 }
184182 }
185183
186184 // Validate mouse button number
187- if binding. type == " mouse " {
185+ if binding. type == . mouse {
188186 if let btn = binding. button {
189187 if btn < 0 {
190188 warnings. append ( " \" \( name) \" : negative mouse button number \( btn) " )
@@ -224,29 +222,46 @@ var bindings = config.bindings
224222var loggingEnabled = config. settings? . logging ?? true
225223showConfigWarnings ( validateBindings ( bindings) )
226224
227- // MARK: - State Tracking
225+ // MARK: - Constants
228226
229- var buttonStates : [ String : Bool ] = [ : ]
230- var activeBindingObjects : [ String : Binding ] = [ : ] // button name -> binding that was activated
227+ // Mouse cursor (left stick)
228+ let mouseDeadzone : Float = 0.10
229+ let mouseSpeed : Double = 20.0
230+ let mouseAccel : Double = 0.35
231+ let mouseDecay : Double = 0.7
232+
233+ // Scroll (right stick)
234+ let scrollDeadzone : Float = 0.02
235+ let scrollSpeed : Float = 30.0
236+ let scrollAccel : Float = 0.15
237+ let scrollDecay : Float = 0.92
238+ let scrollFlat : Float = 1.0
239+ let scrollCurve : Float = 5.5
231240
232241// Universal mouse button fallback: every button combo gets a sequential number (10-51)
233242let canonicalOrder = [ " a " , " b " , " x " , " y " , " rt " , " rb " , " ls " , " rs " , " dpad_up " , " dpad_down " , " dpad_left " , " dpad_right " , " back " , " menu " ]
234243
235- func fallbackMouseButton( for name: String , ltHeld : Bool , lbHeld : Bool ) -> Int {
244+ func fallbackMouseButton( for name: String ) -> Int {
236245 let idx = canonicalOrder. firstIndex ( of: name) ?? 0
237- let layerOffset = lbHeld ? 28 : ( ltHeld ? 14 : 0 )
238- return 10 + layerOffset + idx
246+ return 10 + idx
239247}
240248
241- // Scroll momentum
249+ // Modifier flags attached to fallback mouse events to distinguish layers
250+ func fallbackModifiers( ltHeld: Bool , lbHeld: Bool ) -> CGEventFlags {
251+ if lbHeld { return . maskControl }
252+ if ltHeld { return . maskAlternate }
253+ return [ ]
254+ }
255+
256+ // MARK: - State
257+
258+ var buttonStates : [ String : Bool ] = [ : ]
259+ var activeBindingObjects : [ String : Binding ] = [ : ] // button name -> binding that was activated
260+ var activeMouseModifiers : [ String : CGEventFlags ] = [ : ] // button name -> modifier flags for fallback mouse
242261var scrollVelocityX : Float = 0
243262var scrollVelocityY : Float = 0
244-
245- // Mouse momentum
246263var mouseVelocityX : Double = 0
247264var mouseVelocityY : Double = 0
248-
249- // Mouse click state
250265var mouseDown = false
251266var rightMouseDown = false
252267
@@ -260,6 +275,8 @@ func pressKey(_ keyCode: CGKeyCode, down: Bool, modifiers: CGEventFlags = []) {
260275 event? . post ( tap: . cgSessionEventTap)
261276}
262277
278+ // Posts explicit modifier key-down/up events around the main key.
279+ // Required for system shortcuts (e.g. Cmd+Shift+[) that don't register from just setting CGEvent flags.
263280func tapKey( _ keyCode: CGKeyCode , modifiers: CGEventFlags = [ ] ) {
264281 let modifierKeys : [ ( CGEventFlags , CGKeyCode ) ] = [
265282 ( . maskCommand, 55 ) , ( . maskShift, 56 ) , ( . maskAlternate, 58 ) , ( . maskControl, 59 )
@@ -279,11 +296,12 @@ func tapKey(_ keyCode: CGKeyCode, modifiers: CGEventFlags = []) {
279296 }
280297}
281298
282- func pressMouseButton( _ button: CGMouseButton , down: Bool ) {
299+ func pressMouseButton( _ button: CGMouseButton , down: Bool , modifiers : CGEventFlags = [ ] ) {
283300 let location = CGEvent ( source: nil ) ? . location ?? . zero
284301 let eventType : CGEventType = down ? . otherMouseDown : . otherMouseUp
285302 if let event = CGEvent ( mouseEventSource: eventSource, mouseType: eventType, mouseCursorPosition: location, mouseButton: button) {
286303 event. setIntegerValueField ( . mouseEventButtonNumber, value: Int64 ( button. rawValue) )
304+ if !modifiers. isEmpty { event. flags = modifiers }
287305 event. post ( tap: . cgSessionEventTap)
288306 }
289307}
@@ -340,25 +358,23 @@ func cycleWindows() {
340358
341359// MARK: - Binding Dispatch
342360
343- func executeBinding( _ binding: Binding , down: Bool ) {
361+ func executeBinding( _ binding: Binding , down: Bool , mouseFlags : CGEventFlags = [ ] ) {
344362 switch binding. type {
345- case " key " :
363+ case . key:
346364 guard let name = binding. key, let code = resolveKeyCode ( name) else { return }
347365 pressKey ( code, down: down, modifiers: resolveModifiers ( binding. modifiers ?? [ ] ) )
348- case " tap " :
366+ case . tap:
349367 guard down, let name = binding. key, let code = resolveKeyCode ( name) else { return }
350368 tapKey ( code, modifiers: resolveModifiers ( binding. modifiers ?? [ ] ) )
351- case " mouse " :
352- guard let btn = binding. button, btn >= 0 else { return }
353- pressMouseButton ( CGMouseButton ( rawValue: UInt32 ( btn) ) !, down: down)
354- case " leftClick " :
369+ case . mouse:
370+ guard let btn = binding. button, btn >= 0 , btn <= 31 else { return }
371+ pressMouseButton ( CGMouseButton ( rawValue: UInt32 ( btn) ) !, down: down, modifiers : mouseFlags )
372+ case . leftClick:
355373 mouseClick ( down: down)
356- case " rightClick " :
374+ case . rightClick:
357375 rightClick ( down: down)
358- case " cycleWindows " :
376+ case . cycleWindows:
359377 if down { cycleWindows ( ) }
360- default :
361- break
362378 }
363379}
364380
@@ -379,20 +395,26 @@ func handleButton(_ name: String, pressed: Bool, ltHeld: Bool, lbHeld: Bool) {
379395
380396 // Use config binding if present, otherwise fall back to mouse button
381397 let binding : Binding
398+ let mouseFlags : CGEventFlags
382399 if let b = bindings [ bindingKey] {
383400 binding = b
401+ mouseFlags = [ ]
384402 } else {
385- let mouseBtn = fallbackMouseButton ( for: name, ltHeld: ltHeld, lbHeld: lbHeld)
386- binding = Binding ( type: " mouse " , key: nil , modifiers: nil , button: mouseBtn)
403+ let mouseBtn = fallbackMouseButton ( for: name)
404+ binding = Binding ( type: . mouse, key: nil , modifiers: nil , button: mouseBtn)
405+ mouseFlags = fallbackModifiers ( ltHeld: ltHeld, lbHeld: lbHeld)
387406 }
388407
389408 activeBindingObjects [ name] = binding
390- executeBinding ( binding, down: true )
391- print ( " \( bindingKey) -> \( describeBinding ( binding) ) " )
409+ activeMouseModifiers [ name] = mouseFlags
410+ executeBinding ( binding, down: true , mouseFlags: mouseFlags)
411+ let modDesc = mouseFlags. contains ( . maskControl) ? " +Ctrl " : mouseFlags. contains ( . maskAlternate) ? " +Opt " : " "
412+ print ( " \( bindingKey) -> \( describeBinding ( binding) ) \( modDesc) " )
392413 } else {
393414 // Release the same binding that was activated (prevents stuck keys)
394415 if let binding = activeBindingObjects. removeValue ( forKey: name) {
395- executeBinding ( binding, down: false )
416+ let mouseFlags = activeMouseModifiers. removeValue ( forKey: name) ?? [ ]
417+ executeBinding ( binding, down: false , mouseFlags: mouseFlags)
396418 }
397419 }
398420}
@@ -422,10 +444,6 @@ func pollController(_ gamepad: GCExtendedGamepad) {
422444 // --- Left stick: mouse movement with momentum ---
423445 let mouseX = gamepad. leftThumbstick. xAxis. value
424446 let mouseY = gamepad. leftThumbstick. yAxis. value
425- let mouseDeadzone : Float = 0.10
426- let mouseSpeed : Double = 20.0
427- let mouseAccel : Double = 0.35
428- let mouseDecay : Double = 0.7
429447
430448 if abs ( mouseX) > mouseDeadzone || abs ( mouseY) > mouseDeadzone {
431449 let curvedX = ( mouseX > 0 ? 1.0 : - 1.0 ) * pow( Double ( abs ( mouseX) ) , 1.5 ) * mouseSpeed
@@ -444,25 +462,20 @@ func pollController(_ gamepad: GCExtendedGamepad) {
444462 // --- Right stick: scrolling with momentum ---
445463 let scrollX = gamepad. rightThumbstick. xAxis. value
446464 let scrollY = gamepad. rightThumbstick. yAxis. value
447- let deadzone : Float = 0.02
448- let scrollSpeed : Float = 30.0
449- let accel : Float = 0.15
450-
451- if abs ( scrollX) > deadzone || abs ( scrollY) > deadzone {
452- let curvedX = ( scrollX > 0 ? 1.0 : - 1.0 ) * pow( abs ( scrollX) , 5.5 ) * scrollSpeed
453- let curvedY = ( scrollY > 0 ? 1.0 : - 1.0 ) * pow( abs ( scrollY) , 5.5 ) * scrollSpeed
454- scrollVelocityX += ( curvedX - scrollVelocityX) * accel
455- scrollVelocityY += ( curvedY - scrollVelocityY) * accel
465+
466+ if abs ( scrollX) > scrollDeadzone || abs ( scrollY) > scrollDeadzone {
467+ let curvedX = ( scrollX > 0 ? 1.0 : - 1.0 ) * pow( abs ( scrollX) , scrollCurve) * scrollSpeed
468+ let curvedY = ( scrollY > 0 ? 1.0 : - 1.0 ) * pow( abs ( scrollY) , scrollCurve) * scrollSpeed
469+ scrollVelocityX += ( curvedX - scrollVelocityX) * scrollAccel
470+ scrollVelocityY += ( curvedY - scrollVelocityY) * scrollAccel
456471 } else {
457- let decay : Float = 0.92
458- let flat : Float = 1.0
459- scrollVelocityX *= decay
460- scrollVelocityY *= decay
461- if scrollVelocityX > flat { scrollVelocityX -= flat }
462- else if scrollVelocityX < - flat { scrollVelocityX += flat }
472+ scrollVelocityX *= scrollDecay
473+ scrollVelocityY *= scrollDecay
474+ if scrollVelocityX > scrollFlat { scrollVelocityX -= scrollFlat }
475+ else if scrollVelocityX < - scrollFlat { scrollVelocityX += scrollFlat }
463476 else { scrollVelocityX = 0 }
464- if scrollVelocityY > flat { scrollVelocityY -= flat }
465- else if scrollVelocityY < - flat { scrollVelocityY += flat }
477+ if scrollVelocityY > scrollFlat { scrollVelocityY -= scrollFlat }
478+ else if scrollVelocityY < - scrollFlat { scrollVelocityY += scrollFlat }
466479 else { scrollVelocityY = 0 }
467480 }
468481
@@ -519,66 +532,18 @@ class AppActions: NSObject {
519532 guard let readmePath = Bundle . main. path ( forResource: " README " , ofType: " md " ) ,
520533 let md = try ? String ( contentsOfFile: readmePath, encoding: . utf8) ,
521534 let markedPath = Bundle . main. path ( forResource: " marked.min " , ofType: " js " ) ,
522- let markedJS = try ? String ( contentsOfFile: markedPath, encoding: . utf8) else { return }
523- let base64 = Data ( md. utf8) . base64EncodedString ( )
524- let page = ##"""
525- <!DOCTYPE html>
526- <html><head><meta charset="utf-8"><title>CodeController</title>
527- <script> \##( markedJS) </script>
528- <style>
529- * { box-sizing: border-box; margin: 0; padding: 0; }
530- body { font-family: 'SF Mono','Menlo','Monaco',monospace; background: #0a0a0a; color: #b0b0b0; font-size: 13px; line-height: 1.7; }
531- nav#toc { position: fixed; left: 0; top: 0; width: 200px; height: 100vh; overflow-y: auto; padding: 20px 14px; background: #050505; border-right: 1px solid #1a3a1a; z-index: 10; }
532- nav#toc .toc-title { color: #00ff41; font-size: 11px; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #1a3a1a; }
533- nav#toc a { display: block; color: #666; text-decoration: none; font-size: 11px; padding: 3px 0; transition: color 0.2s; }
534- nav#toc a:hover { color: #00ff41; }
535- nav#toc a.h3 { padding-left: 12px; font-size: 10px; color: #444; }
536- main#content { margin-left: 200px; max-width: 800px; padding: 40px 30px; }
537- h1 { color: #00ff41; border-bottom: 1px solid #1a3a1a; padding-bottom: 10px; font-size: 20px; text-transform: uppercase; letter-spacing: 3px; margin-bottom: 16px; }
538- h2 { color: #00cc33; border-bottom: 1px solid #1a2a1a; padding-bottom: 6px; margin-top: 36px; margin-bottom: 12px; font-size: 15px; letter-spacing: 1px; }
539- h3 { color: #00aa2a; margin-top: 24px; margin-bottom: 8px; font-size: 13px; }
540- p { margin: 8px 0; }
541- a { color: #00ff41; text-decoration: none; border-bottom: 1px solid #1a3a1a; }
542- a:hover { border-bottom-color: #00ff41; }
543- ul, ol { padding-left: 20px; margin: 8px 0; }
544- li { margin: 6px 0; }
545- table { margin: 12px 0; border-collapse: collapse; width: 100%; }
546- th { background: #111; color: #00ff41; text-align: left; padding: 8px 12px; border: 1px solid #1a3a1a; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
547- td { padding: 6px 12px; border: 1px solid #1a2a1a; color: #999; }
548- td:empty { background: #070707; }
549- tr:hover td { background: #0f1a0f; color: #ccc; }
550- code { background: #151515; color: #00ff41; padding: 2px 6px; border-radius: 3px; border: 1px solid #1a3a1a; font-size: 12px; }
551- pre { margin: 12px 0; }
552- pre code { display: block; padding: 14px; background: #0d0d0d; border: 1px solid #1a3a1a; border-radius: 4px; overflow-x: auto; color: #00dd38; border-left: 3px solid #00ff41; }
553- strong { color: #ddd; }
554- em { color: #777; font-style: normal; }
555- @media (max-width: 700px) { nav#toc { display: none; } main#content { margin-left: 0; } }
556- </style>
557- </head><body>
558- <nav id="toc"><div class="toc-title">CodeController</div></nav>
559- <main id="content"></main>
560- <script>
561- const raw = new TextDecoder().decode(Uint8Array.from(atob(' \##( base64) '), c => c.charCodeAt(0)));
562- document.getElementById('content').innerHTML = marked.parse(raw);
563- document.querySelectorAll('h2, h3').forEach(h => {
564- const id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
565- h.id = id;
566- const a = document.createElement('a');
567- a.href = '#' + id;
568- a.textContent = h.textContent;
569- a.className = h.tagName.toLowerCase();
570- document.getElementById('toc').appendChild(a);
571- });
572- </script>
573- </body></html>
574- """##
535+ let markedJS = try ? String ( contentsOfFile: markedPath, encoding: . utf8) ,
536+ let templatePath = Bundle . main. path ( forResource: " readme_template " , ofType: " html " ) ,
537+ let template = try ? String ( contentsOfFile: templatePath, encoding: . utf8) else { return }
538+ let page = template
539+ . replacingOccurrences ( of: " {{MARKED_JS}} " , with: markedJS)
540+ . replacingOccurrences ( of: " {{MARKDOWN_BASE64}} " , with: Data ( md. utf8) . base64EncodedString ( ) )
575541 let tmpPath = NSTemporaryDirectory ( ) + " codecontroller_readme.html "
576542 try ? page. write ( toFile: tmpPath, atomically: true , encoding: . utf8)
577543 NSWorkspace . shared. open ( URL ( fileURLWithPath: tmpPath) )
578544 }
579545
580546 @objc func editBindings( ) {
581- let configPath = NSString ( " ~/.config/codecontroller.json " ) . expandingTildeInPath
582547 NSWorkspace . shared. open ( URL ( fileURLWithPath: configPath) )
583548 }
584549
0 commit comments