kopia lustrzana https://github.com/abourget/shuttle-go
First draft.
commit
cffc6fd992
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2017 Alexandre Bourget
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,28 @@
|
||||||
|
Linux driver for Contour Design Shuttle Pro V2
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
My goal is to set it up for the Lightworks Non-Linear Editor.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Buttons layout on the Contour Design Shuttle Pro v2:
|
||||||
|
|
||||||
|
|
||||||
|
F1 F2 F3 F4
|
||||||
|
|
||||||
|
F5 F6 F7 F8 F9
|
||||||
|
|
||||||
|
|
||||||
|
(Shuttle)
|
||||||
|
S-7 .. S-1 S0 S1 .. S7
|
||||||
|
|
||||||
|
M1 JogL JogR M2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
B2 B3
|
||||||
|
B1 B4
|
||||||
|
|
||||||
|
|
||||||
|
See
|
|
@ -0,0 +1,133 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loadedConfiguration = &Config{}
|
||||||
|
var currentConfiguration *AppConfig
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Apps []*AppConfig `json:"apps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
MatchWindowTitles []string `json:"match_window_titles"`
|
||||||
|
windowTitleRegexps []*regexp.Regexp
|
||||||
|
Bindings map[string]string `json:"bindings"`
|
||||||
|
bindings []*deviceBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppConfig) parse() error {
|
||||||
|
if len(ac.MatchWindowTitles) == 0 {
|
||||||
|
ac.windowTitleRegexps = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`.*`),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, window := range ac.MatchWindowTitles {
|
||||||
|
re, err := regexp.Compile(window)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid regexp in window match %q: %s", window, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.windowTitleRegexps = append(ac.windowTitleRegexps, re)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceBinding struct {
|
||||||
|
// Input
|
||||||
|
heldButtons map[int]bool
|
||||||
|
buttonDown int
|
||||||
|
otherKey string
|
||||||
|
|
||||||
|
// Output
|
||||||
|
holdButtons []string
|
||||||
|
pressButton string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppConfig) parseBindings() error {
|
||||||
|
for key, value := range ac.Bindings {
|
||||||
|
newBinding := &deviceBinding{heldButtons: make(map[int]bool)}
|
||||||
|
|
||||||
|
// Input
|
||||||
|
input := strings.Split(key, "+")
|
||||||
|
for idx, part := range input {
|
||||||
|
cleanPart := strings.TrimSpace(part)
|
||||||
|
key := strings.ToUpper(cleanPart)
|
||||||
|
if shuttleKeys[key] == 0 && !otherShuttleKeysUpper[key] {
|
||||||
|
return fmt.Errorf("invalid shuttle device key map: %q doesn't exist", cleanPart)
|
||||||
|
}
|
||||||
|
if idx == len(input)-1 {
|
||||||
|
if shuttleKeys[key] != 0 {
|
||||||
|
newBinding.buttonDown = shuttleKeys[key]
|
||||||
|
} else {
|
||||||
|
newBinding.otherKey = key
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keyID := shuttleKeys[key]
|
||||||
|
if keyID == 0 {
|
||||||
|
return fmt.Errorf("binding %q, expects a button press, not a shuttle or jog movement")
|
||||||
|
}
|
||||||
|
newBinding.heldButtons[keyID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
output := strings.Split(value, "+")
|
||||||
|
for idx, part := range output {
|
||||||
|
cleanPart := strings.TrimSpace(part)
|
||||||
|
buttonName := strings.ToUpper(cleanPart)
|
||||||
|
if keyboardKeysUpper[buttonName] == 0 {
|
||||||
|
return fmt.Errorf("keyboard key unknown: %q", cleanPart)
|
||||||
|
}
|
||||||
|
if idx == len(output)-1 {
|
||||||
|
newBinding.pressButton = buttonName
|
||||||
|
} else {
|
||||||
|
newBinding.holdButtons = append(newBinding.holdButtons, buttonName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.bindings = append(ac.bindings, newBinding)
|
||||||
|
|
||||||
|
fmt.Printf("BINDING: %#v\n", newBinding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(filename string) error {
|
||||||
|
cnt, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{}
|
||||||
|
err = json.Unmarshal(cnt, &newConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range newConfig.Apps {
|
||||||
|
if err := app.parse(); err != nil {
|
||||||
|
return fmt.Errorf("Error parsing app %q's matchers: %s", app.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.parseBindings(); err != nil {
|
||||||
|
return fmt.Errorf("Error parsing app %q's bindings: %s", app.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedConfiguration = newConfig
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var shuttleKeys = map[string]int{
|
||||||
|
"F1": 256,
|
||||||
|
"F2": 257,
|
||||||
|
"F3": 258,
|
||||||
|
"F4": 259,
|
||||||
|
"F5": 260,
|
||||||
|
"F6": 261,
|
||||||
|
"F7": 262,
|
||||||
|
"F8": 263,
|
||||||
|
"F9": 264,
|
||||||
|
"B1": 267,
|
||||||
|
"B2": 265,
|
||||||
|
"B3": 266,
|
||||||
|
"B4": 268,
|
||||||
|
"M1": 269,
|
||||||
|
"M2": 270,
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherShuttleKeys = map[string]bool{
|
||||||
|
"S-7": true,
|
||||||
|
"S-6": true,
|
||||||
|
"S-5": true,
|
||||||
|
"S-4": true,
|
||||||
|
"S-3": true,
|
||||||
|
"S-2": true,
|
||||||
|
"S-1": true,
|
||||||
|
"S0": true,
|
||||||
|
"S1": true,
|
||||||
|
"S2": true,
|
||||||
|
"S3": true,
|
||||||
|
"S4": true,
|
||||||
|
"S5": true,
|
||||||
|
"S6": true,
|
||||||
|
"S7": true,
|
||||||
|
"JogL": true,
|
||||||
|
"JogR": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyboardKeys = map[string]int{
|
||||||
|
"Esc": 1,
|
||||||
|
"1": 2,
|
||||||
|
"2": 3,
|
||||||
|
"3": 4,
|
||||||
|
"4": 5,
|
||||||
|
"5": 6,
|
||||||
|
"6": 7,
|
||||||
|
"7": 8,
|
||||||
|
"8": 9,
|
||||||
|
"9": 10,
|
||||||
|
"0": 11,
|
||||||
|
"Minus": 12,
|
||||||
|
"-": 12,
|
||||||
|
"Equal": 13,
|
||||||
|
"=": 13,
|
||||||
|
"Backspace": 14,
|
||||||
|
"Tab": 15,
|
||||||
|
"Q": 16,
|
||||||
|
"W": 17,
|
||||||
|
"E": 18,
|
||||||
|
"R": 19,
|
||||||
|
"T": 20,
|
||||||
|
"Y": 21,
|
||||||
|
"U": 22,
|
||||||
|
"I": 23,
|
||||||
|
"O": 24,
|
||||||
|
"P": 25,
|
||||||
|
"LeftBrace": 26,
|
||||||
|
"RightBrace": 27,
|
||||||
|
"{": 26,
|
||||||
|
"}": 27,
|
||||||
|
"Enter": 28,
|
||||||
|
"LeftCtrl": 29,
|
||||||
|
"Ctrl": 29,
|
||||||
|
"A": 30,
|
||||||
|
"S": 31,
|
||||||
|
"D": 32,
|
||||||
|
"F": 33,
|
||||||
|
"G": 34,
|
||||||
|
"H": 35,
|
||||||
|
"J": 36,
|
||||||
|
"K": 37,
|
||||||
|
"L": 38,
|
||||||
|
"Semicolon": 39,
|
||||||
|
";": 39,
|
||||||
|
"Apostrophe": 40,
|
||||||
|
"'": 40,
|
||||||
|
"Grave": 41,
|
||||||
|
"LeftShift": 42,
|
||||||
|
"Shift": 42,
|
||||||
|
"Backslash": 43,
|
||||||
|
"\\": 43,
|
||||||
|
"Z": 44,
|
||||||
|
"X": 45,
|
||||||
|
"C": 46,
|
||||||
|
"V": 47,
|
||||||
|
"B": 48,
|
||||||
|
"N": 49,
|
||||||
|
"M": 50,
|
||||||
|
"Comma": 51,
|
||||||
|
",": 51,
|
||||||
|
"Dot": 52,
|
||||||
|
".": 52,
|
||||||
|
"Slash": 53,
|
||||||
|
"/": 53,
|
||||||
|
"RightShift": 54,
|
||||||
|
"RShift": 54,
|
||||||
|
"KPAsterisk": 55,
|
||||||
|
"*": 55,
|
||||||
|
"LeftAlt": 56,
|
||||||
|
"Alt": 56,
|
||||||
|
"Space": 57,
|
||||||
|
"CapsLock": 58,
|
||||||
|
"F1": 59,
|
||||||
|
"F2": 60,
|
||||||
|
"F3": 61,
|
||||||
|
"F4": 62,
|
||||||
|
"F5": 63,
|
||||||
|
"F6": 64,
|
||||||
|
"F7": 65,
|
||||||
|
"F8": 66,
|
||||||
|
"F9": 67,
|
||||||
|
"F10": 68,
|
||||||
|
"NumLock": 69,
|
||||||
|
"ScrollLock": 70,
|
||||||
|
"KP7": 71,
|
||||||
|
"KP8": 72,
|
||||||
|
"KP9": 73,
|
||||||
|
"KPMinus": 74,
|
||||||
|
"KP4": 75,
|
||||||
|
"KP5": 76,
|
||||||
|
"KP6": 77,
|
||||||
|
"KPPlus": 78,
|
||||||
|
"KP1": 79,
|
||||||
|
"KP2": 80,
|
||||||
|
"KP3": 81,
|
||||||
|
"KP0": 82,
|
||||||
|
"KPDot": 83,
|
||||||
|
"F11": 87,
|
||||||
|
"F12": 88,
|
||||||
|
"KPEnter": 96,
|
||||||
|
"RightCtrl": 97,
|
||||||
|
"RCtrl": 97,
|
||||||
|
"RightAlt": 100,
|
||||||
|
"RAlt": 100,
|
||||||
|
"Linefeed": 101,
|
||||||
|
"Home": 102,
|
||||||
|
"Up": 103,
|
||||||
|
"PageUp": 104,
|
||||||
|
"PgUp": 104,
|
||||||
|
"Left": 105,
|
||||||
|
"Right": 106,
|
||||||
|
"End": 107,
|
||||||
|
"Down": 108,
|
||||||
|
"PageDown": 109,
|
||||||
|
"PgDown": 109,
|
||||||
|
"PgDn": 109,
|
||||||
|
"Insert": 110,
|
||||||
|
"Delete": 111,
|
||||||
|
"Macro": 112,
|
||||||
|
"Mute": 113,
|
||||||
|
"VolumeDown": 114,
|
||||||
|
"VolumeUp": 115,
|
||||||
|
"Power": 116, /*ScSystemPowerDown*/
|
||||||
|
"KPEqual": 117,
|
||||||
|
"KPPlusMinus": 118,
|
||||||
|
"Pause": 119,
|
||||||
|
"Scale": 120, /*AlCompizScale(Expose)*/
|
||||||
|
"KPComma": 121,
|
||||||
|
"LeftMeta": 125,
|
||||||
|
"Meta": 125,
|
||||||
|
"RightMeta": 126,
|
||||||
|
"RMeta": 126,
|
||||||
|
"Compose": 127,
|
||||||
|
"Stop": 128, /*AcStop*/
|
||||||
|
"Again": 129,
|
||||||
|
"Props": 130, /*AcProperties*/
|
||||||
|
"Undo": 131, /*AcUndo*/
|
||||||
|
"Front": 132,
|
||||||
|
"Copy": 133, /*AcCopy*/
|
||||||
|
"Open": 134, /*AcOpen*/
|
||||||
|
"Paste": 135, /*AcPaste*/
|
||||||
|
"Find": 136, /*AcSearch*/
|
||||||
|
"Cut": 137, /*AcCut*/
|
||||||
|
"Help": 138, /*AlIntegratedHelpCenter*/
|
||||||
|
"Menu": 139, /*Menu(ShowMenu)*/
|
||||||
|
"Calc": 140, /*AlCalculator*/
|
||||||
|
"Setup": 141,
|
||||||
|
"Sleep": 142, /*ScSystemSleep*/
|
||||||
|
"Wakeup": 143, /*SystemWakeUp*/
|
||||||
|
"File": 144, /*AlLocalMachineBrowser*/
|
||||||
|
"SendFile": 145,
|
||||||
|
"DeleteFile": 146,
|
||||||
|
"Xfer": 147,
|
||||||
|
"Prog1": 148,
|
||||||
|
"Prog2": 149,
|
||||||
|
"WWW": 150, /*AlInternetBrowser*/
|
||||||
|
"Coffee": 152, /*AlTerminalLock/Screensaver*/
|
||||||
|
"Direction": 153,
|
||||||
|
"CycleWindows": 154,
|
||||||
|
"Mail": 155,
|
||||||
|
"Bookmarks": 156, /*AcBookmarks*/
|
||||||
|
"Computer": 157,
|
||||||
|
"Back": 158, /*AcBack*/
|
||||||
|
"Forward": 159, /*AcForward*/
|
||||||
|
"CloseCD": 160,
|
||||||
|
"EjectCD": 161,
|
||||||
|
"EjectCloseCD": 162,
|
||||||
|
"NextSong": 163,
|
||||||
|
"PlayPause": 164,
|
||||||
|
"PreviousSong": 165,
|
||||||
|
"StopCD": 166,
|
||||||
|
"Record": 167,
|
||||||
|
"Rewind": 168,
|
||||||
|
"Phone": 169, /*MediaSelectTelephone*/
|
||||||
|
"ISO": 170,
|
||||||
|
"Config": 171, /*AlConsumerControlConfiguration*/
|
||||||
|
"Homepage": 172, /*AcHome*/
|
||||||
|
"Refresh": 173, /*AcRefresh*/
|
||||||
|
"Exit": 174, /*AcExit*/
|
||||||
|
"Move": 175,
|
||||||
|
"Edit": 176,
|
||||||
|
"ScrollUp": 177,
|
||||||
|
"ScrollDown": 178,
|
||||||
|
"KPLeftParen": 179,
|
||||||
|
"(": 179,
|
||||||
|
"KPRightParen": 180,
|
||||||
|
")": 180,
|
||||||
|
"New": 181, /*AcNew*/
|
||||||
|
"Redo": 182, /*AcRedo/Repeat*/
|
||||||
|
"F13": 183,
|
||||||
|
"F14": 184,
|
||||||
|
"F15": 185,
|
||||||
|
"F16": 186,
|
||||||
|
"F17": 187,
|
||||||
|
"F18": 188,
|
||||||
|
"F19": 189,
|
||||||
|
"F20": 190,
|
||||||
|
"F21": 191,
|
||||||
|
"F22": 192,
|
||||||
|
"F23": 193,
|
||||||
|
"F24": 194,
|
||||||
|
"PlayCD": 200,
|
||||||
|
"PauseCD": 201,
|
||||||
|
"Prog3": 202,
|
||||||
|
"Prog4": 203,
|
||||||
|
"Dashboard": 204, /*AlDashboard*/
|
||||||
|
"Suspend": 205,
|
||||||
|
"Close": 206, /*AcClose*/
|
||||||
|
"Play": 207,
|
||||||
|
"FastForward": 208,
|
||||||
|
"Print": 210, /*AcPrint*/
|
||||||
|
"Camera": 212,
|
||||||
|
"Sound": 213,
|
||||||
|
"Question": 214,
|
||||||
|
"Email": 215,
|
||||||
|
"Chat": 216,
|
||||||
|
"Search": 217,
|
||||||
|
"Connect": 218,
|
||||||
|
"Finance": 219, /*AlCheckbook/Finance*/
|
||||||
|
"Sport": 220,
|
||||||
|
"Shop": 221,
|
||||||
|
"AltErase": 222,
|
||||||
|
"Cancel": 223, /*AcCancel*/
|
||||||
|
"BrightnessDown": 224,
|
||||||
|
"BrightnessUp": 225,
|
||||||
|
"Media": 226,
|
||||||
|
"Send": 231, /*AcSend*/
|
||||||
|
"Reply": 232, /*AcReply*/
|
||||||
|
"ForwardMail": 233, /*AcForwardMsg*/
|
||||||
|
"Save": 234, /*AcSave*/
|
||||||
|
"Documents": 235,
|
||||||
|
"BrightnessCycle": 243, /*BrightnessUp,AfterMaxIsMin*/
|
||||||
|
"BrightnessZero": 244, /*BrightnessOff,UseAmbient*/
|
||||||
|
"DisplayOff": 245, /*DisplayDeviceToOffState*/
|
||||||
|
"Rfkill": 247, /*KeyThatControlsAllRadios*/
|
||||||
|
"Micmute": 248, /*Mute/UnmuteTheMicrophone*/
|
||||||
|
}
|
||||||
|
|
||||||
|
//var reverseShuttleKeys map[int]string
|
||||||
|
var keyboardKeysUpper = map[string]int{}
|
||||||
|
var otherShuttleKeysUpper = map[string]bool{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// for k, v := range shuttleKeys {
|
||||||
|
// reverseShuttleKeys[v] = k
|
||||||
|
// }
|
||||||
|
for k, v := range keyboardKeys {
|
||||||
|
keyboardKeysUpper[strings.ToUpper(k)] = v
|
||||||
|
}
|
||||||
|
for k, v := range otherShuttleKeys {
|
||||||
|
otherShuttleKeysUpper[strings.ToUpper(k)] = v
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/bendahl/uinput"
|
||||||
|
"github.com/gvalkov/golang-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = flag.String("config", filepath.Join(os.Getenv("HOME"), ".shuttle-go.json"), "Location to the .shuttle-go.json configuration")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(flag.Args()) != 1 {
|
||||||
|
fmt.Println("Missing device name as parameter.\nExample: [program] /dev/input/by-id/usb-Contour_Design_ShuttlePRO_v2-event-if00\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := LoadConfig(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading configuration:", err)
|
||||||
|
os.Exit(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-window title change watcher
|
||||||
|
watcher := NewWindowWatcher()
|
||||||
|
if err := watcher.Setup(); err != nil {
|
||||||
|
fmt.Println("Error watching X window:", err)
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Run()
|
||||||
|
|
||||||
|
// Virtual keyboard
|
||||||
|
vk, err := uinput.CreateKeyboard("/dev/uinput", []byte("Shuttle Pro V2"))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Can't open dev:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuttle device event receiver
|
||||||
|
dev, err := evdev.Open(flag.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Couldn't open Shuttle device:", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("ready")
|
||||||
|
mapper := NewMapper(vk, dev)
|
||||||
|
for {
|
||||||
|
if err := mapper.Process(); err != nil {
|
||||||
|
fmt.Println("Error processing input events (continuing):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bendahl/uinput"
|
||||||
|
evdev "github.com/gvalkov/golang-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mapper receives events from the Shuttle devices, and maps (through
|
||||||
|
// configuration) to the Virtual Keyboard events.
|
||||||
|
type Mapper struct {
|
||||||
|
virtualKeyboard uinput.Keyboard
|
||||||
|
inputDevice *evdev.InputDevice
|
||||||
|
state buttonsState
|
||||||
|
}
|
||||||
|
|
||||||
|
type buttonsState struct {
|
||||||
|
jog int
|
||||||
|
shuttle int
|
||||||
|
buttonsHeld map[int]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMapper(virtualKeyboard uinput.Keyboard, inputDevice *evdev.InputDevice) *Mapper {
|
||||||
|
m := &Mapper{
|
||||||
|
virtualKeyboard: virtualKeyboard,
|
||||||
|
inputDevice: inputDevice,
|
||||||
|
}
|
||||||
|
m.state.buttonsHeld = make(map[int]bool)
|
||||||
|
m.state.jog = -1
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) Process() error {
|
||||||
|
evs, err := m.inputDevice.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("---")
|
||||||
|
m.dispatch(evs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) dispatch(evs []evdev.InputEvent) {
|
||||||
|
newJogVal := jogVal(evs)
|
||||||
|
if m.state.jog != newJogVal {
|
||||||
|
if m.state.jog != -1 {
|
||||||
|
// Trigger JL or JR if we're advancing or not..
|
||||||
|
delta := newJogVal - m.state.jog
|
||||||
|
if (delta > 0 || delta < -200) && (delta < 200) {
|
||||||
|
if err := m.EmitOther("JogR"); err != nil {
|
||||||
|
fmt.Println("Jog right:", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := m.EmitOther("JogL"); err != nil {
|
||||||
|
fmt.Println("Jog left:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.state.jog = newJogVal
|
||||||
|
}
|
||||||
|
|
||||||
|
newShuttleVal := shuttleVal(evs)
|
||||||
|
if m.state.shuttle != newShuttleVal {
|
||||||
|
keyName := fmt.Sprintf("S%d", newShuttleVal)
|
||||||
|
if err := m.EmitOther(keyName); err != nil {
|
||||||
|
fmt.Println("Shuttle movement %q: %s\n", keyName, err)
|
||||||
|
}
|
||||||
|
m.state.shuttle = newShuttleVal
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ev := range evs {
|
||||||
|
if ev.Type != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
heldButtons, lastDown := buttonVals(m.state.buttonsHeld, ev)
|
||||||
|
if lastDown != 0 {
|
||||||
|
modifiers := buttonsToModifiers(heldButtons, lastDown)
|
||||||
|
if err := m.EmitKeys(modifiers, lastDown); err != nil {
|
||||||
|
fmt.Println("Button press:", err)
|
||||||
|
}
|
||||||
|
// fmt.Printf("OUTPUT: Modifiers: %v, Just pressed: %d\n", modifiers, lastDown)
|
||||||
|
}
|
||||||
|
m.state.buttonsHeld = heldButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("TYPE: %d\tCODE: %d\tVALUE: %d\n", ev.Type, ev.Code, ev.Value)
|
||||||
|
// TODO: Lock on configuration changes
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) EmitOther(key string) error {
|
||||||
|
conf := currentConfiguration
|
||||||
|
if conf == nil {
|
||||||
|
return fmt.Errorf("No configuration for this Window")
|
||||||
|
}
|
||||||
|
|
||||||
|
upperKey := strings.ToUpper(key)
|
||||||
|
|
||||||
|
for _, binding := range conf.bindings {
|
||||||
|
if binding.otherKey == upperKey {
|
||||||
|
return m.executeBinding(binding.holdButtons, binding.pressButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("No bindings for those movements")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) EmitKeys(modifiers map[int]bool, keyDown int) error {
|
||||||
|
conf := currentConfiguration
|
||||||
|
if conf == nil {
|
||||||
|
return fmt.Errorf("No configuration for this Window")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, binding := range conf.bindings {
|
||||||
|
if reflect.DeepEqual(binding.heldButtons, modifiers) && binding.buttonDown == keyDown {
|
||||||
|
return m.executeBinding(binding.holdButtons, binding.pressButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("No binding for these keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) executeBinding(holdButtons []string, pressButton string) error {
|
||||||
|
fmt.Println("Executing bindings:", holdButtons, pressButton)
|
||||||
|
for _, button := range holdButtons {
|
||||||
|
if err := m.virtualKeyboard.KeyDown(keyboardKeysUpper[button]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.virtualKeyboard.KeyPress(keyboardKeysUpper[pressButton]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, button := range holdButtons {
|
||||||
|
if err := m.virtualKeyboard.KeyUp(keyboardKeysUpper[button]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jogVal(evs []evdev.InputEvent) int {
|
||||||
|
for _, ev := range evs {
|
||||||
|
if ev.Type == 2 && ev.Code == 7 {
|
||||||
|
return int(ev.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func shuttleVal(evs []evdev.InputEvent) int {
|
||||||
|
for _, ev := range evs {
|
||||||
|
if ev.Type == 2 && ev.Code == 8 {
|
||||||
|
return int(ev.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func buttonVals(current map[int]bool, ev evdev.InputEvent) (out map[int]bool, lastDown int) {
|
||||||
|
out = current
|
||||||
|
|
||||||
|
if ev.Value == 1 {
|
||||||
|
current[int(ev.Code)] = true
|
||||||
|
} else {
|
||||||
|
delete(current, int(ev.Code))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.Value == 1 {
|
||||||
|
lastDown = int(ev.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func buttonsToModifiers(held map[int]bool, buttonDown int) (out map[int]bool) {
|
||||||
|
out = make(map[int]bool)
|
||||||
|
for k := range held {
|
||||||
|
if k == buttonDown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[k] = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "Lightworks",
|
||||||
|
"match_window_titles": [
|
||||||
|
"^Lightworks$", ".*"
|
||||||
|
],
|
||||||
|
"bindings": {
|
||||||
|
"F5": "a",
|
||||||
|
"F6": "s",
|
||||||
|
"F7": "i",
|
||||||
|
"JogL": "KPLeftParen",
|
||||||
|
"JogR": "KPRightParen",
|
||||||
|
"S-7": "KP1+KP2",
|
||||||
|
"S-6": "KP1+KP3",
|
||||||
|
"S-5": "KP1+KP4",
|
||||||
|
"S-4": "KP1+KP5",
|
||||||
|
"S-3": "j",
|
||||||
|
"S-2": "KP1+KP7",
|
||||||
|
"S-1": "KP1+KP8",
|
||||||
|
"S0": "k",
|
||||||
|
"S1": "KP2+KP3",
|
||||||
|
"S2": "KP2+KP4",
|
||||||
|
"S3": "l",
|
||||||
|
"S4": "KP2+KP6",
|
||||||
|
"S5": "KP2+KP7",
|
||||||
|
"S6": "KP2+KP8",
|
||||||
|
"S7": "KP2+KP9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/xgb"
|
||||||
|
"github.com/BurntSushi/xgb/xproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type watcher struct {
|
||||||
|
conn *xgb.Conn
|
||||||
|
root xproto.Window
|
||||||
|
activeAtom, nameAtom xproto.Atom
|
||||||
|
prevWindowName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWindowWatcher() *watcher {
|
||||||
|
return &watcher{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) Setup() error {
|
||||||
|
X, err := xgb.NewConn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the window id of the root window.
|
||||||
|
setup := xproto.Setup(X)
|
||||||
|
|
||||||
|
w.conn = X
|
||||||
|
w.root = setup.DefaultScreen(X).Root
|
||||||
|
|
||||||
|
// Get the atom id (i.e., intern an atom) of "_NET_ACTIVE_WINDOW".
|
||||||
|
aname := "_NET_ACTIVE_WINDOW"
|
||||||
|
activeAtom, err := xproto.InternAtom(X, true, uint16(len(aname)),
|
||||||
|
aname).Reply()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Couldn't get _NET_ACTIVE_WINDOW atom: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the atom id (i.e., intern an atom) of "_NET_WM_NAME".
|
||||||
|
aname = "_NET_WM_NAME"
|
||||||
|
nameAtom, err := xproto.InternAtom(X, true, uint16(len(aname)),
|
||||||
|
aname).Reply()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Couldn't get _NET_WM_NAME atom: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.activeAtom = activeAtom.Atom
|
||||||
|
w.nameAtom = nameAtom.Atom
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) Run() {
|
||||||
|
for {
|
||||||
|
w.watch()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) watch() {
|
||||||
|
// From github.com/BurntSushi/xgb's examples.
|
||||||
|
reply, err := xproto.GetProperty(w.conn, false, w.root, w.activeAtom,
|
||||||
|
xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
windowID := xproto.Window(xgb.Get32(reply.Value))
|
||||||
|
|
||||||
|
reply, err = xproto.GetProperty(w.conn, false, windowID, w.nameAtom,
|
||||||
|
xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
windowName := string(reply.Value)
|
||||||
|
if w.prevWindowName != windowName {
|
||||||
|
w.prevWindowName = windowName
|
||||||
|
|
||||||
|
w.loadWindowConfiguration(windowName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) loadWindowConfiguration(windowName string) {
|
||||||
|
if loadedConfiguration == nil {
|
||||||
|
fmt.Println("Window name switched, but no configuration:", windowName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range loadedConfiguration.Apps {
|
||||||
|
for _, re := range conf.windowTitleRegexps {
|
||||||
|
if re.MatchString(windowName) {
|
||||||
|
currentConfiguration = conf
|
||||||
|
fmt.Printf("Applying configuration for app %q\n", conf.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentConfiguration = nil
|
||||||
|
}
|
Ładowanie…
Reference in New Issue