forked from dakyskye/dxhd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
251 lines (217 loc) · 6.58 KB
/
main.go
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/keybind"
"github.com/BurntSushi/xgbutil/mousebind"
"github.com/BurntSushi/xgbutil/xevent"
"go.uber.org/zap"
)
var usage = `NAME
dxhd - daky's X11 Hotkey Daemon
DESCRIPTION
dxhd is easy to use X11 hotkey daemon, written in Golang programming language.
The biggest advantage of dxhd is that you can write your configs in different languages,
like sh, bash, ksh, zsh, Python, Perl
A config file is meant to have quite easy layout:
first line starting with #! is treated as a shebang
lines having ##+ prefix are ignored
lines having one # and then a keybinding are parsed as keybindings
lines under a keybinding are executed when keybinding is triggered
EXAMPLE
#!/bin/sh
## restart i3
# super + shift + r
i3-msg -t command restart
## switch to workspace 1-10
# super + @{1-9,0}
i3-msg -t command workspace {1-9,10}
## switch to workspace 11-20
# super + ctrl + {1-9,0}
i3-msg -t command workspace {11-19,20}
## switch to next/prev workspace
# super + !mouse{4,5}
i3-msg -t command workspace {next,prev}
BUGS
report bugs here, if you encounter one - https://github.com/dakyskye/dxhd/issues
AUTHOR
Lasha Kanteladze <[email protected]>`
var version = `master`
func main() {
if runtime.GOOS != "linux" {
log.Fatal("dxhd is only supported on linux")
}
var (
kill = flag.Bool("k", false, "gracefully kills every running instances of dxhd")
reload = flag.Bool("r", false, "reloads every running instances of dxhd")
customConfigPath = flag.String("c", "", "reads the config from custom path")
printVersion = flag.Bool("v", false, "prints current version of program")
dryRun = flag.Bool("d", false, "prints bindings and their actions and exits")
parseTime = flag.Bool("p", false, "prints how much time parsing a config took")
)
flag.Usage = func() {
fmt.Println(usage)
fmt.Println("VERSION")
fmt.Println(" " + version)
fmt.Println("FLAGS")
flag.PrintDefaults()
os.Exit(0)
}
flag.Parse()
if *kill || *reload {
execName, err := os.Executable()
if err != nil {
zap.L().Fatal("can not get executable", zap.Error(err))
}
cmd := new(exec.Cmd)
if *kill {
cmd = exec.Command("pkill", "-INT", "-x", filepath.Base(execName))
} else {
cmd = exec.Command("pkill", "-USR1", "-x", filepath.Base(execName))
}
err = cmd.Start()
if err != nil {
if *kill {
log.Println("can not kill dxhd instances:")
log.Fatalln(err)
} else {
log.Println("can not reload dxhd instances:")
log.Fatalln(err)
}
}
if *kill {
fmt.Println("killing every running instances of dxhd")
} else {
fmt.Println("reloading every running instances of dxhd")
}
os.Exit(0)
}
if *printVersion {
fmt.Println("you are using dxhd, version " + version)
os.Exit(0)
}
var (
configFilePath string
err error
validPath bool
)
if *customConfigPath != "" {
if validPath, err = isPathToConfigValid(*customConfigPath); !(err == nil && validPath) {
zap.L().Fatal("path to the config is not valid", zap.String("path", *customConfigPath), zap.Bool("valid", validPath), zap.Error(err))
}
configFilePath = *customConfigPath
} else {
configFilePath, _, err = getDefaultConfigPath()
if err != nil {
zap.L().Fatal("can not get default config path", zap.Error(err))
}
if validPath, err = isPathToConfigValid(configFilePath); !(err == nil && validPath) {
if os.IsNotExist(err) {
err = createDefaultConfig()
if err != nil {
zap.L().Fatal("can not create default config", zap.String("path", configFilePath), zap.Error(err))
}
} else {
zap.L().Fatal("path to the config is not valid", zap.String("path", configFilePath), zap.Bool("valid", validPath), zap.Error(err))
}
}
}
var (
data []filedata
shell string
startTime time.Time
)
if *parseTime {
startTime = time.Now()
}
shell, err = parse(configFilePath, &data)
if err != nil {
zap.L().Fatal("failed to parse config", zap.String("file", configFilePath), zap.Error(err))
}
if *parseTime {
since := time.Since(startTime)
timeTaken := fmt.Sprintf("%.0fs%dms%dµs",
since.Seconds(),
since.Milliseconds(),
since.Microseconds(),
)
fmt.Println(fmt.Sprintf("it took %s to parse the config", timeTaken))
fmt.Println(fmt.Sprintf("%d parsed keybindins (including replicated variants and ranges)", len(data)))
os.Exit(0)
}
if *dryRun {
fmt.Println("dxhd dry run")
for _, d := range data {
fmt.Println("binding: " + d.originalBinding)
fmt.Println("action:")
fmt.Println(d.action.String())
fmt.Println()
}
os.Exit(0)
}
zap.L().Debug("starting dxhd", zap.String("version", version))
// catch these signals
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2)
// errors channel
errs := make(chan error)
// infinite loop - if user sends USR signal, reload configration (so, continue loop), otherwise, exit
toplevel:
for {
if len(data) == 0 {
shell, err = parse(configFilePath, &data)
if err != nil {
zap.L().Fatal("failed to parse config", zap.String("file", configFilePath), zap.Error(err))
}
}
X, err := xgbutil.NewConn()
if err != nil {
zap.L().Fatal("can not open connection to Xorg", zap.Error(err))
}
keybind.Initialize(X)
mousebind.Initialize(X)
for _, d := range data {
err = listenKeybinding(X, errs, d.evtType, shell, d.binding.String(), d.action.String())
if err != nil {
zap.L().Info("can not register a keybinding", zap.String("keybinding", d.binding.String()), zap.Error(err))
}
}
data = nil
go xevent.Main(X)
for {
select {
case err = <-errs:
if err != nil {
zap.L().Info("command resulted into an error", zap.Error(err))
}
continue
case sig := <-signals:
keybind.Detach(X, X.RootWin())
mousebind.Detach(X, X.RootWin())
xevent.Quit(X)
if strings.HasPrefix(sig.String(), "user defined signal") {
zap.L().Debug("user defined signal received, reloading")
continue toplevel
}
zap.L().Info("signal received, shutting down", zap.String("signal", sig.String()))
if env, err := strconv.ParseBool(os.Getenv("STACKTRACE")); env && err == nil {
buf := make([]byte, 1<<20)
stackLen := runtime.Stack(buf, true)
log.Printf("\nPriting goroutine stack trace, because `STACKTRACE` was set.\n%s\n", buf[:stackLen])
}
os.Exit(0)
}
}
}
}