From f449369b3080c91391a8592cfc4998ef7aa1b514 Mon Sep 17 00:00:00 2001 From: dbaumgarten Date: Mon, 4 Jan 2021 23:21:04 +0100 Subject: [PATCH] Auto-type hotkeys only work inside Starbase --- docs/vscode-instructions.md | 3 ++- pkg/langserver/autotype_win.go | 32 ++++++++++++++++++++--- pkg/langserver/win32/hotkey.go | 41 ++++++++++++++++++++++++------ pkg/langserver/win32/window.go | 46 ++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 pkg/langserver/win32/window.go diff --git a/docs/vscode-instructions.md b/docs/vscode-instructions.md index 952dfcd..7ab4f65 100644 --- a/docs/vscode-instructions.md +++ b/docs/vscode-instructions.md @@ -41,12 +41,13 @@ One key-problem when writing yolol-code in an external editor always was how to ***NOTE2***: Make sure to you really have focussed the chip when using the hotkey, otherwise vscode-yolol will send random keystrokes to the game, making your character do all sorts of weird stuff. +***NOTE3***: The hotkeys only work when a window called "Starbase" has the focus. This way the hotkeys shoul (in theory) not interfere with other applications. + If you (for whatever reason) want to disable the global hotkeys, you can do so in the vscode-settings under File->Preferences->Settings->search for 'yolol'->Hotkeys: Enable). Vscode needs to be restarted for changes to this setting to take effect. ## Inseting code into a chip Open the .yolol-script you want to insert in vscode (it has to be the current active file). Go to Stabase's window and open the yolol-chip you want to fill. Unlock it, aim your cursor at it and click a line. Now press ```Ctrl+I```. Vscode will start to auto-type your code into the chip, starting at your current cursor-position. - ## Erasing a chip The auto-typing only works properly when the lines of the chip are empty before inserting code. Vscode-yolol can also automate this for you. Click a line on your chip and press ```Ctrl+D```. This will end the key-strokes Ctrl+A, Entf, Down 20 times, resulting in an empty chip, starting at the line you clicked. (If you clicked line 1, the chip is now completely empty) diff --git a/pkg/langserver/autotype_win.go b/pkg/langserver/autotype_win.go index 0dd1b20..890f293 100644 --- a/pkg/langserver/autotype_win.go +++ b/pkg/langserver/autotype_win.go @@ -3,9 +3,11 @@ package langserver import ( + "context" "fmt" "os" "strings" + "sync" "time" "github.com/dbaumgarten/yodk/pkg/langserver/win32" @@ -37,13 +39,37 @@ const typeDelay = 40 * time.Millisecond // ListenForHotkeys listens for global hotkeys and dispatches the registered actions func (ls *LangServer) ListenForHotkeys() { go func() { - err := win32.ListenForHotkeys(nil, ls.hotkeyHandler, AutotypeHotkey, AutodeleteHotkey, AutooverwriteHotkey) - if err != nil { - fmt.Fprintf(os.Stderr, "Error when registering hotkeys: %s", err) + currentWindow := win32.GetForegroundWindow() + wg := sync.WaitGroup{} + var cancelHotkeyListening context.CancelFunc + var hotkeysRegistered = false + for { + if isStarbaseWindow(currentWindow) && !hotkeysRegistered { + ctx := context.Background() + ctx, cancelHotkeyListening = context.WithCancel(ctx) + hotkeysRegistered = true + go func() { + wg.Add(1) + err := win32.ListenForHotkeys(ctx, ls.hotkeyHandler, AutotypeHotkey, AutodeleteHotkey, AutooverwriteHotkey) + if err != nil { + fmt.Fprintf(os.Stderr, "Error when registering hotkeys: %s", err) + } + wg.Done() + }() + } else if hotkeysRegistered { + cancelHotkeyListening() + wg.Wait() + hotkeysRegistered = false + } + currentWindow = win32.WaitForWindowChange(nil) } }() } +func isStarbaseWindow(name string) bool { + return name == "Starbase" +} + func (ls *LangServer) hotkeyHandler(hk win32.Hotkey) { win32.SendInput(win32.KeyUpInput(win32.KeycodeCtrl)) win32.SendInput(win32.KeyUpInput(uint16(hk.KeyCode))) diff --git a/pkg/langserver/win32/hotkey.go b/pkg/langserver/win32/hotkey.go index 1643cce..0e4aeaa 100644 --- a/pkg/langserver/win32/hotkey.go +++ b/pkg/langserver/win32/hotkey.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "runtime" + "time" "unsafe" ) @@ -21,8 +22,9 @@ const ( ) var ( - reghotkey = user32.MustFindProc("RegisterHotKey") - getmsg = user32.MustFindProc("GetMessageW") + reghotkey = user32.MustFindProc("RegisterHotKey") + unreghotkey = user32.MustFindProc("UnregisterHotKey") + getmsg = user32.MustFindProc("PeekMessageW") ) // Hotkey represents a key-combination pressed by a user @@ -66,6 +68,22 @@ type msg struct { POINT struct{ X, Y int64 } } +func registerHotkey(hk *Hotkey) error { + r1, _, err := reghotkey.Call(0, uintptr(hk.ID), uintptr(hk.Modifiers), uintptr(hk.KeyCode)) + if r1 == 0 { + return err + } + return nil +} + +func unregisterHotkey(hk *Hotkey) error { + r1, _, err := unreghotkey.Call(0, uintptr(hk.ID)) + if r1 == 0 { + return err + } + return nil +} + // ListenForHotkeys registers an listens for the given global Hotkeys. If a hotkey is pressed, the hendler function is executed // This function blocks, so it shoue have it's own goroutine func ListenForHotkeys(ctx context.Context, handler HotkeyHandler, hotkeys ...*Hotkey) error { @@ -74,13 +92,21 @@ func ListenForHotkeys(ctx context.Context, handler HotkeyHandler, hotkeys ...*Ho defer runtime.UnlockOSThread() hotkeymap := make(map[int16]*Hotkey) + + // unregister all hotkeys when exiting + defer func() { + for _, v := range hotkeymap { + unregisterHotkey(v) + } + }() + + // register all requested hotkeys for _, v := range hotkeys { - hotkeymap[int16(v.ID)] = v - r1, _, err := reghotkey.Call( - 0, uintptr(v.ID), uintptr(v.Modifiers), uintptr(v.KeyCode)) - if r1 != 1 { + err := registerHotkey(v) + if err != nil { return err } + hotkeymap[int16(v.ID)] = v } for { @@ -88,7 +114,7 @@ func ListenForHotkeys(ctx context.Context, handler HotkeyHandler, hotkeys ...*Ho return nil } var msg = &msg{} - getmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0) + getmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0, 1) // Registered id is in the WPARAM field: if id := msg.WPARAM; id != 0 { @@ -97,5 +123,6 @@ func ListenForHotkeys(ctx context.Context, handler HotkeyHandler, hotkeys ...*Ho handler(*hk) } } + time.Sleep(50 * time.Millisecond) } } diff --git a/pkg/langserver/win32/window.go b/pkg/langserver/win32/window.go new file mode 100644 index 0000000..d670b22 --- /dev/null +++ b/pkg/langserver/win32/window.go @@ -0,0 +1,46 @@ +// +build windows + +package win32 + +import ( + "context" + "syscall" + "time" + "unsafe" +) + +var ( + forgrWindow = user32.MustFindProc("GetForegroundWindow") + windowText = user32.MustFindProc("GetWindowTextW") + windowTextLen = user32.MustFindProc("GetWindowTextLengthW") +) + +// GetForegroundWindow returns the name of the window that currently has the user-focus +func GetForegroundWindow() string { + hwnd, _, _ := forgrWindow.Call() + + if hwnd != 0 { + textlen, _, _ := windowTextLen.Call(hwnd) + buf := make([]uint16, textlen+1) + windowText.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(textlen+1)) + return syscall.UTF16ToString(buf) + } + + return "" +} + +// WaitForWindowChange blocks until the window with the user-focus changes (or ctx is cancelled). Returns the title of the new active window +func WaitForWindowChange(ctx context.Context) string { + oldWindow := GetForegroundWindow() + for { + time.Sleep(500 * time.Millisecond) + newWindow := GetForegroundWindow() + if newWindow != oldWindow { + return newWindow + } + if ctx != nil && ctx.Err() != nil { + return oldWindow + } + + } +}