Repository navigation
Expand file tree
/
Copy pathmain.go
More file actions
175 lines (157 loc) · 4.9 KB
/
Copy pathmain.go
File metadata and controls
175 lines (157 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//go:build windows
// KeyLayoutPin pins the keyboard layout to each input field
// separately. Switch the command field to en and the comment field to ja,
// and the layout comes back by itself when you return to either. Fields
// where the layout was never switched get the default layout
// from Windows settings.
//
// Layout of the code:
// - internal/engine — when to switch and what to remember (no Windows);
// - internal/store — the memory file (no Windows);
// - field.go — what has focus: field key and kind (MSAA);
// - tracker.go — focus events → resolver threads → engine;
// - window.go, tray.go, autostart.go, hotkey.go — the Windows shell parts;
// - main.go — wiring.
package main
import (
"fmt"
"log"
"os"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/dronem/keylayoutpin/internal/engine"
"github.com/dronem/keylayoutpin/internal/store"
)
func main() {
o := parseOptions()
if o.version {
if guiBuild() {
messageBox("KeyLayoutPin " + versionString())
} else {
fmt.Println("KeyLayoutPin", versionString())
}
return
}
// Check this before touching any shared file (log rotation, memory):
// a second copy must leave everything as it is.
if (!o.diagnostic() || o.reset) && !singleInstance() {
if o.reset {
fatalf("KeyLayoutPin is running — exit it first (tray icon → Exit), otherwise it rewrites the memory")
}
notify("KeyLayoutPin is already running — see the icon in the notification area.")
return
}
closeLog := setupLogging(o.logFile(), o.fullLog())
defer closeLog()
switch {
case o.watch > 0:
runWatch(o.watch, o.chromium)
case o.probe > 0:
runProbe(o.probe, o.chromium)
case o.list:
printLayouts()
case o.reset:
if err := os.Remove(o.db); err != nil && !os.IsNotExist(err) {
fatalf("%v", err)
}
fmt.Println("Memory cleared:", o.db)
default:
run(o)
}
}
// run is the normal mode: tray icon, layout switching.
func run(o *options) {
fallback, err := fallbackFor(o.def)
if err != nil {
fatalf("%v", err)
}
mem, err := store.Open(o.db)
if err != nil {
fatalf("cannot read %s: %v", o.db, err)
}
// The hooks and the window are bound to this thread.
runtime.LockOSThread()
keys, err := startHotkeys()
if err != nil {
fatalf("keyboard hook: %v", err)
}
win, err := newMsgWindow()
if err != nil {
fatalf("window: %v", err)
}
defer win.close()
tr := newTray(win)
defer tr.close()
atExit(tr.close) // fatalf skips defers — don't leave a ghost icon
eng := engine.New(engine.Config{
System: winSystem{keys},
Memory: storeMemory{mem},
Fallback: fallback,
Logf: log.Printf,
Errf: errorf,
Name: layoutName,
})
t := newTracker(engineSink{eng, o.verbose}, win, newDetector(o.chromium, false).describe)
unhook, err := t.hookFocus()
if err != nil {
fatalf("SetWinEventHook: %v", err)
}
defer unhook()
if err := win.every(1, o.interval, t.tick); err != nil {
fatalf("SetTimer: %v", err) // without the timer layout changes go unnoticed
}
if err := initAutostart(); err != nil {
errorf("autostart: %v", err)
}
quitOnSignal()
printLayouts()
log.Printf("KeyLayoutPin %s running. Memory: %s (fields: %d). Ctrl+C to exit.", versionString(), o.db, mem.Len())
messageLoop()
log.Println("Exiting.")
}
// quitOnSignal ends the message loop on Ctrl+C or console close.
func quitOnSignal() {
thread := getCurrentThreadID()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
procPostThreadMessageW.Call(uintptr(thread), wmQuit, 0, 0)
}()
}
// winSystem implements engine.System on top of WinAPI.
type winSystem struct{ keys *hotkeys }
func (winSystem) Layout(tid uint32) HKL { return keyboardLayout(tid) }
func (winSystem) Request(target uintptr, h HKL) bool { return requestLayout(target, h) }
func (winSystem) Foreground() uintptr { return rootWindow(getForegroundWindow()) }
func (s winSystem) LastHotkey() time.Time { return s.keys.Last() }
func (winSystem) Now() time.Time { return time.Now() }
// storeMemory implements engine.Memory on top of the memory file.
type storeMemory struct{ s *store.Store }
func (m storeMemory) Get(key string) (HKL, bool) {
h, ok := m.s.Get(key)
if !ok {
return 0, false
}
h = installed(h) // the layout may have been removed from the system
return h, h != 0
}
func (m storeMemory) Set(key string, h HKL) error { return m.s.Set(key, h) }
func (m storeMemory) Forget(key string) error { return m.s.Forget(key) }
// engineSink feeds resolved focus to the engine.
type engineSink struct {
e *engine.Engine
verbose bool
}
func (s engineSink) Focus(f focus) {
fl := f.field
if s.verbose {
log.Printf("focus [%s] %s (%s): %s", fl.App, fl.Name, fl.Kind, fl.Key)
} else if fl.Kind != engine.Window {
fl.Name = "field" // field labels can be personal — logged only with -v
}
s.e.Focus(fl)
}
func (s engineSink) Tick() { s.e.Tick() }