Implement output filtering

pull/81/head
lawl 3 years ago
parent d8be27d86e
commit 37692a873a

@ -62,7 +62,7 @@ If you have been maintaining a third party package for a while and would like it
## Usage
Select the microphone you want to denoise, and click "Load NoiseTorch", NoiseTorch will create a virtual microphone called "NoiseTorch Microphone" that you can select in any application.
Select the microphone you want to denoise, and click "Load NoiseTorch", NoiseTorch will create a virtual microphone called "NoiseTorch Microphone" that you can select in any application. Output filtering works the same way, simply output the applications you want to filter to "NoiseTorch Headphones".
When you're done using it, simply click "Unload NoiseTorch" to remove it again, until you need it next time.
@ -80,7 +80,9 @@ Please see the [Troubleshooting](https://github.com/lawl/NoiseTorch/wiki/Trouble
## Latency
NoiseTorch may introduce a small amount of latency. The amount of inherent latency introduced by noise supression is 10ms, this is very low and should not be a problem. Additionally PulseAudio currently introduces a variable amount of latency that depends on your system. Lowering this latency [requires a change in PulseAudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/120).
NoiseTorch may introduce a small amount of latency for microphone filtering. The amount of inherent latency introduced by noise supression is 10ms, this is very low and should not be a problem. Additionally PulseAudio currently introduces a variable amount of latency that depends on your system. Lowering this latency [requires a change in PulseAudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/120).
Output filtering currently introduces something on the order of ~100ms with pulseaudio. This should still be fine for regular conferences, VOIPing and gaming. Maybe not for competitive gaming teams.
## Building from source

@ -14,16 +14,30 @@ type config struct {
Threshold int
DisplayMonitorSources bool
EnableUpdates bool
GuiltTripped bool
FilterInput bool
FilterOutput bool
LastUsedInput string
LastUsedOutput string
}
const configFile = "config.toml"
func initializeConfigIfNot() {
log.Println("Checking if config needs to be initialized")
conf := config{Threshold: 95, DisplayMonitorSources: false, EnableUpdates: true, LastUsedInput: ""} // if you're a package maintainer and you mess with this, we have a problem.
// Unless you set -tags release on the build the updater is *not* compiled in any. DO NOT MESS WITH THIS!
// if you're a package maintainer and you mess with this, we have a problem.
// Unless you set -tags release on the build the updater is *not* compiled in anymore. DO NOT MESS WITH THIS!
// This isn't and never was the proper location to disable the updater.
conf := config{
Threshold: 95,
DisplayMonitorSources: false,
EnableUpdates: true,
GuiltTripped: false,
FilterInput: true,
FilterOutput: false,
LastUsedInput: "",
LastUsedOutput: ""}
configdir := configDir()
ok, err := exists(configdir)

@ -5,6 +5,7 @@ go 1.14
require (
gioui.org v0.0.0-20200630184602-223f8fd40ae4 // indirect
github.com/BurntSushi/toml v0.3.1
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba
github.com/lawl/pulseaudio v0.0.0-20200802093727-ab0735955fd0
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635

@ -7,6 +7,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA=
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba h1:7OBB0+T/f0gGMdqTwoXF872nDKosq4dIK/H2cRlrnWI=
github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba/go.mod h1:TsFEH0qn2Uu3C3guJjfIaoCqgpoCvU+laq0SSK2TOyY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=

@ -8,9 +8,13 @@ import (
"log"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/ewmh"
"github.com/BurntSushi/xgbutil/icccm"
"github.com/aarzilli/nucular/font"
"github.com/lawl/pulseaudio"
@ -24,31 +28,36 @@ import (
//go:generate go run scripts/embedversion.go
//go:generate go run scripts/embedlicenses.go
type input struct {
type device struct {
ID string
Name string
isMonitor bool
checked bool
dynamicLatency bool
rate uint32
}
const appName = "NoiseTorch"
func main() {
var pulsepid int
var setcap bool
var sourceName string
var sinkName string
var unload bool
var load bool
var loadInput bool
var loadOutput bool
var threshold int
var list bool
flag.IntVar(&pulsepid, "removerlimit", -1, "for internal use only")
flag.BoolVar(&setcap, "setcap", false, "for internal use only")
flag.StringVar(&sourceName, "s", "", "Use the specified source device ID")
flag.BoolVar(&load, "i", false, "Load supressor for input. If no source device ID is specified the default pulse audio source is used.")
flag.StringVar(&sinkName, "s", "", "Use the specified source/sink device ID")
flag.BoolVar(&loadInput, "i", false, "Load supressor for input. If no source device ID is specified the default pulse audio source is used.")
flag.BoolVar(&loadOutput, "o", false, "Load supressor for output. If no source device ID is specified the default pulse audio source is used.")
flag.BoolVar(&unload, "u", false, "Unload supressor")
flag.IntVar(&threshold, "t", -1, "Voice activation threshold")
flag.BoolVar(&list, "l", false, "List available PulseAudio sources")
flag.BoolVar(&list, "l", false, "List available PulseAudio devices")
flag.Parse()
// we also execute this opportunistically on pulsepid since that's also called as root, but need to do so silently, so no os.Exit()'s
@ -101,9 +110,16 @@ func main() {
}
if list {
fmt.Println("Sources:")
sources := getSources(paClient)
for i := range sources {
fmt.Printf("Device Name: %s\nDevice ID: %s\n\n", sources[i].Name, sources[i].ID)
fmt.Printf("\tDevice Name: %s\n\tDevice ID: %s\n\n", sources[i].Name, sources[i].ID)
}
fmt.Println("Sinks:")
sinks := getSinks(paClient)
for i := range sinks {
fmt.Printf("\tDevice Name: %s\n\tDevice ID: %s\n\n", sinks[i].Name, sinks[i].ID)
}
os.Exit(0)
@ -119,30 +135,53 @@ func main() {
}
if unload {
unloadSupressor(paClient)
unloadSupressor(&ctx)
os.Exit(0)
}
if load {
if loadInput {
ctx.paClient = paClient
sources := getSources(paClient)
if supressorState(paClient) != unloaded {
fmt.Fprintf(os.Stderr, "Supressor is already loaded.\n")
os.Exit(1)
}
if sourceName == "" {
if sinkName == "" {
defaultSource, err := getDefaultSourceID(paClient)
if err != nil {
fmt.Fprintf(os.Stderr, "No source specified to load and failed to load default source: %+v\n", err)
os.Exit(1)
}
sourceName = defaultSource
sinkName = defaultSource
}
for i := range sources {
if sources[i].ID == sourceName {
err := loadSupressor(&ctx, sources[i])
if sources[i].ID == sinkName {
sources[i].checked = true
err := loadSupressor(&ctx, &sources[i], &device{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading PulseAudio Module: %+v\n", err)
os.Exit(1)
}
os.Exit(0)
}
}
fmt.Fprintf(os.Stderr, "PulseAudio source not found: %s\n", sinkName)
os.Exit(1)
}
if loadOutput {
ctx.paClient = paClient
sinks := getSinks(paClient)
if sinkName == "" {
defaultSink, err := getDefaultSinkID(paClient)
if err != nil {
fmt.Fprintf(os.Stderr, "No sink specified to load and failed to load default sink: %+v\n", err)
os.Exit(1)
}
sinkName = defaultSink
}
for i := range sinks {
if sinks[i].ID == sinkName {
sinks[i].checked = true
err := loadSupressor(&ctx, &device{}, &sinks[i])
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading PulseAudio Module: %+v\n", err)
os.Exit(1)
@ -150,7 +189,7 @@ func main() {
os.Exit(0)
}
}
fmt.Fprintf(os.Stderr, "PulseAudio source not found: %s\n", sourceName)
fmt.Fprintf(os.Stderr, "PulseAudio sink not found: %s\n", sinkName)
os.Exit(1)
}
@ -161,13 +200,19 @@ func main() {
go paConnectionWatchdog(&ctx)
wnd := nucular.NewMasterWindowSize(0, "NoiseTorch", image.Point{550, 300}, func(w *nucular.Window) {
wnd := nucular.NewMasterWindowSize(0, appName, image.Point{600, 400}, func(w *nucular.Window) {
updatefn(&ctx, w)
})
ctx.masterWindow = &wnd
style := style.FromTheme(style.DarkTheme, 2.0)
style.Font = font.DefaultFont(16, 1)
wnd.SetStyle(style)
//this is a disgusting hack that searches for the noisetorch window
//and then fixes up the WM_CLASS attribute so it displays
//properly in the taskbar
go fixWindowClass()
wnd.Main()
}
@ -190,29 +235,59 @@ func removeLib(file string) {
log.Printf("Deleted temp librnnoise: %s\n", file)
}
func getSources(client *pulseaudio.Client) []input {
func getSources(client *pulseaudio.Client) []device {
sources, err := client.Sources()
if err != nil {
log.Printf("Couldn't fetch sources from pulseaudio\n")
}
inputs := make([]input, 0)
outputs := make([]device, 0)
for i := range sources {
if sources[i].Name == "nui_mic_remap" {
if strings.Contains(sources[i].Name, "nui_") {
continue
}
log.Printf("Input %s, %+v\n", sources[i].Name, sources[i])
var inp input
var inp device
inp.ID = sources[i].Name
inp.Name = sources[i].PropList["device.description"]
inp.isMonitor = (sources[i].MonitorSourceIndex != 0xffffffff)
inp.rate = sources[i].SampleSpec.Rate
//PA_SOURCE_DYNAMIC_LATENCY = 0x0040U
inp.dynamicLatency = sources[i].Flags&uint32(0x0040) != 0
outputs = append(outputs, inp)
}
return outputs
}
func getSinks(client *pulseaudio.Client) []device {
sources, err := client.Sinks()
if err != nil {
log.Printf("Couldn't fetch sources from pulseaudio\n")
}
inputs := make([]device, 0)
for i := range sources {
if strings.Contains(sources[i].Name, "nui_") {
continue
}
log.Printf("Output %s, %+v\n", sources[i].Name, sources[i])
var inp device
inp.ID = sources[i].Name
inp.Name = sources[i].PropList["device.description"]
inp.rate = sources[i].SampleSpec.Rate
// PA_SINK_DYNAMIC_LATENCY = 0x0080U
inp.dynamicLatency = sources[i].Flags&uint32(0x0080) != 0
inputs = append(inputs, inp)
}
@ -233,9 +308,10 @@ func paConnectionWatchdog(ctx *ntcontext) {
}
ctx.paClient = paClient
go updateNoiseSupressorLoaded(paClient, &ctx.noiseSupressorState)
go updateNoiseSupressorLoaded(ctx)
ctx.inputList = getSourcesWithPreSelectedInput(ctx)
ctx.inputList = preselectDevice(ctx, getSources(ctx.paClient), ctx.config.LastUsedInput, getDefaultSourceID)
ctx.outputList = preselectDevice(ctx, getSinks(paClient), ctx.config.LastUsedOutput, getDefaultSinkID)
resetUI(ctx)
@ -243,32 +319,28 @@ func paConnectionWatchdog(ctx *ntcontext) {
}
}
func getSourcesWithPreSelectedInput(ctx *ntcontext) []input {
inputs := getSources(ctx.paClient)
preselectedInputID := &ctx.config.LastUsedInput
inputExists := false
if preselectedInputID != nil {
for _, input := range inputs {
inputExists = inputExists || input.ID == *preselectedInputID
}
func preselectDevice(ctx *ntcontext, devices []device, preselectID string,
fallbackFunc func(client *pulseaudio.Client) (string, error)) []device {
deviceExists := false
for _, input := range devices {
deviceExists = deviceExists || input.ID == preselectID
}
if !inputExists {
defaultSource, err := getDefaultSourceID(ctx.paClient)
if !deviceExists {
defaultDevice, err := fallbackFunc(ctx.paClient)
if err != nil {
log.Printf("Failed to load default source: %+v\n", err)
log.Printf("Failed to load default device: %+v\n", err)
} else {
preselectedInputID = &defaultSource
preselectID = defaultDevice
}
}
if preselectedInputID != nil {
for i := range inputs {
if inputs[i].ID == *preselectedInputID {
inputs[i].checked = true
}
for i := range devices {
if devices[i].ID == preselectID {
devices[i].checked = true
}
}
return inputs
return devices
}
func getDefaultSourceID(client *pulseaudio.Client) (string, error) {
@ -278,3 +350,43 @@ func getDefaultSourceID(client *pulseaudio.Client) (string, error) {
}
return server.DefaultSource, nil
}
func getDefaultSinkID(client *pulseaudio.Client) (string, error) {
server, err := client.ServerInfo()
if err != nil {
return "", err
}
return server.DefaultSink, nil
}
//this is disgusting
func fixWindowClass() {
xu, err := xgbutil.NewConn()
defer xu.Conn().Close()
if err != nil {
log.Printf("Couldn't create XU xdg conn: %+v\n", err)
return
}
for i := 0; i < 100; i++ {
wnds, _ := ewmh.ClientListGet(xu)
for _, w := range wnds {
n, _ := ewmh.WmNameGet(xu, w)
if n == appName {
_, err := icccm.WmClassGet(xu, w)
//if we have *NO* WM_CLASS, then the above call errors. We *want* to make sure this errors
if err == nil {
continue
}
class := icccm.WmClass{}
class.Class = appName
class.Instance = appName
icccm.WmClassSet(xu, w, &class)
return
}
}
time.Sleep(100 * time.Millisecond)
}
}

@ -4,7 +4,6 @@ import (
"fmt"
"log"
"strings"
"time"
"github.com/lawl/pulseaudio"
)
@ -17,15 +16,15 @@ const (
// the ugly and (partially) repeated strings are unforunately difficult to avoid, as it's what pulse audio expects
func updateNoiseSupressorLoaded(c *pulseaudio.Client, b *int) {
func updateNoiseSupressorLoaded(ctx *ntcontext) {
c := ctx.paClient
upd, err := c.Updates()
if err != nil {
fmt.Printf("Error listening for updates: %v\n", err)
}
for {
*b = supressorState(c)
ctx.noiseSupressorState = supressorState(ctx)
if !c.Connected() {
break
@ -35,39 +34,82 @@ func updateNoiseSupressorLoaded(c *pulseaudio.Client, b *int) {
}
}
func supressorState(c *pulseaudio.Client) int {
func supressorState(ctx *ntcontext) int {
//perform some checks to see if it looks like the noise supressor is loaded
_, nullsink, err := findModule(c, "module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-null-sink: %v\n", err)
}
_, ladspasink, err := findModule(c, "module-ladspa-sink", "sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-ladspa-sink: %v\n", err)
}
_, loopback, err := findModule(c, "module-loopback", "sink=nui_mic_raw_in")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-loopback: %v\n", err)
c := ctx.paClient
var inpLoaded, outLoaded, inputInc, outputInc bool
if ctx.config.FilterInput {
_, nullsink, err := findModule(c, "module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-null-sink: %v\n", err)
}
_, ladspasink, err := findModule(c, "module-ladspa-sink", "sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-ladspa-sink: %v\n", err)
}
_, loopback, err := findModule(c, "module-loopback", "sink=nui_mic_raw_in")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-loopback: %v\n", err)
}
_, remap, err := findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-remap-source: %v\n", err)
}
if nullsink && ladspasink && loopback && remap {
inpLoaded = true
} else if nullsink || ladspasink || loopback || remap {
inputInc = true
}
} else {
inpLoaded = true
}
_, remap, err := findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
if err != nil {
log.Printf("Couldn't fetch module list to check for module-remap-source: %v\n", err)
if ctx.config.FilterOutput {
_, out, err := findModule(c, "module-null-sink", "sink_name=nui_out_out_sink")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
_, lad, err := findModule(c, "module-ladspa-sink", "sink_name=nui_out_ladspa")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
_, loop, err := findModule(c, "module-loopback", "source=nui_out_out_sink.monitor")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
_, outin, err := findModule(c, "module-null-sink", "sink_name=nui_out_in_sink")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
_, loop2, err := findModule(c, "module-loopback", "source=nui_out_in_sink.monitor")
if err != nil {
log.Printf("Couldn't fetch module list to check for output module-ladspa-sink: %v\n", err)
}
outLoaded = out && lad && loop && outin && loop2
outputInc = out || lad || loop || outin || loop2
} else {
outLoaded = true
}
if nullsink && ladspasink && loopback && remap {
if (inpLoaded || !ctx.config.FilterInput) && (outLoaded || !ctx.config.FilterOutput) && !inputInc {
return loaded
}
if nullsink || ladspasink || loopback || remap {
if (inpLoaded && ctx.config.FilterInput) || (outLoaded && ctx.config.FilterOutput) || inputInc || outputInc {
return inconsistent
}
return unloaded
}
func loadSupressor(ctx *ntcontext, inp input) error {
c := ctx.paClient
func loadSupressor(ctx *ntcontext, inp *device, out *device) error {
log.Printf("Querying pulse rlimit\n")
c := ctx.paClient
pid, err := getPulsePid()
if err != nil {
return err
@ -93,45 +135,80 @@ func loadSupressor(ctx *ntcontext, inp input) error {
}
log.Printf("Rlimit: %+v\n", newLim)
log.Printf("Loading supressor\n")
idx, err := c.LoadModule("module-null-sink", "sink_name=nui_mic_denoised_out rate=48000")
if err != nil {
return err
}
log.Printf("Loaded null sink as idx: %d\n", idx)
if inp.checked {
log.Printf("Loading supressor\n")
idx, err := c.LoadModule("module-null-sink", "sink_name=nui_mic_denoised_out rate=48000")
if err != nil {
return err
}
log.Printf("Loaded null sink as idx: %d\n", idx)
time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
idx, err = c.LoadModule("module-ladspa-sink",
fmt.Sprintf("sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out "+
"label=noise_suppressor_mono plugin=%s control=%d", ctx.librnnoise, ctx.config.Threshold))
if err != nil {
return err
}
log.Printf("Loaded ladspa sink as idx: %d\n", idx)
idx, err = c.LoadModule("module-ladspa-sink",
fmt.Sprintf("sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out "+
"label=noise_suppressor_mono plugin=%s control=%d", ctx.librnnoise, ctx.config.Threshold))
if err != nil {
return err
}
log.Printf("Loaded ladspa sink as idx: %d\n", idx)
time.Sleep(time.Millisecond * 1000) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
//time.Sleep(time.Millisecond * 1000) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
idx, err = c.LoadModule("module-loopback",
fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=1 source_dont_move=true sink_dont_move=true", inp.ID))
if err != nil {
return err
idx, err = c.LoadModule("module-loopback",
fmt.Sprintf("source=%s sink=nui_mic_raw_in channels=1 latency_msec=1 source_dont_move=true sink_dont_move=true", inp.ID))
if err != nil {
return err
}
log.Printf("Loaded loopback as idx: %d\n", idx)
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
idx, err = c.LoadModule("module-remap-source", `master=nui_mic_denoised_out.monitor `+
`source_name=nui_mic_remap source_properties="device.description='NoiseTorch Microphone'"`)
if err != nil {
return err
}
log.Printf("Loaded remap source as idx: %d\n", idx)
}
log.Printf("Loaded loopback as idx: %d\n", idx)
time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
if out.checked {
_, err := c.LoadModule("module-null-sink", `sink_name=nui_out_out_sink`)
if err != nil {
return err
}
_, err = c.LoadModule("module-null-sink", `sink_name=nui_out_in_sink sink_properties="device.description='NoiseTorch Headphones'"`)
if err != nil {
return err
}
_, err = c.LoadModule("module-ladspa-sink", fmt.Sprintf(`sink_name=nui_out_ladspa sink_master=nui_out_out_sink `+
`label=noise_suppressor_mono channels=1 plugin=%s control=%d rate=%d`,
/*out.ID,*/ ctx.librnnoise, ctx.config.Threshold /*out.rate*/, 48000))
if err != nil {
return err
}
_, err = c.LoadModule("module-loopback",
fmt.Sprintf("source=nui_out_out_sink.monitor sink=%s channels=2 latency_msec=50 source_dont_move=true sink_dont_move=true", out.ID))
if err != nil {
return err
}
_, err = c.LoadModule("module-loopback",
fmt.Sprintf("source=nui_out_in_sink.monitor sink=nui_out_ladspa channels=1 latency_msec=50 source_dont_move=true sink_dont_move=true"))
if err != nil {
return err
}
idx, err = c.LoadModule("module-remap-source", `master=nui_mic_denoised_out.monitor `+
`source_name=nui_mic_remap source_properties="device.description='NoiseTorch Microphone'"`)
if err != nil {
return err
}
log.Printf("Loaded ladspa sink as idx: %d\n", idx)
return nil
}
func unloadSupressor(c *pulseaudio.Client) error {
func unloadSupressor(ctx *ntcontext) error {
log.Printf("Unloading pulseaudio modules\n")
// we ignore errors here on purpose, since NT didn't use to do this for unloading anyways
@ -146,6 +223,7 @@ func unloadSupressor(c *pulseaudio.Client) error {
}
log.Printf("Searching for null-sink\n")
c := ctx.paClient
m, found, err := findModule(c, "module-null-sink", "sink_name=nui_mic_denoised_out")
if err != nil {
return err
@ -154,7 +232,7 @@ func unloadSupressor(c *pulseaudio.Client) error {
log.Printf("Found null-sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for ladspa-sink\n")
m, found, err = findModule(c, "module-ladspa-sink", "sink_name=nui_mic_raw_in sink_master=nui_mic_denoised_out")
if err != nil {
@ -164,7 +242,7 @@ func unloadSupressor(c *pulseaudio.Client) error {
log.Printf("Found ladspa-sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for loopback\n")
m, found, err = findModule(c, "module-loopback", "sink=nui_mic_raw_in")
if err != nil {
@ -174,7 +252,7 @@ func unloadSupressor(c *pulseaudio.Client) error {
log.Printf("Found loopback at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for remap-source\n")
m, found, err = findModule(c, "module-remap-source", "master=nui_mic_denoised_out.monitor source_name=nui_mic_remap")
if err != nil {
@ -184,6 +262,62 @@ func unloadSupressor(c *pulseaudio.Client) error {
log.Printf("Found remap source at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for output module-null-sink\n")
m, found, err = findModule(c, "module-null-sink", "sink_name=nui_out_out_sink")
if err != nil {
return err
}
if found {
log.Printf("Found output null sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for output module-null-sink\n")
m, found, err = findModule(c, "module-null-sink", "sink_name=nui_out_in_sink")
if err != nil {
return err
}
if found {
log.Printf("Found output null sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for output module-ladspa-sink\n")
m, found, err = findModule(c, "module-ladspa-sink", "sink_name=nui_out_ladspa")
if err != nil {
return err
}
if found {
log.Printf("Found output ladspa sink at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for output module-loopback\n")
m, found, err = findModule(c, "module-loopback", "source=nui_out_out_sink.monitor")
if err != nil {
return err
}
if found {
log.Printf("Found output loopback at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
//time.Sleep(time.Millisecond * 500) // pulseaudio gets SIGKILL'd because of RLIMITS if we send these too fast
log.Printf("Searching for output module-loopback\n")
m, found, err = findModule(c, "module-loopback", "source=nui_out_in_sink.monitor")
if err != nil {
return err
}
if found {
log.Printf("Found output loopback at id [%d], sending unload command\n", m.Index)
c.UnloadModule(m.Index)
}
return nil
}

198
ui.go

@ -17,12 +17,12 @@ import (
)
type ntcontext struct {
inputList []input
inputList []device
outputList []device
noiseSupressorState int
paClient *pulseaudio.Client
librnnoise string
sourceListColdWidthIndex int
useBuiltinRNNoise bool
config *config
loadingScreen bool
licenseScreen bool
@ -137,9 +137,22 @@ func updatefn(ctx *ntcontext, w *nucular.Window) {
w.LabelColored("Reloading NoiseTorch is required to apply these changes.", "LC", orange)
}
w.Row(15).Dynamic(2)
if w.CheckboxText("Filter Microphone", &ctx.config.FilterInput) {
ctx.sourceListColdWidthIndex++ //recompute the with because of new elements
go writeConfig(ctx.config)
go (func() { ctx.noiseSupressorState = supressorState(ctx) })()
}
if w.CheckboxText("Filter Headphones", &ctx.config.FilterOutput) {
ctx.sourceListColdWidthIndex++ //recompute the with because of new elements
go writeConfig(ctx.config)
go (func() { ctx.noiseSupressorState = supressorState(ctx) })()
}
w.TreePop()
}
if w.TreePush(nucular.TreeTab, "Select Device", true) {
if ctx.config.FilterInput && w.TreePush(nucular.TreeTab, "Select Microphone", true) {
w.Row(15).Dynamic(1)
w.Label("Select an input device below:", "LC")
@ -163,69 +176,125 @@ func updatefn(ctx *ntcontext, w *nucular.Window) {
}
}
w.Row(30).Dynamic(1)
w.Spacing(1)
w.TreePop()
}
if ctx.config.FilterOutput && w.TreePush(nucular.TreeTab, "Select Headphones", true) {
if ctx.config.GuiltTripped {
w.Row(15).Dynamic(1)
w.Label("Select an output device below:", "LC")
for i := range ctx.outputList {
el := &ctx.outputList[i]
if el.isMonitor && !ctx.config.DisplayMonitorSources {
continue
}
w.Row(15).Static()
w.LayoutFitWidth(0, 0)
if w.CheckboxText("", &el.checked) {
ensureOnlyOneInputSelected(&ctx.outputList, el)
}
w.LayoutFitWidth(ctx.sourceListColdWidthIndex, 0)
if el.dynamicLatency {
w.Label(el.Name, "LC")
} else {
w.LabelColored("(incompatible?) "+el.Name, "LC", orange)
}
w.Row(25).Dynamic(2)
if ctx.noiseSupressorState != unloaded {
if w.ButtonText("Unload NoiseTorch") {
ctx.loadingScreen = true
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if err := unloadSupressor(ctx.paClient); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx.paClient) != unloaded {
time.Sleep(time.Millisecond * 500)
}
}
ctx.loadingScreen = false
(*ctx.masterWindow).Changed()
}()
}
} else {
w.Spacing(1)
}
txt := "Load NoiseTorch"
if ctx.noiseSupressorState == loaded {
txt = "Reload NoiseTorch"
w.Row(15).Dynamic(1)
w.Label("This feature is only for patrons.", "LC")
w.Row(15).Dynamic(1)
w.Label("You can still use it eitherway, but you are legally required to feel bad.", "LC")
w.Row(25).Dynamic(2)
if w.ButtonText("Become a patron") {
exec.Command("xdg-open", "https://patreon.com/lawl").Run()
ctx.config.GuiltTripped = true
go writeConfig(ctx.config)
}
if w.ButtonText("Feel bad") {
ctx.config.GuiltTripped = true
go writeConfig(ctx.config)
}
}
if inp, ok := inputSelection(ctx); ok && ctx.noiseSupressorState != inconsistent {
if w.ButtonText(txt) {
ctx.loadingScreen = true
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if ctx.noiseSupressorState == loaded {
if err := unloadSupressor(ctx.paClient); err != nil {
log.Println(err)
}
w.TreePop()
}
w.Row(15).Dynamic(1)
w.Spacing(1)
w.Row(25).Dynamic(2)
if ctx.noiseSupressorState != unloaded {
if w.ButtonText("Unload NoiseTorch") {
ctx.loadingScreen = true
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx) != unloaded {
time.Sleep(time.Millisecond * 500)
}
if err := loadSupressor(ctx, inp); err != nil {
}
ctx.loadingScreen = false
(*ctx.masterWindow).Changed()
}()
}
} else {
w.Spacing(1)
}
txt := "Load NoiseTorch"
if ctx.noiseSupressorState == loaded {
txt = "Reload NoiseTorch"
}
inp, inpOk := inputSelection(ctx)
out, outOk := outputSelection(ctx)
if (!ctx.config.FilterInput || (ctx.config.FilterInput && inpOk)) &&
(!ctx.config.FilterOutput || (ctx.config.FilterOutput && outOk)) &&
(ctx.config.FilterInput || ctx.config.FilterOutput) &&
((ctx.config.FilterOutput && ctx.config.GuiltTripped) || !ctx.config.FilterOutput) &&
ctx.noiseSupressorState != inconsistent {
if w.ButtonText(txt) {
ctx.loadingScreen = true
ctx.reloadRequired = false
go func() { // don't block the UI thread, just display a working screen so user can't run multiple loads/unloads
if ctx.noiseSupressorState == loaded {
if err := unloadSupressor(ctx); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx.paClient) != loaded {
time.Sleep(time.Millisecond * 500)
}
}
if err := loadSupressor(ctx, &inp, &out); err != nil {
log.Println(err)
}
//wait until PA reports it has actually loaded it, timeout at 10s
for i := 0; i < 20; i++ {
if supressorState(ctx) != loaded {
time.Sleep(time.Millisecond * 500)
}
ctx.config.LastUsedInput = inp.ID
go writeConfig(ctx.config)
ctx.loadingScreen = false
(*ctx.masterWindow).Changed()
}()
}
} else {
w.Spacing(1)
}
ctx.config.LastUsedInput = inp.ID
ctx.config.LastUsedOutput = out.ID
go writeConfig(ctx.config)
ctx.loadingScreen = false
(*ctx.masterWindow).Changed()
}()
}
w.TreePop()
} else {
w.Spacing(1)
}
}
func ensureOnlyOneInputSelected(inps *[]input, current *input) {
func ensureOnlyOneInputSelected(inps *[]device, current *device) {
if current.checked != true {
return
}
@ -236,13 +305,30 @@ func ensureOnlyOneInputSelected(inps *[]input, current *input) {
current.checked = true
}
func inputSelection(ctx *ntcontext) (input, bool) {
func inputSelection(ctx *ntcontext) (device, bool) {
if !ctx.config.FilterInput {
return device{}, false
}
for _, in := range ctx.inputList {
if in.checked {
return in, true
}
}
return input{}, false
return device{}, false
}
func outputSelection(ctx *ntcontext) (device, bool) {
if !ctx.config.FilterOutput {
return device{}, false
}
for _, out := range ctx.outputList {
if out.checked {
return out, true
}
}
return device{}, false
}
func loadingScreen(ctx *ntcontext, w *nucular.Window) {

@ -1,9 +1,5 @@
// +build !release
package main
import "errors"
type updateui struct {
serverVersion string
available bool
@ -12,17 +8,9 @@ type updateui struct {
}
func updateCheck(ctx *ntcontext) {
// noop for non-release versions
}
func update(ctx *ntcontext) {
}
func fetchFile(file string) ([]byte, error) {
return make([]byte, 0), errors.New("Disabled by build flags")
}
func publickey() []byte {
return make([]byte, 0)
// noop for non-release versions
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,718 @@
// Package xinerama is the X client API for the XINERAMA extension.
package xinerama
// This file is automatically generated from xinerama.xml. Edit at your peril!
import (
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
// Init must be called before using the XINERAMA extension.
func Init(c *xgb.Conn) error {
reply, err := xproto.QueryExtension(c, 8, "XINERAMA").Reply()
switch {
case err != nil:
return err
case !reply.Present:
return xgb.Errorf("No extension named XINERAMA could be found on on the server.")
}
c.ExtLock.Lock()
c.Extensions["XINERAMA"] = reply.MajorOpcode
c.ExtLock.Unlock()
for evNum, fun := range xgb.NewExtEventFuncs["XINERAMA"] {
xgb.NewEventFuncs[int(reply.FirstEvent)+evNum] = fun
}
for errNum, fun := range xgb.NewExtErrorFuncs["XINERAMA"] {
xgb.NewErrorFuncs[int(reply.FirstError)+errNum] = fun
}
return nil
}
func init() {
xgb.NewExtEventFuncs["XINERAMA"] = make(map[int]xgb.NewEventFun)
xgb.NewExtErrorFuncs["XINERAMA"] = make(map[int]xgb.NewErrorFun)
}
type ScreenInfo struct {
XOrg int16
YOrg int16
Width uint16
Height uint16
}
// ScreenInfoRead reads a byte slice into a ScreenInfo value.
func ScreenInfoRead(buf []byte, v *ScreenInfo) int {
b := 0
v.XOrg = int16(xgb.Get16(buf[b:]))
b += 2
v.YOrg = int16(xgb.Get16(buf[b:]))
b += 2
v.Width = xgb.Get16(buf[b:])
b += 2
v.Height = xgb.Get16(buf[b:])
b += 2
return b
}
// ScreenInfoReadList reads a byte slice into a list of ScreenInfo values.
func ScreenInfoReadList(buf []byte, dest []ScreenInfo) int {
b := 0
for i := 0; i < len(dest); i++ {
dest[i] = ScreenInfo{}
b += ScreenInfoRead(buf[b:], &dest[i])
}
return xgb.Pad(b)
}
// Bytes writes a ScreenInfo value to a byte slice.
func (v ScreenInfo) Bytes() []byte {
buf := make([]byte, 8)
b := 0
xgb.Put16(buf[b:], uint16(v.XOrg))
b += 2
xgb.Put16(buf[b:], uint16(v.YOrg))
b += 2
xgb.Put16(buf[b:], v.Width)
b += 2
xgb.Put16(buf[b:], v.Height)
b += 2
return buf[:b]
}
// ScreenInfoListBytes writes a list of ScreenInfo values to a byte slice.
func ScreenInfoListBytes(buf []byte, list []ScreenInfo) int {
b := 0
var structBytes []byte
for _, item := range list {
structBytes = item.Bytes()
copy(buf[b:], structBytes)
b += len(structBytes)
}
return xgb.Pad(b)
}
// Skipping definition for base type 'Bool'
// Skipping definition for base type 'Byte'
// Skipping definition for base type 'Card8'
// Skipping definition for base type 'Char'
// Skipping definition for base type 'Void'
// Skipping definition for base type 'Double'
// Skipping definition for base type 'Float'
// Skipping definition for base type 'Int16'
// Skipping definition for base type 'Int32'
// Skipping definition for base type 'Int8'
// Skipping definition for base type 'Card16'
// Skipping definition for base type 'Card32'
// GetScreenCountCookie is a cookie used only for GetScreenCount requests.
type GetScreenCountCookie struct {
*xgb.Cookie
}
// GetScreenCount sends a checked request.
// If an error occurs, it will be returned with the reply by calling GetScreenCountCookie.Reply()
func GetScreenCount(c *xgb.Conn, Window xproto.Window) GetScreenCountCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetScreenCount' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(getScreenCountRequest(c, Window), cookie)
return GetScreenCountCookie{cookie}
}
// GetScreenCountUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func GetScreenCountUnchecked(c *xgb.Conn, Window xproto.Window) GetScreenCountCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetScreenCount' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(getScreenCountRequest(c, Window), cookie)
return GetScreenCountCookie{cookie}
}
// GetScreenCountReply represents the data returned from a GetScreenCount request.
type GetScreenCountReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
ScreenCount byte
Window xproto.Window
}
// Reply blocks and returns the reply data for a GetScreenCount request.
func (cook GetScreenCountCookie) Reply() (*GetScreenCountReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return getScreenCountReply(buf), nil
}
// getScreenCountReply reads a byte slice into a GetScreenCountReply value.
func getScreenCountReply(buf []byte) *GetScreenCountReply {
v := new(GetScreenCountReply)
b := 1 // skip reply determinant
v.ScreenCount = buf[b]
b += 1
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.Window = xproto.Window(xgb.Get32(buf[b:]))
b += 4
return v
}
// Write request to wire for GetScreenCount
// getScreenCountRequest writes a GetScreenCount request to a byte slice.
func getScreenCountRequest(c *xgb.Conn, Window xproto.Window) []byte {
size := 8
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 2 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
xgb.Put32(buf[b:], uint32(Window))
b += 4
return buf
}
// GetScreenSizeCookie is a cookie used only for GetScreenSize requests.
type GetScreenSizeCookie struct {
*xgb.Cookie
}
// GetScreenSize sends a checked request.
// If an error occurs, it will be returned with the reply by calling GetScreenSizeCookie.Reply()
func GetScreenSize(c *xgb.Conn, Window xproto.Window, Screen uint32) GetScreenSizeCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetScreenSize' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(getScreenSizeRequest(c, Window, Screen), cookie)
return GetScreenSizeCookie{cookie}
}
// GetScreenSizeUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func GetScreenSizeUnchecked(c *xgb.Conn, Window xproto.Window, Screen uint32) GetScreenSizeCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetScreenSize' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(getScreenSizeRequest(c, Window, Screen), cookie)
return GetScreenSizeCookie{cookie}
}
// GetScreenSizeReply represents the data returned from a GetScreenSize request.
type GetScreenSizeReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
// padding: 1 bytes
Width uint32
Height uint32
Window xproto.Window
Screen uint32
}
// Reply blocks and returns the reply data for a GetScreenSize request.
func (cook GetScreenSizeCookie) Reply() (*GetScreenSizeReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return getScreenSizeReply(buf), nil
}
// getScreenSizeReply reads a byte slice into a GetScreenSizeReply value.
func getScreenSizeReply(buf []byte) *GetScreenSizeReply {
v := new(GetScreenSizeReply)
b := 1 // skip reply determinant
b += 1 // padding
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.Width = xgb.Get32(buf[b:])
b += 4
v.Height = xgb.Get32(buf[b:])
b += 4
v.Window = xproto.Window(xgb.Get32(buf[b:]))
b += 4
v.Screen = xgb.Get32(buf[b:])
b += 4
return v
}
// Write request to wire for GetScreenSize
// getScreenSizeRequest writes a GetScreenSize request to a byte slice.
func getScreenSizeRequest(c *xgb.Conn, Window xproto.Window, Screen uint32) []byte {
size := 12
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 3 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
xgb.Put32(buf[b:], uint32(Window))
b += 4
xgb.Put32(buf[b:], Screen)
b += 4
return buf
}
// GetStateCookie is a cookie used only for GetState requests.
type GetStateCookie struct {
*xgb.Cookie
}
// GetState sends a checked request.
// If an error occurs, it will be returned with the reply by calling GetStateCookie.Reply()
func GetState(c *xgb.Conn, Window xproto.Window) GetStateCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetState' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(getStateRequest(c, Window), cookie)
return GetStateCookie{cookie}
}
// GetStateUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func GetStateUnchecked(c *xgb.Conn, Window xproto.Window) GetStateCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'GetState' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(getStateRequest(c, Window), cookie)
return GetStateCookie{cookie}
}
// GetStateReply represents the data returned from a GetState request.
type GetStateReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
State byte
Window xproto.Window
}
// Reply blocks and returns the reply data for a GetState request.
func (cook GetStateCookie) Reply() (*GetStateReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return getStateReply(buf), nil
}
// getStateReply reads a byte slice into a GetStateReply value.
func getStateReply(buf []byte) *GetStateReply {
v := new(GetStateReply)
b := 1 // skip reply determinant
v.State = buf[b]
b += 1
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.Window = xproto.Window(xgb.Get32(buf[b:]))
b += 4
return v
}
// Write request to wire for GetState
// getStateRequest writes a GetState request to a byte slice.
func getStateRequest(c *xgb.Conn, Window xproto.Window) []byte {
size := 8
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 1 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
xgb.Put32(buf[b:], uint32(Window))
b += 4
return buf
}
// IsActiveCookie is a cookie used only for IsActive requests.
type IsActiveCookie struct {
*xgb.Cookie
}
// IsActive sends a checked request.
// If an error occurs, it will be returned with the reply by calling IsActiveCookie.Reply()
func IsActive(c *xgb.Conn) IsActiveCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'IsActive' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(isActiveRequest(c), cookie)
return IsActiveCookie{cookie}
}
// IsActiveUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func IsActiveUnchecked(c *xgb.Conn) IsActiveCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'IsActive' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(isActiveRequest(c), cookie)
return IsActiveCookie{cookie}
}
// IsActiveReply represents the data returned from a IsActive request.
type IsActiveReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
// padding: 1 bytes
State uint32
}
// Reply blocks and returns the reply data for a IsActive request.
func (cook IsActiveCookie) Reply() (*IsActiveReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return isActiveReply(buf), nil
}
// isActiveReply reads a byte slice into a IsActiveReply value.
func isActiveReply(buf []byte) *IsActiveReply {
v := new(IsActiveReply)
b := 1 // skip reply determinant
b += 1 // padding
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.State = xgb.Get32(buf[b:])
b += 4
return v
}
// Write request to wire for IsActive
// isActiveRequest writes a IsActive request to a byte slice.
func isActiveRequest(c *xgb.Conn) []byte {
size := 4
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 4 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
return buf
}
// QueryScreensCookie is a cookie used only for QueryScreens requests.
type QueryScreensCookie struct {
*xgb.Cookie
}
// QueryScreens sends a checked request.
// If an error occurs, it will be returned with the reply by calling QueryScreensCookie.Reply()
func QueryScreens(c *xgb.Conn) QueryScreensCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'QueryScreens' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(queryScreensRequest(c), cookie)
return QueryScreensCookie{cookie}
}
// QueryScreensUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func QueryScreensUnchecked(c *xgb.Conn) QueryScreensCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'QueryScreens' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(queryScreensRequest(c), cookie)
return QueryScreensCookie{cookie}
}
// QueryScreensReply represents the data returned from a QueryScreens request.
type QueryScreensReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
// padding: 1 bytes
Number uint32
// padding: 20 bytes
ScreenInfo []ScreenInfo // size: xgb.Pad((int(Number) * 8))
}
// Reply blocks and returns the reply data for a QueryScreens request.
func (cook QueryScreensCookie) Reply() (*QueryScreensReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return queryScreensReply(buf), nil
}
// queryScreensReply reads a byte slice into a QueryScreensReply value.
func queryScreensReply(buf []byte) *QueryScreensReply {
v := new(QueryScreensReply)
b := 1 // skip reply determinant
b += 1 // padding
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.Number = xgb.Get32(buf[b:])
b += 4
b += 20 // padding
v.ScreenInfo = make([]ScreenInfo, v.Number)
b += ScreenInfoReadList(buf[b:], v.ScreenInfo)
return v
}
// Write request to wire for QueryScreens
// queryScreensRequest writes a QueryScreens request to a byte slice.
func queryScreensRequest(c *xgb.Conn) []byte {
size := 4
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 5 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
return buf
}
// QueryVersionCookie is a cookie used only for QueryVersion requests.
type QueryVersionCookie struct {
*xgb.Cookie
}
// QueryVersion sends a checked request.
// If an error occurs, it will be returned with the reply by calling QueryVersionCookie.Reply()
func QueryVersion(c *xgb.Conn, Major byte, Minor byte) QueryVersionCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'QueryVersion' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(true, true)
c.NewRequest(queryVersionRequest(c, Major, Minor), cookie)
return QueryVersionCookie{cookie}
}
// QueryVersionUnchecked sends an unchecked request.
// If an error occurs, it can only be retrieved using xgb.WaitForEvent or xgb.PollForEvent.
func QueryVersionUnchecked(c *xgb.Conn, Major byte, Minor byte) QueryVersionCookie {
c.ExtLock.RLock()
defer c.ExtLock.RUnlock()
if _, ok := c.Extensions["XINERAMA"]; !ok {
panic("Cannot issue request 'QueryVersion' using the uninitialized extension 'XINERAMA'. xinerama.Init(connObj) must be called first.")
}
cookie := c.NewCookie(false, true)
c.NewRequest(queryVersionRequest(c, Major, Minor), cookie)
return QueryVersionCookie{cookie}
}
// QueryVersionReply represents the data returned from a QueryVersion request.
type QueryVersionReply struct {
Sequence uint16 // sequence number of the request for this reply
Length uint32 // number of bytes in this reply
// padding: 1 bytes
Major uint16
Minor uint16
}
// Reply blocks and returns the reply data for a QueryVersion request.
func (cook QueryVersionCookie) Reply() (*QueryVersionReply, error) {
buf, err := cook.Cookie.Reply()
if err != nil {
return nil, err
}
if buf == nil {
return nil, nil
}
return queryVersionReply(buf), nil
}
// queryVersionReply reads a byte slice into a QueryVersionReply value.
func queryVersionReply(buf []byte) *QueryVersionReply {
v := new(QueryVersionReply)
b := 1 // skip reply determinant
b += 1 // padding
v.Sequence = xgb.Get16(buf[b:])
b += 2
v.Length = xgb.Get32(buf[b:]) // 4-byte units
b += 4
v.Major = xgb.Get16(buf[b:])
b += 2
v.Minor = xgb.Get16(buf[b:])
b += 2
return v
}
// Write request to wire for QueryVersion
// queryVersionRequest writes a QueryVersion request to a byte slice.
func queryVersionRequest(c *xgb.Conn, Major byte, Minor byte) []byte {
size := 8
b := 0
buf := make([]byte, size)
c.ExtLock.RLock()
buf[b] = c.Extensions["XINERAMA"]
c.ExtLock.RUnlock()
b += 1
buf[b] = 0 // request opcode
b += 1
xgb.Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
buf[b] = Major
b += 1
buf[b] = Minor
b += 1
return buf
}

@ -0,0 +1,6 @@
*.swp
*.png
tst_first
tst_graphics
TAGS

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

@ -0,0 +1,36 @@
all: callback.go types_auto.go gofmt
install:
go install -p 6 . ./ewmh ./gopher ./icccm ./keybind ./motif ./mousebind \
./xcursor ./xevent ./xgraphics ./xinerama ./xprop ./xrect ./xwindow
push:
git push origin master
git push github master
build-ex:
find ./_examples/ -type d -wholename './_examples/[a-z]*' -print0 \
| xargs -0 go build -p 6
gofmt:
gofmt -w *.go */*.go _examples/*/*.go
colcheck *.go */*.go _examples/*/*.go
callback.go:
scripts/write-events callbacks > xevent/callback.go
types_auto.go:
scripts/write-events evtypes > xevent/types_auto.go
tags:
find ./ \( -name '*.go' -and -not -wholename './tests/*' -and -not -wholename './_examples/*' \) -print0 | xargs -0 gotags > TAGS
loc:
find ./ -name '*.go' -and -not -wholename './tests*' -and -not -name '*keysymdef.go' -and -not -name '*gopher.go' -print | sort | xargs wc -l
ex-%:
go run _examples/$*/main.go
gopherimg:
go-bindata -f GopherPng -p gopher -i gopher/gophercolor-small.png -o gopher/gopher.go

@ -0,0 +1,55 @@
xgbutil is a utility library designed to work with the X Go Binding. This
project's main goal is to make various X related tasks easier. For example,
binding keys, using the EWMH or ICCCM specs with the window manager,
moving/resizing windows, assigning function callbacks to particular events,
drawing images to a window, etc.
xgbutil attempts to be thread safe, but it has not been completely tested in
this regard. In general, the X event loop implemented in the xevent package is
sequential. The idea is to be sequential by default, and let the user spawn
concurrent code at their discretion. (i.e., the complexity of making the main
event loop generally concurrent is vast.)
You may sleep safely at night by assuming that XGB is thread safe, though.
To start using xgbutil, you should have at least a passing familiarity with X.
Your first stop should be the examples directory.
Installation
============
go get github.com/BurntSushi/xgbutil
Dependencies
============
XGB is the main dependency. Use of the xgraphics packages requires graphics-go
and freetype-go.
XGB project URL: https://github.com/BurntSushi/xgb
Quick Example
=============
go get github.com/BurntSushi/xgbutil/_examples/window-name-sizes
"$GOPATH"/bin/window-name-sizes
The output will be a list of names of all top-level windows and their geometry
including window manager decorations. (Assuming your window manager supports
some basic EWMH properties.)
Documentation
=============
https://godoc.org/github.com/BurntSushi/xgbutil
Examples
========
There are several examples in the examples directory covering common use cases.
They are heavily documented and should run out of the box.
Python
======
An older project of mine, xpybutil, served as inspiration for xgbutil. If you
want to use Python, xpybutil should help quite a bit. Please note though, that
at this point, xgbutil provides a lot more functionality and is much better
documented.
xpybutil project URL: https://github.com/BurntSushi/xpybutil

@ -0,0 +1,29 @@
I like to keep all my code to 80 columns or less. I have plenty of screen real
estate, but enjoy 80 columns so that I can have multiple code windows open side
to side and not be plagued by the ugly auto-wrapping of a text editor.
If you don't oblige me, I will fix any patch you submit to abide 80 columns.
Note that this style restriction does not preclude gofmt, but introduces a few
peculiarities. The first is that gofmt will occasionally add spacing (typically
to comments) that ends up going over 80 columns. Either shorten the comment or
put it on its own line.
The second and more common hiccup is when a function definition extends beyond
80 columns. If one adds line breaks to keep it below 80 columns, gofmt will
indent all subsequent lines in a function definition to the same indentation
level of the function body. This results in a less-than-ideal separation
between function definition and function body. To remedy this, simply add a
line break like so:
func RestackWindowExtra(xu *xgbutil.XUtil, win xproto.Window, stackMode int,
sibling xproto.Window, source int) error {
return ClientEvent(xu, win, "_NET_RESTACK_WINDOW", source, int(sibling),
stackMode)
}
Something similar should also be applied to long 'if' or 'for' conditionals,
although it would probably be preferrable to break up the conditional to
smaller chunks with a few helper variables.

@ -0,0 +1,67 @@
/*
Package xgbutil is a utility library designed to make common tasks with the X
server easier. The central design choice that has driven development is to hide
the complexity of X wherever possible but expose it when necessary.
For example, the xevent package provides an implementation of an X event loop
that acts as a dispatcher to event handlers set up with the xevent, keybind and
mousebind packages. At the same time, the event queue is exposed and can be
modified using xevent.Peek and xevent.DequeueAt.
Sub-packages
The xgbutil package is considerably small, and only contains some type
definitions and the initial setup for an X connection. Much of the
functionality of xgbutil comes from its sub-packages. Each sub-package is
appropriately documented.
Installation
xgbutil is go-gettable:
go get github.com/BurntSushi/xgbutil
Dependencies
XGB is the main dependency, and is required for all packages inside xgbutil.
graphics-go and freetype-go are also required if using the xgraphics package.
Quick Example
A quick example to demonstrate that xgbutil is working correctly:
go get github.com/BurntSushi/xgbutil/examples/window-name-sizes
GO/PATH/bin/window-name-sizes
The output will be a list of names of all top-level windows and their geometry
including window manager decorations. (Assuming your window manager supports
some basic EWMH properties.)
Examples
The examples directory contains a sizable number of examples demonstrating
common tasks with X. They are intended to demonstrate a single thing each,
although a few that require setup are necessarily long. Each example is
heavily documented.
The examples directory should be your first stop when learning how to use
xgbutil.
xgbutil is also used heavily throughout my window manager, Wingo. It may be
useful reference material.
Wingo project page: https://github.com/BurntSushi/wingo
Thread Safety
While I am fairly confident that XGB is thread safe, I am only somewhat
confident that xgbutil is thread safe. It simply has not been tested enough for
my confidence to be higher.
Note that the xevent package's X event loop is not concurrent. Namely,
designing a generally concurrent X event loop is extremely complex. Instead,
the onus is on you, the user, to design concurrent callback functions if
concurrency is desired.
*/
package xgbutil

@ -0,0 +1,56 @@
/*
Package ewmh provides a comprehensive API to get and set properties specified
by the EWMH spec, as well as perform actions specified by the EWMH spec.
Since there are so many functions and they adhere to an existing spec,
this package file does not contain much documentation. Indeed, each
method has only a single comment associated with it: the EWMH property name.
The idea is to
provide a consistent interface to use all facilities described in the EWMH
spec: http://standards.freedesktop.org/wm-spec/wm-spec-latest.html.
Naming scheme
Using "_NET_ACTIVE_WINDOW" as an example,
functions "ActiveWindowGet" and "ActiveWindowSet" get and set the
property, respectively. Both of these functions exist for most EWMH
properties. Additionally, some EWMH properties support sending a client
message event to request the window manager to perform some action. In the
case of "_NET_ACTIVE_WINDOW", this request is used to set the active
window.
These sorts of functions end in "Req". So for "_NET_ACTIVE_WINDOW",
the method name is "ActiveWindowReq". Moreover, most requests include
various parameters that don't need to be changed often (like the source
indication). Thus, by default, functions ending in "Req" force these to
sensible defaults. If you need access to all of the parameters, use the
corresponding "ReqExtra" method. So for "_NET_ACTIVE_WINDOW", that would
be "ActiveWindowReqExtra". (If no "ReqExtra" method exists, then the
"Req" method covers all available parameters.)
This naming scheme has one exception: if a property's only use is through
sending an event (like "_NET_CLOSE_WINDOW"), then the name will be
"CloseWindow" for the short-hand version and "CloseWindowExtra"
for access to all of the parameters. (Since there is no "_NET_CLOSE_WINDOW"
property, there is no need for "CloseWindowGet" and "CloseWindowSet"
functions.)
For properties that store more than just a simple integer, name or list
of integers, structs have been created and exposed to organize the
information returned in a sensible manner. For example, the
"_NET_DESKTOP_GEOMETRY" property would typically return a slice of integers
of length 2, where the first integer is the width and the second is the
height. Xgbutil will wrap this in a struct with the obvious members. These
structs are documented.
Finally, functions ending in "*Set" are typically only used when setting
properties on clients *you've* created or when the window manager sets
properties. Thus, it's unlikely that you should use them unless you're
creating a top-level client or building a window manager.
Functions ending in "Get" or "Req[Extra]" are commonly used.
N.B. Not all properties have "*Req" functions.
*/
package ewmh

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
package ewmh
import (
"fmt"
"github.com/BurntSushi/xgbutil"
)
// GetEwmhWM uses the EWMH spec to find if a conforming window manager
// is currently running or not. If it is, then its name will be returned.
// Otherwise, an error will be returned explaining why one couldn't be found.
func GetEwmhWM(xu *xgbutil.XUtil) (string, error) {
childCheck, err := SupportingWmCheckGet(xu, xu.RootWin())
if err != nil {
return "", fmt.Errorf("GetEwmhWM: Failed because: %s", err)
}
childCheck2, err := SupportingWmCheckGet(xu, childCheck)
if err != nil {
return "", fmt.Errorf("GetEwmhWM: Failed because: %s", err)
}
if childCheck != childCheck2 {
return "", fmt.Errorf(
"GetEwmhWM: _NET_SUPPORTING_WM_CHECK value on the root window "+
"(%x) does not match _NET_SUPPORTING_WM_CHECK value "+
"on the child window (%x).", childCheck, childCheck2)
}
return WmNameGet(xu, childCheck)
}

@ -0,0 +1,61 @@
/*
Package icccm provides an API for a portion of the ICCCM, namely, getters
and setters for many of the properties specified in the ICCCM. There is also a
smattering of support for other protocols specified by ICCCM. For example, to
satisfy the WM_DELETE_WINDOW protocol, package icccm provides 'IsDeleteProtocol'
which returns whether a ClientMessage event satisfies the WM_DELETE_WINDOW
protocol.
If a property has values that aren't simple strings or integers, struct types
are provided to organize the data. In particular, WM_NORMAL_HINTS and WM_HINTS.
Also note that properties like WM_NORMAL_HINTS and WM_HINTS contain a 'Flags'
field (a bit mask) that specifies which values are "active". This is of
importance when setting and reading WM_NORMAL_HINTS and WM_HINTS; one must make
sure the appropriate bit is set in Flags.
For example, you might want to check if a window has specified a resize
increment in the WM_NORMAL_HINTS property. The values in the corresponding
NormalHints struct are WidthInc and HeightInc. So to check if such values exist
*and* should be used:
normalHints, err := icccm.WmNormalHintsGet(XUtilValue, window-id)
if err != nil {
// handle error
}
if normalHints.Flags&icccm.SizeHintPResizeInc > 0 {
// Use normalHints.WidthInc and normalHints.HeightInc
}
When you should use icccm
Although the ICCCM is extremely old, a lot of it is still used. In fact, the
EWMH spec itself specifically states that the ICCCM should still be used unless
otherwise noted by the EWMH. For example, WM_HINTS and WM_NORMAL_HINTS are
still used, but _NET_WM_NAME replaces WM_NAME.
With that said, many applications (like xterm or LibreOffice) have not been
updated to be fully EWMH compliant. Therefore, code that finds a window's name
often looks like this:
winName, err := ewmh.WmNameGet(XUtilValue, window-id)
if err != nil || winName == "" {
winName, err = icccm.WmNameGet(XUtilValue, window-id)
if err != nill || winName == "" {
winName = "N/A"
}
}
Something similar can be said for the _NET_WM_ICON and the IconPixmap field
in WM_HINTS.
Naming scheme
The naming scheme is precisely the same as the one found in the ewmh package.
The documentation for the ewmh package describes the naming scheme in more
detail. The only difference (currently) is that the icccm package only contains
functions ending in "Get" and "Set". It is planned to add "Req" functions. (An
example of a Req function would be to send a ClientMessage implementing the
WM_DELETE_WINDOW protocol to a client window.)
*/
package icccm

@ -0,0 +1,358 @@
package icccm
import (
"fmt"
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/xprop"
)
const (
HintInput = (1 << iota)
HintState
HintIconPixmap
HintIconWindow
HintIconPosition
HintIconMask
HintWindowGroup
HintMessage
HintUrgency
)
const (
SizeHintUSPosition = (1 << iota)
SizeHintUSSize
SizeHintPPosition
SizeHintPSize
SizeHintPMinSize
SizeHintPMaxSize
SizeHintPResizeInc
SizeHintPAspect
SizeHintPBaseSize
SizeHintPWinGravity
)
const (
StateWithdrawn = iota
StateNormal
StateZoomed
StateIconic
StateInactive
)
// WM_NAME get
func WmNameGet(xu *xgbutil.XUtil, win xproto.Window) (string, error) {
return xprop.PropValStr(xprop.GetProperty(xu, win, "WM_NAME"))
}
// WM_NAME set
func WmNameSet(xu *xgbutil.XUtil, win xproto.Window, name string) error {
return xprop.ChangeProp(xu, win, 8, "WM_NAME", "STRING", ([]byte)(name))
}
// WM_ICON_NAME get
func WmIconNameGet(xu *xgbutil.XUtil, win xproto.Window) (string, error) {
return xprop.PropValStr(xprop.GetProperty(xu, win, "WM_ICON_NAME"))
}
// WM_ICON_NAME set
func WmIconNameSet(xu *xgbutil.XUtil, win xproto.Window, name string) error {
return xprop.ChangeProp(xu, win, 8, "WM_ICON_NAME", "STRING",
([]byte)(name))
}
// NormalHints is a struct that organizes the information related to the
// WM_NORMAL_HINTS property. Please see the ICCCM spec for more details.
type NormalHints struct {
Flags uint
X, Y int
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight uint
WidthInc, HeightInc uint
MinAspectNum, MinAspectDen, MaxAspectNum, MaxAspectDen uint
BaseWidth, BaseHeight, WinGravity uint
}
// WM_NORMAL_HINTS get
func WmNormalHintsGet(xu *xgbutil.XUtil,
win xproto.Window) (nh *NormalHints, err error) {
lenExpect := 18
hints, err := xprop.PropValNums(xprop.GetProperty(xu, win,
"WM_NORMAL_HINTS"))
if err != nil {
return nil, err
}
if len(hints) != lenExpect {
return nil,
fmt.Errorf("WmNormalHint: There are %d fields in WM_NORMAL_HINTS, "+
"but xgbutil expects %d.", len(hints), lenExpect)
}
nh = &NormalHints{}
nh.Flags = hints[0]
nh.X = int(hints[1])
nh.Y = int(hints[2])
nh.Width = hints[3]
nh.Height = hints[4]
nh.MinWidth = hints[5]
nh.MinHeight = hints[6]
nh.MaxWidth = hints[7]
nh.MaxHeight = hints[8]
nh.WidthInc = hints[9]
nh.HeightInc = hints[10]
nh.MinAspectNum = hints[11]
nh.MinAspectDen = hints[12]
nh.MaxAspectNum = hints[13]
nh.MaxAspectDen = hints[14]
nh.BaseWidth = hints[15]
nh.BaseHeight = hints[16]
nh.WinGravity = hints[17]
if nh.WinGravity <= 0 {
nh.WinGravity = xproto.GravityNorthWest
}
return nh, nil
}
// WM_NORMAL_HINTS set
// Make sure to set the flags in the NormalHints struct correctly!
func WmNormalHintsSet(xu *xgbutil.XUtil, win xproto.Window,
nh *NormalHints) error {
raw := []uint{
nh.Flags,
uint(nh.X), uint(nh.Y), nh.Width, nh.Height,
nh.MinWidth, nh.MinHeight,
nh.MaxWidth, nh.MaxHeight,
nh.WidthInc, nh.HeightInc,
nh.MinAspectNum, nh.MinAspectDen,
nh.MaxAspectNum, nh.MaxAspectDen,
nh.BaseWidth, nh.BaseHeight,
nh.WinGravity,
}
return xprop.ChangeProp32(xu, win, "WM_NORMAL_HINTS", "WM_SIZE_HINTS",
raw...)
}
// Hints is a struct that organizes information related to the WM_HINTS
// property. Once again, I refer you to the ICCCM spec for documentation.
type Hints struct {
Flags uint
Input, InitialState uint
IconX, IconY int
IconPixmap, IconMask xproto.Pixmap
WindowGroup, IconWindow xproto.Window
}
// WM_HINTS get
func WmHintsGet(xu *xgbutil.XUtil,
win xproto.Window) (hints *Hints, err error) {
lenExpect := 9
raw, err := xprop.PropValNums(xprop.GetProperty(xu, win, "WM_HINTS"))
if err != nil {
return nil, err
}
if len(raw) != lenExpect {
return nil,
fmt.Errorf("WmHints: There are %d fields in "+
"WM_HINTS, but xgbutil expects %d.", len(raw), lenExpect)
}
hints = &Hints{}
hints.Flags = raw[0]
hints.Input = raw[1]
hints.InitialState = raw[2]
hints.IconPixmap = xproto.Pixmap(raw[3])
hints.IconWindow = xproto.Window(raw[4])
hints.IconX = int(raw[5])
hints.IconY = int(raw[6])
hints.IconMask = xproto.Pixmap(raw[7])
hints.WindowGroup = xproto.Window(raw[8])
return hints, nil
}
// WM_HINTS set
// Make sure to set the flags in the Hints struct correctly!
func WmHintsSet(xu *xgbutil.XUtil, win xproto.Window, hints *Hints) error {
raw := []uint{
hints.Flags, hints.Input, hints.InitialState,
uint(hints.IconPixmap), uint(hints.IconWindow),
uint(hints.IconX), uint(hints.IconY),
uint(hints.IconMask),
uint(hints.WindowGroup),
}
return xprop.ChangeProp32(xu, win, "WM_HINTS", "WM_HINTS", raw...)
}
// WmClass struct contains two data points:
// the instance and a class of a window.
type WmClass struct {
Instance, Class string
}
// WM_CLASS get
func WmClassGet(xu *xgbutil.XUtil, win xproto.Window) (*WmClass, error) {
raw, err := xprop.PropValStrs(xprop.GetProperty(xu, win, "WM_CLASS"))
if err != nil {
return nil, err
}
if len(raw) != 2 {
return nil,
fmt.Errorf("WmClass: Two string make up WM_CLASS, but "+
"xgbutil found %d in '%v'.", len(raw), raw)
}
return &WmClass{
Instance: raw[0],
Class: raw[1],
}, nil
}
// WM_CLASS set
func WmClassSet(xu *xgbutil.XUtil, win xproto.Window, class *WmClass) error {
raw := make([]byte, len(class.Instance)+len(class.Class)+2)
copy(raw, class.Instance)
copy(raw[(len(class.Instance)+1):], class.Class)
return xprop.ChangeProp(xu, win, 8, "WM_CLASS", "STRING", raw)
}
// WM_TRANSIENT_FOR get
func WmTransientForGet(xu *xgbutil.XUtil,
win xproto.Window) (xproto.Window, error) {
return xprop.PropValWindow(xprop.GetProperty(xu, win, "WM_TRANSIENT_FOR"))
}
// WM_TRANSIENT_FOR set
func WmTransientForSet(xu *xgbutil.XUtil, win xproto.Window,
transient xproto.Window) error {
return xprop.ChangeProp32(xu, win, "WM_TRANSIENT_FOR", "WINDOW",
uint(transient))
}
// WM_PROTOCOLS get
func WmProtocolsGet(xu *xgbutil.XUtil, win xproto.Window) ([]string, error) {
raw, err := xprop.GetProperty(xu, win, "WM_PROTOCOLS")
return xprop.PropValAtoms(xu, raw, err)
}
// WM_PROTOCOLS set
func WmProtocolsSet(xu *xgbutil.XUtil, win xproto.Window,
atomNames []string) error {
atoms, err := xprop.StrToAtoms(xu, atomNames)
if err != nil {
return err
}
return xprop.ChangeProp32(xu, win, "WM_PROTOCOLS", "ATOM", atoms...)
}
// WM_COLORMAP_WINDOWS get
func WmColormapWindowsGet(xu *xgbutil.XUtil,
win xproto.Window) ([]xproto.Window, error) {
return xprop.PropValWindows(xprop.GetProperty(xu, win,
"WM_COLORMAP_WINDOWS"))
}
// WM_COLORMAP_WINDOWS set
func WmColormapWindowsSet(xu *xgbutil.XUtil, win xproto.Window,
windows []xproto.Window) error {
return xprop.ChangeProp32(xu, win, "WM_COLORMAP_WINDOWS", "WINDOW",
xprop.WindowToInt(windows)...)
}
// WM_CLIENT_MACHINE get
func WmClientMachineGet(xu *xgbutil.XUtil, win xproto.Window) (string, error) {
return xprop.PropValStr(xprop.GetProperty(xu, win, "WM_CLIENT_MACHINE"))
}
// WM_CLIENT_MACHINE set
func WmClientMachineSet(xu *xgbutil.XUtil, win xproto.Window,
client string) error {
return xprop.ChangeProp(xu, win, 8, "WM_CLIENT_MACHINE", "STRING",
([]byte)(client))
}
// WmState is a struct that organizes information related to the WM_STATE
// property. Namely, the state (corresponding to a State* constant in this file)
// and the icon window (probably not used).
type WmState struct {
State uint
Icon xproto.Window
}
// WM_STATE get
func WmStateGet(xu *xgbutil.XUtil, win xproto.Window) (*WmState, error) {
raw, err := xprop.PropValNums(xprop.GetProperty(xu, win, "WM_STATE"))
if err != nil {
return nil, err
}
if len(raw) != 2 {
return nil,
fmt.Errorf("WmState: Expected two integers in WM_STATE property "+
"but xgbutil found %d in '%v'.", len(raw), raw)
}
return &WmState{
State: raw[0],
Icon: xproto.Window(raw[1]),
}, nil
}
// WM_STATE set
func WmStateSet(xu *xgbutil.XUtil, win xproto.Window, state *WmState) error {
raw := []uint{
state.State,
uint(state.Icon),
}
return xprop.ChangeProp32(xu, win, "WM_STATE", "WM_STATE", raw...)
}
// IconSize is a struct the organizes information related to the WM_ICON_SIZE
// property. Mostly info about its dimensions.
type IconSize struct {
MinWidth, MinHeight, MaxWidth, MaxHeight, WidthInc, HeightInc uint
}
// WM_ICON_SIZE get
func WmIconSizeGet(xu *xgbutil.XUtil, win xproto.Window) (*IconSize, error) {
raw, err := xprop.PropValNums(xprop.GetProperty(xu, win, "WM_ICON_SIZE"))
if err != nil {
return nil, err
}
if len(raw) != 6 {
return nil,
fmt.Errorf("WmIconSize: Expected six integers in WM_ICON_SIZE "+
"property, but xgbutil found "+"%d in '%v'.", len(raw), raw)
}
return &IconSize{
MinWidth: raw[0], MinHeight: raw[1],
MaxWidth: raw[2], MaxHeight: raw[3],
WidthInc: raw[4], HeightInc: raw[5],
}, nil
}
// WM_ICON_SIZE set
func WmIconSizeSet(xu *xgbutil.XUtil, win xproto.Window,
icondim *IconSize) error {
raw := []uint{
icondim.MinWidth, icondim.MinHeight,
icondim.MaxWidth, icondim.MaxHeight,
icondim.WidthInc, icondim.HeightInc,
}
return xprop.ChangeProp32(xu, win, "WM_ICON_SIZE", "WM_ICON_SIZE", raw...)
}

@ -0,0 +1,64 @@
package icccm
import (
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/xevent"
"github.com/BurntSushi/xgbutil/xprop"
)
// IsDeleteProtocol checks whether a ClientMessage event satisfies the
// WM_DELETE_WINDOW protocol. Namely, the format must be 32, the type must
// be the WM_PROTOCOLS atom, and the first data item must be the atom
// WM_DELETE_WINDOW.
//
// Note that if you're using the xwindow package, you should use the
// WMGracefulClose method instead of directly using IsDeleteProtocol.
func IsDeleteProtocol(X *xgbutil.XUtil, ev xevent.ClientMessageEvent) bool {
// Make sure the Format is 32. (Meaning that each data item is
// 32 bits.)
if ev.Format != 32 {
return false
}
// Check to make sure the Type atom is WM_PROTOCOLS.
typeName, err := xprop.AtomName(X, ev.Type)
if err != nil || typeName != "WM_PROTOCOLS" { // not what we want
return false
}
// Check to make sure the first data item is WM_DELETE_WINDOW.
protocolType, err := xprop.AtomName(X,
xproto.Atom(ev.Data.Data32[0]))
if err != nil || protocolType != "WM_DELETE_WINDOW" {
return false
}
return true
}
// IsFocusProtocol checks whether a ClientMessage event satisfies the
// WM_TAKE_FOCUS protocol.
func IsFocusProtocol(X *xgbutil.XUtil, ev xevent.ClientMessageEvent) bool {
// Make sure the Format is 32. (Meaning that each data item is
// 32 bits.)
if ev.Format != 32 {
return false
}
// Check to make sure the Type atom is WM_PROTOCOLS.
typeName, err := xprop.AtomName(X, ev.Type)
if err != nil || typeName != "WM_PROTOCOLS" { // not what we want
return false
}
// Check to make sure the first data item is WM_TAKE_FOCUS.
protocolType, err := xprop.AtomName(X,
xproto.Atom(ev.Data.Data32[0]))
if err != nil || protocolType != "WM_TAKE_FOCUS" {
return false
}
return true
}

@ -0,0 +1 @@
au BufWritePost *.go silent!make tags > /dev/null 2>&1

@ -0,0 +1,167 @@
package xgbutil
/*
types.go contains several types used in the XUtil structure. In an ideal world,
they would be defined in their appropriate packages, but must be defined here
(and exported) for use in some sub-packages. (Namely, xevent, keybind and
mousebind.)
*/
import (
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
// Callback is an interface that should be implemented by event callback
// functions. Namely, to assign a function to a particular event/window
// combination, simply define a function with type 'SomeEventFun' (pre-defined
// in xevent/callback.go), and call the 'Connect' method.
// The 'Run' method is used inside the Main event loop, and shouldn't be used
// by the user.
// Also, it is perfectly legitimate to connect to events that don't specify
// a window (like MappingNotify and KeymapNotify). In this case, simply
// use 'xgbutil.NoWindow' as the window id.
//
// Example to respond to ConfigureNotify events on window 0x1
//
// xevent.ConfigureNotifyFun(
// func(X *xgbutil.XUtil, e xevent.ConfigureNotifyEvent) {
// fmt.Printf("(%d, %d) %dx%d\n", e.X, e.Y, e.Width, e.Height)
// }).Connect(X, 0x1)
type Callback interface {
// Connect modifies XUtil's state to attach an event handler to a
// particular event.
Connect(xu *XUtil, win xproto.Window)
// Run is exported for use in the xevent package but should not be
// used by the user. (It is used to run the callback function in the
// main event loop.)
Run(xu *XUtil, ev interface{})
}
// CallbackHook works similarly to the more general Callback, but it is
// for hooks into the main xevent loop. As such it does not get attached
// to a window.
type CallbackHook interface {
// Connect connects this hook to the main loop of the passed XUtil
// instance.
Connect(xu *XUtil)
// Run is exported for use in the xevent package, but should not be
// used by the user. It should return true if it's ok to process
// the event as usual, or false if it should be suppressed.
Run(xu *XUtil, ev interface{}) bool
}
// CallbackKey works similarly to the more general Callback, but it adds
// parameters specific to key bindings.
type CallbackKey interface {
// Connect modifies XUtil's state to attach an event handler to a
// particular key press. If grab is true, connect will request a passive
// grab.
Connect(xu *XUtil, win xproto.Window, keyStr string, grab bool) error
// Run is exported for use in the keybind package but should not be
// used by the user. (It is used to run the callback function in the
// main event loop.
Run(xu *XUtil, ev interface{})
}
// CallbackMouse works similarly to the more general Callback, but it adds
// parameters specific to mouse bindings.
type CallbackMouse interface {
// Connect modifies XUtil's state to attach an event handler to a
// particular button press.
// If sync is true, the grab will be synchronous. (This will require a
// call to xproto.AllowEvents in response, otherwise no further events
// will be processed and your program will lock.)
// If grab is true, connect will request a passive grab.
Connect(xu *XUtil, win xproto.Window, buttonStr string,
sync bool, grab bool) error
// Run is exported for use in the mousebind package but should not be
// used by the user. (It is used to run the callback function in the
// main event loop.)
Run(xu *XUtil, ev interface{})
}
// KeyKey is the type of the key in the map of keybindings.
// It essentially represents the tuple
// (event type, window id, modifier, keycode).
// It is exported for use in the keybind package. It should not be used.
type KeyKey struct {
Evtype int
Win xproto.Window
Mod uint16
Code xproto.Keycode
}
// KeyString is the type of a key binding string used to connect to particular
// key combinations. A list of all such key strings is maintained in order to
// rebind keys when the keyboard mapping has been changed.
type KeyString struct {
Str string
Callback CallbackKey
Evtype int
Win xproto.Window
Grab bool
}
// MouseKey is the type of the key in the map of mouse bindings.
// It essentially represents the tuple
// (event type, window id, modifier, button).
// It is exported for use in the mousebind package. It should not be used.
type MouseKey struct {
Evtype int
Win xproto.Window
Mod uint16
Button xproto.Button
}
// KeyboardMapping embeds a keyboard mapping reply from XGB.
// It should be retrieved using keybind.KeyMapGet, if necessary.
// xgbutil tries quite hard to absolve you from ever having to use this.
// A keyboard mapping is a table that maps keycodes to one or more keysyms.
type KeyboardMapping struct {
*xproto.GetKeyboardMappingReply
}
// ModifierMapping embeds a modifier mapping reply from XGB.
// It should be retrieved using keybind.ModMapGet, if necessary.
// xgbutil tries quite hard to absolve you from ever having to use this.
// A modifier mapping is a table that maps modifiers to one or more keycodes.
type ModifierMapping struct {
*xproto.GetModifierMappingReply
}
// ErrorHandlerFun is the type of function required to handle errors that
// come in through the main event loop.
// For example, to set a new error handler, use:
//
// xevent.ErrorHandlerSet(xgbutil.ErrorHandlerFun(
// func(err xgb.Error) {
// // do something with err
// }))
type ErrorHandlerFun func(err xgb.Error)
// EventOrError is a struct that contains either an event value or an error
// value. It is an error to contain both. Containing neither indicates an
// error too.
// This is exported for use in the xevent package. You shouldn't have any
// direct contact with values of this type, unless you need to inspect the
// queue directly with xevent.Peek.
type EventOrError struct {
Event xgb.Event
Err xgb.Error
}
// MouseDragFun is the kind of function used on each dragging step
// and at the end of a drag.
type MouseDragFun func(xu *XUtil, rootX, rootY, eventX, eventY int)
// MouseDragBeginFun is the kind of function used to initialize a drag.
// The difference between this and MouseDragFun is that the begin function
// returns a bool (of whether or not to cancel the drag) and an X resource
// identifier corresponding to a cursor.
type MouseDragBeginFun func(xu *XUtil, rootX, rootY,
eventX, eventY int) (bool, xproto.Cursor)

@ -0,0 +1,389 @@
package xevent
/*
Does all the plumbing to allow a simple callback interface for users.
This file is automatically generated using `scripts/write-events callbacks`.
Edit it at your peril.
*/
import (
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
)
type KeyPressFun func(xu *xgbutil.XUtil, event KeyPressEvent)
func (callback KeyPressFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, KeyPress, win, callback)
}
func (callback KeyPressFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(KeyPressEvent))
}
type KeyReleaseFun func(xu *xgbutil.XUtil, event KeyReleaseEvent)
func (callback KeyReleaseFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, KeyRelease, win, callback)
}
func (callback KeyReleaseFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(KeyReleaseEvent))
}
type ButtonPressFun func(xu *xgbutil.XUtil, event ButtonPressEvent)
func (callback ButtonPressFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ButtonPress, win, callback)
}
func (callback ButtonPressFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ButtonPressEvent))
}
type ButtonReleaseFun func(xu *xgbutil.XUtil, event ButtonReleaseEvent)
func (callback ButtonReleaseFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ButtonRelease, win, callback)
}
func (callback ButtonReleaseFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ButtonReleaseEvent))
}
type MotionNotifyFun func(xu *xgbutil.XUtil, event MotionNotifyEvent)
func (callback MotionNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, MotionNotify, win, callback)
}
func (callback MotionNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(MotionNotifyEvent))
}
type EnterNotifyFun func(xu *xgbutil.XUtil, event EnterNotifyEvent)
func (callback EnterNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, EnterNotify, win, callback)
}
func (callback EnterNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(EnterNotifyEvent))
}
type LeaveNotifyFun func(xu *xgbutil.XUtil, event LeaveNotifyEvent)
func (callback LeaveNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, LeaveNotify, win, callback)
}
func (callback LeaveNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(LeaveNotifyEvent))
}
type FocusInFun func(xu *xgbutil.XUtil, event FocusInEvent)
func (callback FocusInFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, FocusIn, win, callback)
}
func (callback FocusInFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(FocusInEvent))
}
type FocusOutFun func(xu *xgbutil.XUtil, event FocusOutEvent)
func (callback FocusOutFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, FocusOut, win, callback)
}
func (callback FocusOutFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(FocusOutEvent))
}
type KeymapNotifyFun func(xu *xgbutil.XUtil, event KeymapNotifyEvent)
func (callback KeymapNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, KeymapNotify, win, callback)
}
func (callback KeymapNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(KeymapNotifyEvent))
}
type ExposeFun func(xu *xgbutil.XUtil, event ExposeEvent)
func (callback ExposeFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, Expose, win, callback)
}
func (callback ExposeFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ExposeEvent))
}
type GraphicsExposureFun func(xu *xgbutil.XUtil, event GraphicsExposureEvent)
func (callback GraphicsExposureFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, GraphicsExposure, win, callback)
}
func (callback GraphicsExposureFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(GraphicsExposureEvent))
}
type NoExposureFun func(xu *xgbutil.XUtil, event NoExposureEvent)
func (callback NoExposureFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, NoExposure, win, callback)
}
func (callback NoExposureFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(NoExposureEvent))
}
type VisibilityNotifyFun func(xu *xgbutil.XUtil, event VisibilityNotifyEvent)
func (callback VisibilityNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, VisibilityNotify, win, callback)
}
func (callback VisibilityNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(VisibilityNotifyEvent))
}
type CreateNotifyFun func(xu *xgbutil.XUtil, event CreateNotifyEvent)
func (callback CreateNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, CreateNotify, win, callback)
}
func (callback CreateNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(CreateNotifyEvent))
}
type DestroyNotifyFun func(xu *xgbutil.XUtil, event DestroyNotifyEvent)
func (callback DestroyNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, DestroyNotify, win, callback)
}
func (callback DestroyNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(DestroyNotifyEvent))
}
type UnmapNotifyFun func(xu *xgbutil.XUtil, event UnmapNotifyEvent)
func (callback UnmapNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, UnmapNotify, win, callback)
}
func (callback UnmapNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(UnmapNotifyEvent))
}
type MapNotifyFun func(xu *xgbutil.XUtil, event MapNotifyEvent)
func (callback MapNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, MapNotify, win, callback)
}
func (callback MapNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(MapNotifyEvent))
}
type MapRequestFun func(xu *xgbutil.XUtil, event MapRequestEvent)
func (callback MapRequestFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, MapRequest, win, callback)
}
func (callback MapRequestFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(MapRequestEvent))
}
type ReparentNotifyFun func(xu *xgbutil.XUtil, event ReparentNotifyEvent)
func (callback ReparentNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ReparentNotify, win, callback)
}
func (callback ReparentNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ReparentNotifyEvent))
}
type ConfigureNotifyFun func(xu *xgbutil.XUtil, event ConfigureNotifyEvent)
func (callback ConfigureNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ConfigureNotify, win, callback)
}
func (callback ConfigureNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ConfigureNotifyEvent))
}
type ConfigureRequestFun func(xu *xgbutil.XUtil, event ConfigureRequestEvent)
func (callback ConfigureRequestFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ConfigureRequest, win, callback)
}
func (callback ConfigureRequestFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ConfigureRequestEvent))
}
type GravityNotifyFun func(xu *xgbutil.XUtil, event GravityNotifyEvent)
func (callback GravityNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, GravityNotify, win, callback)
}
func (callback GravityNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(GravityNotifyEvent))
}
type ResizeRequestFun func(xu *xgbutil.XUtil, event ResizeRequestEvent)
func (callback ResizeRequestFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ResizeRequest, win, callback)
}
func (callback ResizeRequestFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ResizeRequestEvent))
}
type CirculateNotifyFun func(xu *xgbutil.XUtil, event CirculateNotifyEvent)
func (callback CirculateNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, CirculateNotify, win, callback)
}
func (callback CirculateNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(CirculateNotifyEvent))
}
type CirculateRequestFun func(xu *xgbutil.XUtil, event CirculateRequestEvent)
func (callback CirculateRequestFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, CirculateRequest, win, callback)
}
func (callback CirculateRequestFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(CirculateRequestEvent))
}
type PropertyNotifyFun func(xu *xgbutil.XUtil, event PropertyNotifyEvent)
func (callback PropertyNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, PropertyNotify, win, callback)
}
func (callback PropertyNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(PropertyNotifyEvent))
}
type SelectionClearFun func(xu *xgbutil.XUtil, event SelectionClearEvent)
func (callback SelectionClearFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, SelectionClear, win, callback)
}
func (callback SelectionClearFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(SelectionClearEvent))
}
type SelectionRequestFun func(xu *xgbutil.XUtil, event SelectionRequestEvent)
func (callback SelectionRequestFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, SelectionRequest, win, callback)
}
func (callback SelectionRequestFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(SelectionRequestEvent))
}
type SelectionNotifyFun func(xu *xgbutil.XUtil, event SelectionNotifyEvent)
func (callback SelectionNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, SelectionNotify, win, callback)
}
func (callback SelectionNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(SelectionNotifyEvent))
}
type ColormapNotifyFun func(xu *xgbutil.XUtil, event ColormapNotifyEvent)
func (callback ColormapNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ColormapNotify, win, callback)
}
func (callback ColormapNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ColormapNotifyEvent))
}
type ClientMessageFun func(xu *xgbutil.XUtil, event ClientMessageEvent)
func (callback ClientMessageFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ClientMessage, win, callback)
}
func (callback ClientMessageFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ClientMessageEvent))
}
type MappingNotifyFun func(xu *xgbutil.XUtil, event MappingNotifyEvent)
func (callback MappingNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, MappingNotify, win, callback)
}
func (callback MappingNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(MappingNotifyEvent))
}
type ShapeNotifyFun func(xu *xgbutil.XUtil, event ShapeNotifyEvent)
func (callback ShapeNotifyFun) Connect(xu *xgbutil.XUtil,
win xproto.Window) {
attachCallback(xu, ShapeNotify, win, callback)
}
func (callback ShapeNotifyFun) Run(xu *xgbutil.XUtil, event interface{}) {
callback(xu, event.(ShapeNotifyEvent))
}

@ -0,0 +1,76 @@
/*
Package xevent provides an event handler interface for attaching callback
functions to X events, and an implementation of an X event loop.
The X event loop
One of the biggest conveniences offered by xgbutil is its event handler system.
That is, the ability to attach an arbitrary callback function to any X event.
In order for such things to work, xgbutil needs to control the main X event
loop and act as a dispatcher for all event handlers created by you.
To run the X event loop, use xevent.Main or xevent.MainPing. The former
runs a normal event loop in the current goroutine and processes events. The
latter runs the event loop in a new goroutine and returns a pingBefore and
a pingAfter channel. The pingBefore channel is sent a benign value right before
an event is dequeued, and the pingAfter channel is sent a benign value right
after after all callbacks for that event have finished execution. These
synchronization points in the main event loop can be combined with a 'select'
statement to process data from other input sources. An example of this is given
in the documentation for the MainPing function. A complete example called
multiple-source-event-loop can also be found in the examples directory of the
xgbutil package.
To quit the main event loop, you may use xevent.Quit, but there is nothing
inherently wrong with stopping dead using os.Exit. xevent.Quit is provided for
your convenience should you need to run any clean-up code after the main event
loop returns.
The X event queue
xgbutil's event queue contains values that are either events or errors. (Never
both and never neither.) Namely, errors are received in the event loop from
unchecked requests. (Errors generated by checked requests are guaranteed to be
returned to the caller and are never received in the event loop.) Also, a
default error handler function can be set with xevent.ErrorHandlerSet.
To this end, xgbutil's event queue can be inspected. This is advantageous when
information about what events will be processed in the future could be helpful
(i.e., if there is an UnmapNotify event waiting to be processed.) The event
queue can also be manipulated to facilitate event compression. (Two events that
are common candidates for compression are ConfigureNotify and MotionNotify.)
Detach events
Whenever a window can no longer receive events (i.e., when it is destroyed),
all event handlers related to that window should be detached. (If this is
omitted, then Go's garbage collector will not be able to reuse memory occupied
by the now-unused event handlers for that window.) Moreover, its possible that
a window id can be reused after it has been discarded, which could result in
odd behavior in your application.
To detach a window from all event handlers in the xevent package, use
xevent.Detach. If you're also using the keybind and mousebind packages, you'll
need to call keybind.Detach and mousebind.Detach too. So to detach your window
from all possible event handlers in xgbutil, use something like:
xevent.Detach(XUtilValue, your-window-id)
keybind.Detach(XUtilValue, your-window-id)
mousebind.Detach(XUtilValue, your-window-id)
Quick example
A small example that shows how to respond to ConfigureNotify events sent to
your-window-id.
xevent.ConfigureNotifyFun(
func(X *xgbutil.XUtil, e xevent.ConfigureNotifyEvent) {
fmt.Printf("(%d, %d) %dx%d\n", e.X, e.Y, e.Width, e.Height)
}).Connect(XUtilValue, your-window-id)
More examples
The xevent package is used in several of the examples in the examples directory
in the xgbutil package.
*/
package xevent

@ -0,0 +1,291 @@
package xevent
/*
xevent/eventloop.go contains code that implements a main X event loop.
Namely, it provides facilities to read new events into xevent's event queue,
run a normal main event loop and run a main event loop that pings a channel
each time an event is about to be dequeued. The latter facility allows one to
easily include other input sources for processing in a program's main event
loop.
*/
import (
"github.com/BurntSushi/xgb/shape"
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
)
// Read reads one or more events and queues them in XUtil.
// If 'block' is True, then call 'WaitForEvent' before sucking up
// all events that have been queued by XGB.
func Read(xu *xgbutil.XUtil, block bool) {
if block {
ev, err := xu.Conn().WaitForEvent()
if ev == nil && err == nil {
xgbutil.Logger.Fatal("BUG: Could not read an event or an error.")
}
Enqueue(xu, ev, err)
}
// Clean up anything that's in the queue
for {
ev, err := xu.Conn().PollForEvent()
// No events left...
if ev == nil && err == nil {
break
}
// We're good, queue it up
Enqueue(xu, ev, err)
}
}
// Main starts the main X event loop. It will read events and call appropriate
// callback functions.
// N.B. If you have multiple X connections in the same program, you should be
// able to run this in different goroutines concurrently. However, only
// *one* of these should run for *each* connection.
func Main(xu *xgbutil.XUtil) {
mainEventLoop(xu, nil, nil, nil)
}
// MainPing starts the main X event loop, and returns three "ping" channels:
// the first is pinged before an event is dequeued, the second is pinged
// after all callbacks for a particular event have been called and the last
// is pinged when the event loop stops (e.g., after a call to xevent.Quit).
// pingAfter channel.
//
// This is useful if your event loop needs to draw from other sources. e.g.,
//
// pingBefore, pingAfter, pingQuit := xevent.MainPing()
// for {
// select {
// case <-pingBefore:
// // Wait for event processing to finish.
// <-pingAfter
// case val <- someOtherChannel:
// // do some work with val
// case <-pingQuit:
// fmt.Printf("xevent loop has quit")
// return
// }
// }
//
// Note that an unbuffered channel is returned, which implies that any work
// done with 'val' will delay further X event processing.
//
// A complete example using MainPing can be found in the examples directory in
// the xgbutil package under the name multiple-source-event-loop.
func MainPing(xu *xgbutil.XUtil) (chan struct{}, chan struct{}, chan struct{}) {
pingBefore := make(chan struct{}, 0)
pingAfter := make(chan struct{}, 0)
pingQuit := make(chan struct{}, 0)
go func() {
mainEventLoop(xu, pingBefore, pingAfter, pingQuit)
}()
return pingBefore, pingAfter, pingQuit
}
// mainEventLoop runs the main event loop with an optional ping channel.
func mainEventLoop(xu *xgbutil.XUtil,
pingBefore, pingAfter, pingQuit chan struct{}) {
for {
if Quitting(xu) {
if pingQuit != nil {
pingQuit <- struct{}{}
}
break
}
// Gobble up as many events as possible (into the queue).
// If there are no events, we block.
Read(xu, true)
// Now process every event/error in the queue.
processEventQueue(xu, pingBefore, pingAfter)
}
}
// processEventQueue processes every item in the event/error queue.
func processEventQueue(xu *xgbutil.XUtil, pingBefore, pingAfter chan struct{}) {
for !Empty(xu) {
if Quitting(xu) {
return
}
// We send the ping *before* the next event is dequeued.
// This is so the queue doesn't present a misrepresentation of which
// events haven't been processed yet.
if pingBefore != nil && pingAfter != nil {
pingBefore <- struct{}{}
}
ev, err := Dequeue(xu)
// If we gobbled up an error, send it to the error event handler
// and move on the next event/error.
if err != nil {
ErrorHandlerGet(xu)(err)
if pingBefore != nil && pingAfter != nil {
pingAfter <- struct{}{}
}
continue
}
// We know there isn't an error. If there isn't an event either,
// then there's a bug somewhere.
if ev == nil {
xgbutil.Logger.Fatal("BUG: Expected an event but got nil.")
}
hooks := getHooks(xu)
for _, hook := range hooks {
if !hook.Run(xu, ev) {
goto END
}
}
switch event := ev.(type) {
case xproto.KeyPressEvent:
e := KeyPressEvent{&event}
// If we're redirecting key events, this is the place to do it!
if wid := RedirectKeyGet(xu); wid > 0 {
e.Event = wid
}
xu.TimeSet(e.Time)
runCallbacks(xu, e, KeyPress, e.Event)
case xproto.KeyReleaseEvent:
e := KeyReleaseEvent{&event}
// If we're redirecting key events, this is the place to do it!
if wid := RedirectKeyGet(xu); wid > 0 {
e.Event = wid
}
xu.TimeSet(e.Time)
runCallbacks(xu, e, KeyRelease, e.Event)
case xproto.ButtonPressEvent:
e := ButtonPressEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, ButtonPress, e.Event)
case xproto.ButtonReleaseEvent:
e := ButtonReleaseEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, ButtonRelease, e.Event)
case xproto.MotionNotifyEvent:
e := MotionNotifyEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, MotionNotify, e.Event)
case xproto.EnterNotifyEvent:
e := EnterNotifyEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, EnterNotify, e.Event)
case xproto.LeaveNotifyEvent:
e := LeaveNotifyEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, LeaveNotify, e.Event)
case xproto.FocusInEvent:
e := FocusInEvent{&event}
runCallbacks(xu, e, FocusIn, e.Event)
case xproto.FocusOutEvent:
e := FocusOutEvent{&event}
runCallbacks(xu, e, FocusOut, e.Event)
case xproto.KeymapNotifyEvent:
e := KeymapNotifyEvent{&event}
runCallbacks(xu, e, KeymapNotify, NoWindow)
case xproto.ExposeEvent:
e := ExposeEvent{&event}
runCallbacks(xu, e, Expose, e.Window)
case xproto.GraphicsExposureEvent:
e := GraphicsExposureEvent{&event}
runCallbacks(xu, e, GraphicsExposure, xproto.Window(e.Drawable))
case xproto.NoExposureEvent:
e := NoExposureEvent{&event}
runCallbacks(xu, e, NoExposure, xproto.Window(e.Drawable))
case xproto.VisibilityNotifyEvent:
e := VisibilityNotifyEvent{&event}
runCallbacks(xu, e, VisibilityNotify, e.Window)
case xproto.CreateNotifyEvent:
e := CreateNotifyEvent{&event}
runCallbacks(xu, e, CreateNotify, e.Parent)
case xproto.DestroyNotifyEvent:
e := DestroyNotifyEvent{&event}
runCallbacks(xu, e, DestroyNotify, e.Window)
case xproto.UnmapNotifyEvent:
e := UnmapNotifyEvent{&event}
runCallbacks(xu, e, UnmapNotify, e.Window)
case xproto.MapNotifyEvent:
e := MapNotifyEvent{&event}
runCallbacks(xu, e, MapNotify, e.Event)
case xproto.MapRequestEvent:
e := MapRequestEvent{&event}
runCallbacks(xu, e, MapRequest, e.Window)
runCallbacks(xu, e, MapRequest, e.Parent)
case xproto.ReparentNotifyEvent:
e := ReparentNotifyEvent{&event}
runCallbacks(xu, e, ReparentNotify, e.Window)
case xproto.ConfigureNotifyEvent:
e := ConfigureNotifyEvent{&event}
runCallbacks(xu, e, ConfigureNotify, e.Window)
case xproto.ConfigureRequestEvent:
e := ConfigureRequestEvent{&event}
runCallbacks(xu, e, ConfigureRequest, e.Window)
runCallbacks(xu, e, ConfigureRequest, e.Parent)
case xproto.GravityNotifyEvent:
e := GravityNotifyEvent{&event}
runCallbacks(xu, e, GravityNotify, e.Window)
case xproto.ResizeRequestEvent:
e := ResizeRequestEvent{&event}
runCallbacks(xu, e, ResizeRequest, e.Window)
case xproto.CirculateNotifyEvent:
e := CirculateNotifyEvent{&event}
runCallbacks(xu, e, CirculateNotify, e.Window)
case xproto.CirculateRequestEvent:
e := CirculateRequestEvent{&event}
runCallbacks(xu, e, CirculateRequest, e.Window)
case xproto.PropertyNotifyEvent:
e := PropertyNotifyEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, PropertyNotify, e.Window)
case xproto.SelectionClearEvent:
e := SelectionClearEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, SelectionClear, e.Owner)
case xproto.SelectionRequestEvent:
e := SelectionRequestEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, SelectionRequest, e.Requestor)
case xproto.SelectionNotifyEvent:
e := SelectionNotifyEvent{&event}
xu.TimeSet(e.Time)
runCallbacks(xu, e, SelectionNotify, e.Requestor)
case xproto.ColormapNotifyEvent:
e := ColormapNotifyEvent{&event}
runCallbacks(xu, e, ColormapNotify, e.Window)
case xproto.ClientMessageEvent:
e := ClientMessageEvent{&event}
runCallbacks(xu, e, ClientMessage, e.Window)
case xproto.MappingNotifyEvent:
e := MappingNotifyEvent{&event}
runCallbacks(xu, e, MappingNotify, NoWindow)
case shape.NotifyEvent:
e := ShapeNotifyEvent{&event}
runCallbacks(xu, e, ShapeNotify, e.AffectedWindow)
default:
if event != nil {
xgbutil.Logger.Printf("ERROR: UNSUPPORTED EVENT TYPE: %T",
event)
}
}
END:
if pingBefore != nil && pingAfter != nil {
pingAfter <- struct{}{}
}
}
}

@ -0,0 +1,336 @@
package xevent
/*
Defines event types and their associated methods automatically.
This file is automatically generated using `scripts/write-events evtypes`.
Edit it at your peril.
*/
import (
"fmt"
"github.com/BurntSushi/xgb/shape"
"github.com/BurntSushi/xgb/xproto"
)
type KeyPressEvent struct {
*xproto.KeyPressEvent
}
const KeyPress = xproto.KeyPress
func (ev KeyPressEvent) String() string {
return fmt.Sprintf("%v", ev.KeyPressEvent)
}
type KeyReleaseEvent struct {
*xproto.KeyReleaseEvent
}
const KeyRelease = xproto.KeyRelease
func (ev KeyReleaseEvent) String() string {
return fmt.Sprintf("%v", ev.KeyReleaseEvent)
}
type ButtonPressEvent struct {
*xproto.ButtonPressEvent
}
const ButtonPress = xproto.ButtonPress
func (ev ButtonPressEvent) String() string {
return fmt.Sprintf("%v", ev.ButtonPressEvent)
}
type ButtonReleaseEvent struct {
*xproto.ButtonReleaseEvent
}
const ButtonRelease = xproto.ButtonRelease
func (ev ButtonReleaseEvent) String() string {
return fmt.Sprintf("%v", ev.ButtonReleaseEvent)
}
type MotionNotifyEvent struct {
*xproto.MotionNotifyEvent
}
const MotionNotify = xproto.MotionNotify
func (ev MotionNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.MotionNotifyEvent)
}
type EnterNotifyEvent struct {
*xproto.EnterNotifyEvent
}
const EnterNotify = xproto.EnterNotify
func (ev EnterNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.EnterNotifyEvent)
}
type LeaveNotifyEvent struct {
*xproto.LeaveNotifyEvent
}
const LeaveNotify = xproto.LeaveNotify
func (ev LeaveNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.LeaveNotifyEvent)
}
type FocusInEvent struct {
*xproto.FocusInEvent
}
const FocusIn = xproto.FocusIn
func (ev FocusInEvent) String() string {
return fmt.Sprintf("%v", ev.FocusInEvent)
}
type FocusOutEvent struct {
*xproto.FocusOutEvent
}
const FocusOut = xproto.FocusOut
func (ev FocusOutEvent) String() string {
return fmt.Sprintf("%v", ev.FocusOutEvent)
}
type KeymapNotifyEvent struct {
*xproto.KeymapNotifyEvent
}
const KeymapNotify = xproto.KeymapNotify
func (ev KeymapNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.KeymapNotifyEvent)
}
type ExposeEvent struct {
*xproto.ExposeEvent
}
const Expose = xproto.Expose
func (ev ExposeEvent) String() string {
return fmt.Sprintf("%v", ev.ExposeEvent)
}
type GraphicsExposureEvent struct {
*xproto.GraphicsExposureEvent
}
const GraphicsExposure = xproto.GraphicsExposure
func (ev GraphicsExposureEvent) String() string {
return fmt.Sprintf("%v", ev.GraphicsExposureEvent)
}
type NoExposureEvent struct {
*xproto.NoExposureEvent
}
const NoExposure = xproto.NoExposure
func (ev NoExposureEvent) String() string {
return fmt.Sprintf("%v", ev.NoExposureEvent)
}
type VisibilityNotifyEvent struct {
*xproto.VisibilityNotifyEvent
}
const VisibilityNotify = xproto.VisibilityNotify
func (ev VisibilityNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.VisibilityNotifyEvent)
}
type CreateNotifyEvent struct {
*xproto.CreateNotifyEvent
}
const CreateNotify = xproto.CreateNotify
func (ev CreateNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.CreateNotifyEvent)
}
type DestroyNotifyEvent struct {
*xproto.DestroyNotifyEvent
}
const DestroyNotify = xproto.DestroyNotify
func (ev DestroyNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.DestroyNotifyEvent)
}
type UnmapNotifyEvent struct {
*xproto.UnmapNotifyEvent
}
const UnmapNotify = xproto.UnmapNotify
func (ev UnmapNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.UnmapNotifyEvent)
}
type MapNotifyEvent struct {
*xproto.MapNotifyEvent
}
const MapNotify = xproto.MapNotify
func (ev MapNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.MapNotifyEvent)
}
type MapRequestEvent struct {
*xproto.MapRequestEvent
}
const MapRequest = xproto.MapRequest
func (ev MapRequestEvent) String() string {
return fmt.Sprintf("%v", ev.MapRequestEvent)
}
type ReparentNotifyEvent struct {
*xproto.ReparentNotifyEvent
}
const ReparentNotify = xproto.ReparentNotify
func (ev ReparentNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.ReparentNotifyEvent)
}
type ConfigureRequestEvent struct {
*xproto.ConfigureRequestEvent
}
const ConfigureRequest = xproto.ConfigureRequest
func (ev ConfigureRequestEvent) String() string {
return fmt.Sprintf("%v", ev.ConfigureRequestEvent)
}
type GravityNotifyEvent struct {
*xproto.GravityNotifyEvent
}
const GravityNotify = xproto.GravityNotify
func (ev GravityNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.GravityNotifyEvent)
}
type ResizeRequestEvent struct {
*xproto.ResizeRequestEvent
}
const ResizeRequest = xproto.ResizeRequest
func (ev ResizeRequestEvent) String() string {
return fmt.Sprintf("%v", ev.ResizeRequestEvent)
}
type CirculateNotifyEvent struct {
*xproto.CirculateNotifyEvent
}
const CirculateNotify = xproto.CirculateNotify
func (ev CirculateNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.CirculateNotifyEvent)
}
type CirculateRequestEvent struct {
*xproto.CirculateRequestEvent
}
const CirculateRequest = xproto.CirculateRequest
func (ev CirculateRequestEvent) String() string {
return fmt.Sprintf("%v", ev.CirculateRequestEvent)
}
type PropertyNotifyEvent struct {
*xproto.PropertyNotifyEvent
}
const PropertyNotify = xproto.PropertyNotify
func (ev PropertyNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.PropertyNotifyEvent)
}
type SelectionClearEvent struct {
*xproto.SelectionClearEvent
}
const SelectionClear = xproto.SelectionClear
func (ev SelectionClearEvent) String() string {
return fmt.Sprintf("%v", ev.SelectionClearEvent)
}
type SelectionRequestEvent struct {
*xproto.SelectionRequestEvent
}
const SelectionRequest = xproto.SelectionRequest
func (ev SelectionRequestEvent) String() string {
return fmt.Sprintf("%v", ev.SelectionRequestEvent)
}
type SelectionNotifyEvent struct {
*xproto.SelectionNotifyEvent
}
const SelectionNotify = xproto.SelectionNotify
func (ev SelectionNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.SelectionNotifyEvent)
}
type ColormapNotifyEvent struct {
*xproto.ColormapNotifyEvent
}
const ColormapNotify = xproto.ColormapNotify
func (ev ColormapNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.ColormapNotifyEvent)
}
type MappingNotifyEvent struct {
*xproto.MappingNotifyEvent
}
const MappingNotify = xproto.MappingNotify
func (ev MappingNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.MappingNotifyEvent)
}
type ShapeNotifyEvent struct {
*shape.NotifyEvent
}
const ShapeNotify = shape.Notify
func (ev ShapeNotifyEvent) String() string {
return fmt.Sprintf("%v", ev.NotifyEvent)
}

@ -0,0 +1,90 @@
package xevent
import (
"fmt"
"github.com/BurntSushi/xgb/xproto"
)
// ClientMessageEvent embeds the struct by the same name from the xgb library.
type ClientMessageEvent struct {
*xproto.ClientMessageEvent
}
const ClientMessage = xproto.ClientMessage
// NewClientMessage takes all arguments required to build a ClientMessageEvent
// struct and hides the messy details.
// The variadic parameters coincide with the "data" part of a client message.
// The type of the variadic parameters depends upon the value of Format.
// If Format is 8, 'data' should have type byte.
// If Format is 16, 'data' should have type int16.
// If Format is 32, 'data' should have type int.
// Any other value of Format returns an error.
func NewClientMessage(Format byte, Window xproto.Window, Type xproto.Atom,
data ...interface{}) (*ClientMessageEvent, error) {
// Create the client data list first
var clientData xproto.ClientMessageDataUnion
// Don't support formats 8 or 16 yet. They aren't used in EWMH anyway.
switch Format {
case 8:
buf := make([]byte, 20)
for i := 0; i < 20; i++ {
if i >= len(data) {
break
}
buf[i] = data[i].(byte)
}
clientData = xproto.ClientMessageDataUnionData8New(buf)
case 16:
buf := make([]uint16, 10)
for i := 0; i < 10; i++ {
if i >= len(data) {
break
}
buf[i] = uint16(data[i].(int16))
}
clientData = xproto.ClientMessageDataUnionData16New(buf)
case 32:
buf := make([]uint32, 5)
for i := 0; i < 5; i++ {
if i >= len(data) {
break
}
buf[i] = uint32(data[i].(int))
}
clientData = xproto.ClientMessageDataUnionData32New(buf)
default:
return nil, fmt.Errorf("NewClientMessage: Unsupported format '%d'.",
Format)
}
return &ClientMessageEvent{&xproto.ClientMessageEvent{
Format: Format,
Window: Window,
Type: Type,
Data: clientData,
}}, nil
}
// ConfigureNotifyEvent embeds the struct by the same name in XGB.
type ConfigureNotifyEvent struct {
*xproto.ConfigureNotifyEvent
}
const ConfigureNotify = xproto.ConfigureNotify
// NewConfigureNotify takes all arguments required to build a
// ConfigureNotifyEvent struct and returns a ConfigureNotifyEvent value.
func NewConfigureNotify(Event, Window, AboveSibling xproto.Window,
X, Y, Width, Height int, BorderWidth uint16,
OverrideRedirect bool) *ConfigureNotifyEvent {
return &ConfigureNotifyEvent{&xproto.ConfigureNotifyEvent{
Event: Event, Window: Window, AboveSibling: AboveSibling,
X: int16(X), Y: int16(Y), Width: uint16(Width), Height: uint16(Height),
BorderWidth: BorderWidth, OverrideRedirect: OverrideRedirect,
}}
}

@ -0,0 +1,237 @@
package xevent
import (
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
)
// Sometimes we need to specify NO WINDOW when a window is typically
// expected. (Like connecting to MappingNotify or KeymapNotify events.)
// Use this value to do that.
var NoWindow xproto.Window = 0
// IgnoreMods is a list of X modifiers that we don't want interfering
// with our mouse or key bindings. In particular, for each mouse or key binding
// issued, there is a seperate mouse or key binding made for each of the
// modifiers specified.
//
// You may modify this slice to add (or remove) modifiers, but it should be
// done before *any* key or mouse bindings are attached with the keybind and
// mousebind packages. It should not be modified afterwards.
//
// TODO: We're assuming numlock is in the 'mod2' modifier, which is a pretty
// common setup, but by no means guaranteed. This should be modified to actually
// inspect the modifiers table and look for the special Num_Lock keysym.
var IgnoreMods []uint16 = []uint16{
0,
xproto.ModMaskLock, // Caps lock
xproto.ModMask2, // Num lock
xproto.ModMaskLock | xproto.ModMask2, // Caps and Num lock
}
// Enqueue queues up an event read from X.
// Note that an event read may return an error, in which case, this queue
// entry will be an error and not an event.
//
// ev, err := XUtilValue.Conn().WaitForEvent()
// xevent.Enqueue(XUtilValue, ev, err)
//
// You probably shouldn't have to enqueue events yourself. This is done
// automatically if you're using xevent.Main{Ping} and/or xevent.Read.
func Enqueue(xu *xgbutil.XUtil, ev xgb.Event, err xgb.Error) {
xu.EvqueueLck.Lock()
defer xu.EvqueueLck.Unlock()
xu.Evqueue = append(xu.Evqueue, xgbutil.EventOrError{
Event: ev,
Err: err,
})
}
// Dequeue pops an event/error from the queue and returns it.
// The queue item is unwrapped and returned as multiple return values.
// Only one of the return values can be nil.
func Dequeue(xu *xgbutil.XUtil) (xgb.Event, xgb.Error) {
xu.EvqueueLck.Lock()
defer xu.EvqueueLck.Unlock()
everr := xu.Evqueue[0]
xu.Evqueue = xu.Evqueue[1:]
return everr.Event, everr.Err
}
// DequeueAt removes a particular item from the queue.
// This is primarily useful when attempting to compress events.
func DequeueAt(xu *xgbutil.XUtil, i int) {
xu.EvqueueLck.Lock()
defer xu.EvqueueLck.Unlock()
xu.Evqueue = append(xu.Evqueue[:i], xu.Evqueue[i+1:]...)
}
// Empty returns whether the event queue is empty or not.
func Empty(xu *xgbutil.XUtil) bool {
xu.EvqueueLck.RLock()
defer xu.EvqueueLck.RUnlock()
return len(xu.Evqueue) == 0
}
// Peek returns a *copy* of the current queue so we can examine it.
// This can be useful when trying to determine if a particular kind of
// event will be processed in the future.
func Peek(xu *xgbutil.XUtil) []xgbutil.EventOrError {
xu.EvqueueLck.RLock()
defer xu.EvqueueLck.RUnlock()
cpy := make([]xgbutil.EventOrError, len(xu.Evqueue))
copy(cpy, xu.Evqueue)
return cpy
}
// ErrorHandlerSet sets the default error handler for errors that come
// into the main event loop. (This may be removed in the future in favor
// of a particular callback interface like events, but these sorts of errors
// aren't handled often in practice, so maybe not.)
// This is only called for errors returned from unchecked (asynchronous error
// handling) requests.
// The default error handler just emits them to stderr.
func ErrorHandlerSet(xu *xgbutil.XUtil, fun xgbutil.ErrorHandlerFun) {
xu.ErrorHandler = fun
}
// ErrorHandlerGet retrieves the default error handler.
func ErrorHandlerGet(xu *xgbutil.XUtil) xgbutil.ErrorHandlerFun {
return xu.ErrorHandler
}
type HookFun func(xu *xgbutil.XUtil, event interface{}) bool
func (callback HookFun) Connect(xu *xgbutil.XUtil) {
xu.HooksLck.Lock()
defer xu.HooksLck.Unlock()
// COW
newHooks := make([]xgbutil.CallbackHook, len(xu.Hooks))
copy(newHooks, xu.Hooks)
newHooks = append(newHooks, callback)
xu.Hooks = newHooks
}
func (callback HookFun) Run(xu *xgbutil.XUtil, event interface{}) bool {
return callback(xu, event)
}
func getHooks(xu *xgbutil.XUtil) []xgbutil.CallbackHook {
xu.HooksLck.RLock()
defer xu.HooksLck.RUnlock()
return xu.Hooks
}
// RedirectKeyEvents, when set to a window id (greater than 0), will force
// *all* Key{Press,Release} to callbacks attached to the specified window.
// This is close to emulating a Keyboard grab without the racing.
// To stop redirecting key events, use window identifier '0'.
func RedirectKeyEvents(xu *xgbutil.XUtil, wid xproto.Window) {
xu.KeyRedirect = wid
}
// RedirectKeyGet gets the window that key events are being redirected to.
// If 0, then no redirection occurs.
func RedirectKeyGet(xu *xgbutil.XUtil) xproto.Window {
return xu.KeyRedirect
}
// Quit elegantly exits out of the main event loop.
// "Elegantly" in this case means that it finishes processing the current
// event, and breaks out of the loop afterwards.
// There is no particular reason to use this instead of something like os.Exit
// other than you might have code to run after the main event loop exits to
// "clean up."
func Quit(xu *xgbutil.XUtil) {
xu.Quit = true
}
// Quitting returns whether it's time to quit.
// This is only used in the main event loop in xevent.
func Quitting(xu *xgbutil.XUtil) bool {
return xu.Quit
}
// attachCallback associates a (event, window) tuple with an event.
// Use copy on write since we run callbacks *a lot* more than attaching them.
// (The copy on write only applies to the slice of callbacks rather than
// the map itself, since the initial allocation is guaranteed to come before
// any use of it.)
func attachCallback(xu *xgbutil.XUtil, evtype int, win xproto.Window,
fun xgbutil.Callback) {
xu.CallbacksLck.Lock()
defer xu.CallbacksLck.Unlock()
if _, ok := xu.Callbacks[evtype]; !ok {
xu.Callbacks[evtype] = make(map[xproto.Window][]xgbutil.Callback, 20)
}
if _, ok := xu.Callbacks[evtype][win]; !ok {
xu.Callbacks[evtype][win] = make([]xgbutil.Callback, 0)
}
// COW
newCallbacks := make([]xgbutil.Callback, len(xu.Callbacks[evtype][win]))
copy(newCallbacks, xu.Callbacks[evtype][win])
newCallbacks = append(newCallbacks, fun)
xu.Callbacks[evtype][win] = newCallbacks
}
// runCallbacks executes every callback corresponding to a
// particular event/window tuple.
func runCallbacks(xu *xgbutil.XUtil, event interface{}, evtype int,
win xproto.Window) {
// The callback slice for a particular (event type, window) tuple uses
// copy on write. So just take a pointer to whatever is there and use that.
// We can be sure that the slice won't change from underneathe us.
xu.CallbacksLck.RLock()
cbs := xu.Callbacks[evtype][win]
xu.CallbacksLck.RUnlock()
for _, cb := range cbs {
cb.Run(xu, event)
}
}
// Detach removes all callbacks associated with a particular window.
// Note that if you're also using the keybind and mousebind packages, a complete
// detachment should look like:
//
// keybind.Detach(XUtilValue, window-id)
// mousebind.Detach(XUtilValue, window-id)
// xevent.Detach(XUtilValue, window-id)
//
// If a window is no longer receiving events, these methods should be called.
// Otherwise, the memory used to store the handler info for that window will
// never be released.
func Detach(xu *xgbutil.XUtil, win xproto.Window) {
xu.CallbacksLck.Lock()
defer xu.CallbacksLck.Unlock()
for evtype, _ := range xu.Callbacks {
delete(xu.Callbacks[evtype], win)
}
}
// SendRootEvent takes a type implementing the xgb.Event interface, converts it
// to raw X bytes, and sends it to the root window using the SendEvent request.
func SendRootEvent(xu *xgbutil.XUtil, ev xgb.Event, evMask uint32) error {
return xproto.SendEventChecked(xu.Conn(), false, xu.RootWin(), evMask,
string(ev.Bytes())).Check()
}
// ReplayPointer is a quick alias to AllowEvents with 'ReplayPointer' mode.
func ReplayPointer(xu *xgbutil.XUtil) {
xproto.AllowEvents(xu.Conn(), xproto.AllowReplayPointer, 0)
}

@ -0,0 +1,348 @@
package xgbutil
import (
"log"
"os"
"sync"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xinerama"
"github.com/BurntSushi/xgb/xproto"
)
// Logger is used through xgbutil when messages need to be emitted to stderr.
var Logger = log.New(os.Stderr, "[xgbutil] ", log.Lshortfile)
// The current maximum request size. I think we can expand this with
// BigReq, but it probably isn't worth it at the moment.
const MaxReqSize = (1 << 16) * 4
// An XUtil represents the state of xgbutil. It keeps track of the current
// X connection, the root window, event callbacks, key/mouse bindings, etc.
// Regrettably, many of the members are exported, even though they should not
// be used directly by the user. They are exported for use in sub-packages.
// (Namely, xevent, keybind and mousebind.) In fact, there should *never*
// be a reason to access any members of an XUtil value directly. Any
// interaction with an XUtil value should be through its methods.
type XUtil struct {
// conn is the XGB connection object used to issue protocol requests.
conn *xgb.Conn
// Quit can be set to true, and the main event loop will finish processing
// the current event, and gracefully quit afterwards.
// This is exported for use in the xevent package. Please us xevent.Quit
// to set this value.
Quit bool // when true, the main event loop will stop gracefully
// setup contains all the setup information retrieved at connection time.
setup *xproto.SetupInfo
// screen is a simple alias to the default screen info.
screen *xproto.ScreenInfo
// root is an alias to the default root window.
root xproto.Window
// Atoms is a cache of atom names to resource identifiers. This minimizes
// round trips to the X server, since atom identifiers never change.
// It is exported for use in the xprop package. It should not be used.
Atoms map[string]xproto.Atom
AtomsLck *sync.RWMutex
// AtomNames is a cache just like 'atoms', but in the reverse direction.
// It is exported for use in the xprop package. It should not be used.
AtomNames map[xproto.Atom]string
AtomNamesLck *sync.RWMutex
// Evqueue is the queue that stores the results of xgb.WaitForEvent.
// Namely, each value is either an Event *or* an Error.
// It is exported for use in the xevent package. Do not use it.
// If you need to interact with the event queue, please use the functions
// available in the xevent package: Dequeue, DequeueAt, QueueEmpty
// and QueuePeek.
Evqueue []EventOrError
EvqueueLck *sync.RWMutex
// Callbacks is a map of event numbers to a map of window identifiers
// to callback functions.
// This is the data structure that stores all callback functions, where
// a callback function is always attached to a (event, window) tuple.
// It is exported for use in the xevent package. Do not use it.
Callbacks map[int]map[xproto.Window][]Callback
CallbacksLck *sync.RWMutex
// Hooks are called by the XEvent main loop before processing the event
// itself. These are meant for instances when it's not possible / easy
// to use the normal Hook system. You should not modify this yourself.
Hooks []CallbackHook
HooksLck *sync.RWMutex
// eventTime is the last time recorded by an event. It is automatically
// updated if xgbutil's main event loop is used.
eventTime xproto.Timestamp
// Keymap corresponds to xgbutil's current conception of the keyboard
// mapping. It is automatically kept up-to-date if xgbutil's event loop
// is used.
// It is exported for use in the keybind package. It should not be
// accessed directly. Instead, use keybind.KeyMapGet.
Keymap *KeyboardMapping
// Modmap corresponds to xgbutil's current conception of the modifier key
// mapping. It is automatically kept up-to-date if xgbutil's event loop
// is used.
// It is exported for use in the keybind package. It should not be
// accessed directly. Instead, use keybind.ModMapGet.
Modmap *ModifierMapping
// KeyRedirect corresponds to a window identifier that, when set,
// automatically receives *all* keyboard events. This is a sort-of
// synthetic grab and is helpful in avoiding race conditions.
// It is exported for use in the xevent and keybind packages. Do not use
// it directly. To redirect key events, please use xevent.RedirectKeyEvents.
KeyRedirect xproto.Window
// Keybinds is the data structure storing all callbacks for key bindings.
// This is extremely similar to the general notion of event callbacks,
// but adds extra support to make handling key bindings easier. (Like
// specifying human readable key sequences to bind to.)
// KeyBindKey is a struct representing the 4-tuple
// (event-type, window-id, modifiers, keycode).
// It is exported for use in the keybind package. Do not access it directly.
Keybinds map[KeyKey][]CallbackKey
KeybindsLck *sync.RWMutex
// Keygrabs is a frequency count of the number of callbacks associated
// with a particular KeyBindKey. This is necessary because we can only
// grab a particular key *once*, but we may want to attach several callbacks
// to a single keypress.
// It is exported for use in the keybind package. Do not access it directly.
Keygrabs map[KeyKey]int
// Keystrings is a list of all key strings used to connect keybindings.
// They are used to rebuild key grabs when the keyboard mapping is updated.
// It is exported for use in the keybind package. Do not access it directly.
Keystrings []KeyString
// Mousebinds is the data structure storing all callbacks for mouse
// bindings.This is extremely similar to the general notion of event
// callbacks,but adds extra support to make handling mouse bindings easier.
// (Like specifying human readable mouse sequences to bind to.)
// MouseBindKey is a struct representing the 4-tuple
// (event-type, window-id, modifiers, button).
// It is exported for use in the mousebind package. Do not use it.
Mousebinds map[MouseKey][]CallbackMouse
MousebindsLck *sync.RWMutex
// Mousegrabs is a frequency count of the number of callbacks associated
// with a particular MouseBindKey. This is necessary because we can only
// grab a particular mouse button *once*, but we may want to attach
// several callbacks to a single button press.
// It is exported for use in the mousebind package. Do not use it.
Mousegrabs map[MouseKey]int
// InMouseDrag is true if a drag is currently in progress.
// It is exported for use in the mousebind package. Do not use it.
InMouseDrag bool
// MouseDragStep is the function executed for each step (i.e., pointer
// movement) in the current mouse drag. Note that this is nil when a drag
// is not in progress.
// It is exported for use in the mousebind package. Do not use it.
MouseDragStepFun MouseDragFun
// MouseDragEnd is the function executed at the end of the current
// mouse drag. This is nil when a drag is not in progress.
// It is exported for use in the mousebind package. Do not use it.
MouseDragEndFun MouseDragFun
// gc is a general purpose graphics context; used to paint images.
// Since we don't do any real X drawing, we don't really care about the
// particulars of our graphics context.
gc xproto.Gcontext
// dummy is a dummy window used for mouse/key GRABs.
// Basically, whenever a grab is instituted, mouse and key events are
// redirected to the dummy the window.
dummy xproto.Window
// ErrorHandler is the function that handles errors *in the event loop*.
// By default, it simply emits them to stderr.
// It is exported for use in the xevent package. To set the default error
// handler, please use xevent.ErrorHandlerSet.
ErrorHandler ErrorHandlerFun
}
// NewConn connects to the X server using the DISPLAY environment variable
// and creates a new XUtil. Most environments have the DISPLAY environment
// variable set, so this is probably what you want to use to connect to X.
func NewConn() (*XUtil, error) {
return NewConnDisplay("")
}
// NewConnDisplay connects to the X server and creates a new XUtil.
// If 'display' is empty, the DISPLAY environment variable is used. Otherwise
// there are several different display formats supported:
//
// NewConn(":1") -> net.Dial("unix", "", "/tmp/.X11-unix/X1")
// NewConn("/tmp/launch-12/:0") -> net.Dial("unix", "", "/tmp/launch-12/:0")
// NewConn("hostname:2.1") -> net.Dial("tcp", "", "hostname:6002")
// NewConn("tcp/hostname:1.0") -> net.Dial("tcp", "", "hostname:6001")
func NewConnDisplay(display string) (*XUtil, error) {
c, err := xgb.NewConnDisplay(display)
if err != nil {
return nil, err
}
return NewConnXgb(c)
}
// NewConnXgb use the specific xgb.Conn to create a new XUtil.
//
// NewConn, NewConnDisplay are wrapper of this function.
func NewConnXgb(c *xgb.Conn) (*XUtil, error) {
setup := xproto.Setup(c)
screen := setup.DefaultScreen(c)
// Initialize our central struct that stores everything.
xu := &XUtil{
conn: c,
Quit: false,
Evqueue: make([]EventOrError, 0, 1000),
EvqueueLck: &sync.RWMutex{},
setup: setup,
screen: screen,
root: screen.Root,
eventTime: xproto.Timestamp(0), // last event time
Atoms: make(map[string]xproto.Atom, 50),
AtomsLck: &sync.RWMutex{},
AtomNames: make(map[xproto.Atom]string, 50),
AtomNamesLck: &sync.RWMutex{},
Callbacks: make(map[int]map[xproto.Window][]Callback, 33),
CallbacksLck: &sync.RWMutex{},
Hooks: make([]CallbackHook, 0),
HooksLck: &sync.RWMutex{},
Keymap: nil, // we don't have anything yet
Modmap: nil,
KeyRedirect: 0,
Keybinds: make(map[KeyKey][]CallbackKey, 10),
KeybindsLck: &sync.RWMutex{},
Keygrabs: make(map[KeyKey]int, 10),
Keystrings: make([]KeyString, 0, 10),
Mousebinds: make(map[MouseKey][]CallbackMouse, 10),
MousebindsLck: &sync.RWMutex{},
Mousegrabs: make(map[MouseKey]int, 10),
InMouseDrag: false,
MouseDragStepFun: nil,
MouseDragEndFun: nil,
ErrorHandler: func(err xgb.Error) { Logger.Println(err) },
}
var err error = nil
// Create a general purpose graphics context
xu.gc, err = xproto.NewGcontextId(xu.conn)
if err != nil {
return nil, err
}
xproto.CreateGC(xu.conn, xu.gc, xproto.Drawable(xu.root),
xproto.GcForeground, []uint32{xu.screen.WhitePixel})
// Create a dummy window
xu.dummy, err = xproto.NewWindowId(xu.conn)
if err != nil {
return nil, err
}
xproto.CreateWindow(xu.conn, xu.Screen().RootDepth, xu.dummy, xu.RootWin(),
-1000, -1000, 1, 1, 0,
xproto.WindowClassInputOutput, xu.Screen().RootVisual,
xproto.CwEventMask|xproto.CwOverrideRedirect,
[]uint32{1, xproto.EventMaskPropertyChange})
xproto.MapWindow(xu.conn, xu.dummy)
// Register the Xinerama extension... because it doesn't cost much.
err = xinerama.Init(xu.conn)
// If we can't register Xinerama, that's okay. Output something
// and move on.
if err != nil {
Logger.Printf("WARNING: %s\n", err)
Logger.Printf("MESSAGE: The 'xinerama' package cannot be used " +
"because the XINERAMA extension could not be loaded.")
}
return xu, nil
}
// Conn returns the xgb connection object.
func (xu *XUtil) Conn() *xgb.Conn {
return xu.conn
}
// ExtInitialized returns true if an extension has been initialized.
// This is useful for determining whether an extension is available or not.
func (xu *XUtil) ExtInitialized(extName string) bool {
_, ok := xu.Conn().Extensions[extName]
return ok
}
// Sync forces XGB to catch up with all events/requests and synchronize.
// This is done by issuing a benign round trip request to X.
func (xu *XUtil) Sync() {
xproto.GetInputFocus(xu.Conn()).Reply()
}
// Setup returns the setup information retrieved during connection time.
func (xu *XUtil) Setup() *xproto.SetupInfo {
return xu.setup
}
// Screen returns the default screen
func (xu *XUtil) Screen() *xproto.ScreenInfo {
return xu.screen
}
// RootWin returns the current root window.
func (xu *XUtil) RootWin() xproto.Window {
return xu.root
}
// RootWinSet will change the current root window to the one provided.
// N.B. This probably shouldn't be used unless you're desperately trying
// to support multiple X screens. (This is *not* the same as Xinerama/RandR or
// TwinView. All of those have a single root window.)
func (xu *XUtil) RootWinSet(root xproto.Window) {
xu.root = root
}
// TimeGet gets the most recent time seen by an event.
func (xu *XUtil) TimeGet() xproto.Timestamp {
return xu.eventTime
}
// TimeSet sets the most recent time seen by an event.
func (xu *XUtil) TimeSet(t xproto.Timestamp) {
xu.eventTime = t
}
// GC gets a general purpose graphics context that is typically used to simply
// paint images.
func (xu *XUtil) GC() xproto.Gcontext {
return xu.gc
}
// Dummy gets the id of the dummy window.
func (xu *XUtil) Dummy() xproto.Window {
return xu.dummy
}
// Grabs the server. Everything becomes synchronous.
func (xu *XUtil) Grab() {
xproto.GrabServer(xu.Conn())
}
// Ungrabs the server.
func (xu *XUtil) Ungrab() {
xproto.UngrabServer(xu.Conn())
}

@ -0,0 +1,103 @@
package xprop
/*
xprop/atom.go contains functions related to interning atoms and retrieving
atom names from an atom identifier.
It also manages an atom cache so that once an atom is interned from the X
server, all future atom interns use that value. (So that one and only one
request is sent for interning each atom.)
*/
import (
"fmt"
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
)
// Atm is a short alias for Atom in the common case of interning an atom.
// Namely, interning the atom always succeeds. (If the atom does not already
// exist, a new one is created.)
func Atm(xu *xgbutil.XUtil, name string) (xproto.Atom, error) {
aid, err := Atom(xu, name, false)
if err != nil {
return 0, err
}
if aid == 0 {
return 0, fmt.Errorf("Atm: '%s' returned an identifier of 0.", name)
}
return aid, err
}
// Atom interns an atom and panics if there is any error.
func Atom(xu *xgbutil.XUtil, name string,
onlyIfExists bool) (xproto.Atom, error) {
// Check the cache first
if aid, ok := atomGet(xu, name); ok {
return aid, nil
}
reply, err := xproto.InternAtom(xu.Conn(), onlyIfExists,
uint16(len(name)), name).Reply()
if err != nil {
return 0, fmt.Errorf("Atom: Error interning atom '%s': %s", name, err)
}
// If we're here, it means we didn't have this atom cached. So cache it!
cacheAtom(xu, name, reply.Atom)
return reply.Atom, nil
}
// AtomName fetches a string representation of an ATOM given its integer id.
func AtomName(xu *xgbutil.XUtil, aid xproto.Atom) (string, error) {
// Check the cache first
if atomName, ok := atomNameGet(xu, aid); ok {
return string(atomName), nil
}
reply, err := xproto.GetAtomName(xu.Conn(), aid).Reply()
if err != nil {
return "", fmt.Errorf("AtomName: Error fetching name for ATOM "+
"id '%d': %s", aid, err)
}
// If we're here, it means we didn't have ths ATOM id cached. So cache it.
atomName := string(reply.Name)
cacheAtom(xu, atomName, aid)
return atomName, nil
}
// atomGet retrieves an atom identifier from a cache if it exists.
func atomGet(xu *xgbutil.XUtil, name string) (xproto.Atom, bool) {
xu.AtomsLck.RLock()
defer xu.AtomsLck.RUnlock()
aid, ok := xu.Atoms[name]
return aid, ok
}
// atomNameGet retrieves an atom name from a cache if it exists.
func atomNameGet(xu *xgbutil.XUtil, aid xproto.Atom) (string, bool) {
xu.AtomNamesLck.RLock()
defer xu.AtomNamesLck.RUnlock()
name, ok := xu.AtomNames[aid]
return name, ok
}
// cacheAtom puts an atom into the cache.
func cacheAtom(xu *xgbutil.XUtil, name string, aid xproto.Atom) {
xu.AtomsLck.Lock()
xu.AtomNamesLck.Lock()
defer xu.AtomsLck.Unlock()
defer xu.AtomNamesLck.Unlock()
xu.Atoms[name] = aid
xu.AtomNames[aid] = name
}

@ -0,0 +1,40 @@
/*
Package xprop provides a cache for interning atoms and helper functions for
dealing with GetProperty and ChangeProperty X requests.
Atoms
Atoms in X are interned, meaning that strings are assigned unique integer
identifiers. This minimizes the amount of data transmitted over an X connection.
Once atoms have been interned, they are never changed while the X server is
running. xgbutil takes advantage of this invariant and will only issue an
intern atom request once and cache the result.
To use the xprop package to intern an atom, use Atom:
atom, err := xprop.Atom(XUtilValue, "THE_ATOM_NAME", false)
if err == nil {
println("The atom number: ", atom.Atom)
}
The 'false' parameter corresponds to the 'only_if_exists' parameter of the
X InternAtom request. When it's false, the atom being interned always returns
a non-zero atom number---even if the string being interned hasn't been interned
before. If 'only_if_exists' is true, the atom being interned will return a 0
atom number if it hasn't already been interned.
The typical case is to set 'only_if_exists' to false. To this end, xprop.Atm is
an alias that always sets this value to false.
The reverse can also be done: getting an atom string if you have an atom
number. This can be done with the xprop.AtomName function.
Properties
The other facility of xprop is to help with the use of GetProperty and
ChangeProperty. Please see the source code of the ewmh package for plenty of
examples.
*/
package xprop

@ -0,0 +1,270 @@
package xprop
import (
"fmt"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
)
// GetProperty abstracts the messiness of calling xgb.GetProperty.
func GetProperty(xu *xgbutil.XUtil, win xproto.Window, atom string) (
*xproto.GetPropertyReply, error) {
atomId, err := Atm(xu, atom)
if err != nil {
return nil, err
}
reply, err := xproto.GetProperty(xu.Conn(), false, win, atomId,
xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
if err != nil {
return nil, fmt.Errorf("GetProperty: Error retrieving property '%s' "+
"on window %x: %s", atom, win, err)
}
if reply.Format == 0 {
return nil, fmt.Errorf("GetProperty: No such property '%s' on "+
"window %x.", atom, win)
}
return reply, nil
}
// ChangeProperty abstracts the semi-nastiness of xgb.ChangeProperty.
func ChangeProp(xu *xgbutil.XUtil, win xproto.Window, format byte, prop string,
typ string, data []byte) error {
propAtom, err := Atm(xu, prop)
if err != nil {
return err
}
typAtom, err := Atm(xu, typ)
if err != nil {
return err
}
return xproto.ChangePropertyChecked(xu.Conn(), xproto.PropModeReplace, win,
propAtom, typAtom, format,
uint32(len(data)/(int(format)/8)), data).Check()
}
// ChangeProperty32 makes changing 32 bit formatted properties easier
// by constructing the raw X data for you.
func ChangeProp32(xu *xgbutil.XUtil, win xproto.Window, prop string, typ string,
data ...uint) error {
buf := make([]byte, len(data)*4)
for i, datum := range data {
xgb.Put32(buf[(i*4):], uint32(datum))
}
return ChangeProp(xu, win, 32, prop, typ, buf)
}
// WindowToUint is a covenience function for converting []xproto.Window
// to []uint.
func WindowToInt(ids []xproto.Window) []uint {
ids32 := make([]uint, len(ids))
for i, v := range ids {
ids32[i] = uint(v)
}
return ids32
}
// AtomToInt is a covenience function for converting []xproto.Atom
// to []uint.
func AtomToUint(ids []xproto.Atom) []uint {
ids32 := make([]uint, len(ids))
for i, v := range ids {
ids32[i] = uint(v)
}
return ids32
}
// StrToAtoms is a convenience function for converting
// []string to []uint32 atoms.
// NOTE: If an atom name in the list doesn't exist, it will be created.
func StrToAtoms(xu *xgbutil.XUtil, atomNames []string) ([]uint, error) {
var err error
atoms := make([]uint, len(atomNames))
for i, atomName := range atomNames {
a, err := Atom(xu, atomName, false)
if err != nil {
return nil, err
}
atoms[i] = uint(a)
}
return atoms, err
}
// PropValAtom transforms a GetPropertyReply struct into an ATOM name.
// The property reply must be in 32 bit format.
func PropValAtom(xu *xgbutil.XUtil, reply *xproto.GetPropertyReply,
err error) (string, error) {
if err != nil {
return "", err
}
if reply.Format != 32 {
return "", fmt.Errorf("PropValAtom: Expected format 32 but got %d",
reply.Format)
}
return AtomName(xu, xproto.Atom(xgb.Get32(reply.Value)))
}
// PropValAtoms is the same as PropValAtom, except that it returns a slice
// of atom names. Also must be 32 bit format.
// This is a method of an XUtil struct, unlike the other 'PropVal...' functions.
func PropValAtoms(xu *xgbutil.XUtil, reply *xproto.GetPropertyReply,
err error) ([]string, error) {
if err != nil {
return nil, err
}
if reply.Format != 32 {
return nil, fmt.Errorf("PropValAtoms: Expected format 32 but got %d",
reply.Format)
}
ids := make([]string, reply.ValueLen)
vals := reply.Value
for i := 0; len(vals) >= 4; i++ {
ids[i], err = AtomName(xu, xproto.Atom(xgb.Get32(vals)))
if err != nil {
return nil, err
}
vals = vals[4:]
}
return ids, nil
}
// PropValWindow transforms a GetPropertyReply struct into an X resource
// window identifier.
// The property reply must be in 32 bit format.
func PropValWindow(reply *xproto.GetPropertyReply,
err error) (xproto.Window, error) {
if err != nil {
return 0, err
}
if reply.Format != 32 {
return 0, fmt.Errorf("PropValId: Expected format 32 but got %d",
reply.Format)
}
return xproto.Window(xgb.Get32(reply.Value)), nil
}
// PropValWindows is the same as PropValWindow, except that it returns a slice
// of identifiers. Also must be 32 bit format.
func PropValWindows(reply *xproto.GetPropertyReply,
err error) ([]xproto.Window, error) {
if err != nil {
return nil, err
}
if reply.Format != 32 {
return nil, fmt.Errorf("PropValIds: Expected format 32 but got %d",
reply.Format)
}
ids := make([]xproto.Window, reply.ValueLen)
vals := reply.Value
for i := 0; len(vals) >= 4; i++ {
ids[i] = xproto.Window(xgb.Get32(vals))
vals = vals[4:]
}
return ids, nil
}
// PropValNum transforms a GetPropertyReply struct into an unsigned
// integer. Useful when the property value is a single integer.
func PropValNum(reply *xproto.GetPropertyReply, err error) (uint, error) {
if err != nil {
return 0, err
}
if reply.Format != 32 {
return 0, fmt.Errorf("PropValNum: Expected format 32 but got %d",
reply.Format)
}
return uint(xgb.Get32(reply.Value)), nil
}
// PropValNums is the same as PropValNum, except that it returns a slice
// of integers. Also must be 32 bit format.
func PropValNums(reply *xproto.GetPropertyReply, err error) ([]uint, error) {
if err != nil {
return nil, err
}
if reply.Format != 32 {
return nil, fmt.Errorf("PropValIds: Expected format 32 but got %d",
reply.Format)
}
nums := make([]uint, reply.ValueLen)
vals := reply.Value
for i := 0; len(vals) >= 4; i++ {
nums[i] = uint(xgb.Get32(vals))
vals = vals[4:]
}
return nums, nil
}
// PropValNum64 transforms a GetPropertyReply struct into a 64 bit
// integer. Useful when the property value is a single integer.
func PropValNum64(reply *xproto.GetPropertyReply, err error) (int64, error) {
if err != nil {
return 0, err
}
if reply.Format != 32 {
return 0, fmt.Errorf("PropValNum: Expected format 32 but got %d",
reply.Format)
}
return int64(xgb.Get32(reply.Value)), nil
}
// PropValStr transforms a GetPropertyReply struct into a string.
// Useful when the property value is a null terminated string represented
// by integers. Also must be 8 bit format.
func PropValStr(reply *xproto.GetPropertyReply, err error) (string, error) {
if err != nil {
return "", err
}
if reply.Format != 8 {
return "", fmt.Errorf("PropValStr: Expected format 8 but got %d",
reply.Format)
}
return string(reply.Value), nil
}
// PropValStrs is the same as PropValStr, except that it returns a slice
// of strings. The raw byte string is a sequence of null terminated strings,
// which is translated into a slice of strings.
func PropValStrs(reply *xproto.GetPropertyReply, err error) ([]string, error) {
if err != nil {
return nil, err
}
if reply.Format != 8 {
return nil, fmt.Errorf("PropValStrs: Expected format 8 but got %d",
reply.Format)
}
var strs []string
sstart := 0
for i, c := range reply.Value {
if c == 0 {
strs = append(strs, string(reply.Value[sstart:i]))
sstart = i + 1
}
}
if sstart < int(reply.ValueLen) {
strs = append(strs, string(reply.Value[sstart:]))
}
return strs, nil
}

@ -41,8 +41,17 @@ github.com/BurntSushi/toml
# github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
github.com/BurntSushi/xgb
github.com/BurntSushi/xgb/render
github.com/BurntSushi/xgb/shape
github.com/BurntSushi/xgb/shm
github.com/BurntSushi/xgb/xinerama
github.com/BurntSushi/xgb/xproto
# github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046
## explicit
github.com/BurntSushi/xgbutil
github.com/BurntSushi/xgbutil/ewmh
github.com/BurntSushi/xgbutil/icccm
github.com/BurntSushi/xgbutil/xevent
github.com/BurntSushi/xgbutil/xprop
# github.com/aarzilli/nucular v0.0.0-20200615134801-81910c722bba
## explicit
github.com/aarzilli/nucular

Loading…
Cancel
Save