Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
i3status-go
|
||||||
|
i3status-go.json
|
||||||
66
.golangci.yml
Normal file
66
.golangci.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
skip-dirs:
|
||||||
|
- data
|
||||||
|
- vendor
|
||||||
|
modules-download-mode: readonly
|
||||||
|
allow-parallel-runners: true
|
||||||
|
go: '1.21'
|
||||||
|
output:
|
||||||
|
sort-results: true
|
||||||
|
linters-settings:
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
# not relevant here
|
||||||
|
- G404 # Insecure random number source (rand)
|
||||||
|
- G306 # Expect WriteFile permissions to be 0600 or less (gosec)
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
- decorder
|
||||||
|
- durationcheck
|
||||||
|
- errchkjson
|
||||||
|
- errorlint
|
||||||
|
- exportloopref
|
||||||
|
- ginkgolinter
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gocritic
|
||||||
|
- godot
|
||||||
|
- goerr113
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- grouper
|
||||||
|
- importas
|
||||||
|
- ireturn
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nonamedreturns
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- stylecheck
|
||||||
|
- unconvert
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wsl
|
||||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Sergei Fedosoff <eleksir@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
Makefile
Normal file
29
Makefile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env gmake -f
|
||||||
|
|
||||||
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
||||||
|
FILELIST=batteries.go clock.go collection.go config.go cputemp.go cron.go i3.go ifmon.go la.go main.go memory.go \
|
||||||
|
pulseaudio.go signal.go spawn.go stdin.go vpn.go
|
||||||
|
|
||||||
|
BINARY=i3status-go
|
||||||
|
|
||||||
|
## Use calssic targets where first one is deafult target
|
||||||
|
all: clean build
|
||||||
|
|
||||||
|
## This target compiles binary
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ${FILELIST}
|
||||||
|
|
||||||
|
|
||||||
|
## Remove binary with golang compiler' means
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
|
||||||
|
|
||||||
|
## Misc target, for development purposes. Updates vendored libs, brutal way.
|
||||||
|
upgrade:
|
||||||
|
$(RM) -r vendor
|
||||||
|
go get -d -u -t ./...
|
||||||
|
go mod tidy
|
||||||
|
go mod vendor
|
||||||
|
|
||||||
|
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
||||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# i3status-go
|
||||||
|
|
||||||
|
It is i3bar text generator.
|
||||||
|
|
||||||
|
## What it can
|
||||||
|
|
||||||
|
* Application launcher buttons
|
||||||
|
* Memory statistics
|
||||||
|
* LA, last 5 minutes
|
||||||
|
* Show battery charge
|
||||||
|
* Show average CPU cores temperature
|
||||||
|
* Network interfaces status
|
||||||
|
* OpenVPN status (including tcp checks)
|
||||||
|
* PulseAudio volume indicator, can adjust master volume too
|
||||||
|
* Clock
|
||||||
|
* Cron jobs (intended to use for show periodic desktop notifications but not limited to)
|
||||||
|
|
||||||
|
## How to build it
|
||||||
|
|
||||||
|
This appliction was tested to compile with go v1.21 but can be built with earlier versions (seems down to 1.18, not
|
||||||
|
sure).
|
||||||
|
All you need is go lang compiler and gnu make utility. Invoke
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
that will produce binary *i3status-go*. You should place it to some dir intended for storing user binaries (~/.local/bin,
|
||||||
|
~/bin, or something else) and edit i3wm config according to
|
||||||
|
[manual](https://i3wm.org/docs/userguide.html#_configuring_i3bar) after editing i3wm config either run **i3status-go** by
|
||||||
|
hands and kill it or copy **i3status-go-example.json** to **$XDG_CONFIG_HOME/i3status-go.json** and adjust it to your
|
||||||
|
habbit.
|
||||||
110
batteries.go
Normal file
110
batteries.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/distatus/battery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Batt shows battery charge on i3bar.
|
||||||
|
var Batt = "<big>⚡</big> ??% •"
|
||||||
|
|
||||||
|
// UpdateBatteryInfo updates info about battery charge.
|
||||||
|
func UpdateBatteryInfo() {
|
||||||
|
var (
|
||||||
|
ch int
|
||||||
|
status string
|
||||||
|
Batts string
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
batteries, err := battery.GetAll()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
Batts = "<big>⚡</big> no batt"
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
battsInfo string
|
||||||
|
chStr string
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, b := range batteries {
|
||||||
|
switch b.State.Raw {
|
||||||
|
case battery.Charging:
|
||||||
|
status = `▲`
|
||||||
|
case battery.Discharging:
|
||||||
|
status = `▼`
|
||||||
|
case battery.Empty:
|
||||||
|
status = `✘`
|
||||||
|
default:
|
||||||
|
status = `•`
|
||||||
|
}
|
||||||
|
|
||||||
|
// N.B. there can be case when battery is overcharged and shows >100%. It alse can indicate that
|
||||||
|
// calibration data is out of date and battery should be re-calibrated.
|
||||||
|
ch = int(math.Round((b.Full - b.Current) * (100 / b.Full)))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ch > 85:
|
||||||
|
if Conf.Battery.Color.Full == "" {
|
||||||
|
chStr = fmt.Sprintf("% 3d%%", ch)
|
||||||
|
} else {
|
||||||
|
chStr = fmt.Sprintf(
|
||||||
|
`<span foreground="%s">% 3d%%</span>`,
|
||||||
|
Conf.Battery.Color.Full,
|
||||||
|
ch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ch < 85 && ch > 40:
|
||||||
|
if Conf.Battery.Color.AlmostFull == "" {
|
||||||
|
chStr = fmt.Sprintf("% 3d%%", ch)
|
||||||
|
} else {
|
||||||
|
chStr = fmt.Sprintf(
|
||||||
|
`<span foreground="%s">% 3d%%</span>`,
|
||||||
|
Conf.Battery.Color.AlmostFull,
|
||||||
|
ch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ch <= 40 && ch >= 10:
|
||||||
|
if Conf.Battery.Color.AlmostEmpty == "" {
|
||||||
|
chStr = fmt.Sprintf("% 3d%%", ch)
|
||||||
|
} else {
|
||||||
|
chStr = fmt.Sprintf(
|
||||||
|
`<span foreground="%s">% 3d%%</span>`,
|
||||||
|
Conf.Battery.Color.AlmostEmpty,
|
||||||
|
ch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ch < 10:
|
||||||
|
if Conf.Battery.Color.Empty == "" {
|
||||||
|
chStr = fmt.Sprintf("% 3d%%", ch)
|
||||||
|
} else {
|
||||||
|
chStr = fmt.Sprintf(
|
||||||
|
`<span foreground="%s">% 3d%%</span>`,
|
||||||
|
Conf.Battery.Color.Empty,
|
||||||
|
ch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
battsInfo += fmt.Sprintf("<big>⚡</big>B%d %s %s", i, chStr, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if battsInfo != "" {
|
||||||
|
Batts = battsInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Batt == Batts {
|
||||||
|
Batt = Batts
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
clock.go
Normal file
30
clock.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clock shows current date and time on i3bar.
|
||||||
|
var Clock = "Thu, 1 Jan 1970 1:00"
|
||||||
|
|
||||||
|
// UpdateClock get and updates (on i3bar) info about system clock.
|
||||||
|
func UpdateClock() {
|
||||||
|
for {
|
||||||
|
currentTime := time.Now()
|
||||||
|
hours, minutes, _ := currentTime.Clock()
|
||||||
|
year, month, day := currentTime.Date()
|
||||||
|
dow := currentTime.Weekday()
|
||||||
|
rmonth := [12]string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"}
|
||||||
|
rdow := [7]string{"Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"}
|
||||||
|
|
||||||
|
myclock := fmt.Sprintf("<big> %s, %d %s %d % 2d:%02d </big>", rdow[dow], day, rmonth[month-1], year, hours, minutes)
|
||||||
|
|
||||||
|
if myclock != Clock {
|
||||||
|
Clock = myclock
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
collection.go
Normal file
69
collection.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collection is sync.Map, that stores arbitrary data.
|
||||||
|
type Collection struct {
|
||||||
|
items sync.Map
|
||||||
|
close chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item just interface for stoing any data.
|
||||||
|
type item struct {
|
||||||
|
data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCollection creates new instance of "collection".
|
||||||
|
func NewCollection() *Collection {
|
||||||
|
c := &Collection{ //nolint:exhaustruct
|
||||||
|
close: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value by given key. Or empty value.
|
||||||
|
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
||||||
|
obj, exists := collection.items.Load(key)
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
item := obj.(item)
|
||||||
|
|
||||||
|
return item.data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set saves value to given "collection".
|
||||||
|
func (collection *Collection) Set(key interface{}, value interface{}) {
|
||||||
|
collection.items.Store(key, item{
|
||||||
|
data: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range apply function f to all keys/values in collection. Retuns immediatly on first error.
|
||||||
|
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
||||||
|
fn := func(key, value interface{}) bool {
|
||||||
|
item := value.(item)
|
||||||
|
|
||||||
|
return f(key, item.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.items.Range(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes key and corresponding value from collection.
|
||||||
|
func (collection *Collection) Delete(key interface{}) {
|
||||||
|
collection.items.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close frees memory occupied by collection and deletes collection itself.
|
||||||
|
func (collection *Collection) Close() {
|
||||||
|
collection.close <- struct{}{}
|
||||||
|
collection.items = sync.Map{}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
416
config.go
Normal file
416
config.go
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyConfig struct {
|
||||||
|
|
||||||
|
// Default text color
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
// Default background color
|
||||||
|
Background string `json:"background,omitempty"`
|
||||||
|
|
||||||
|
// LA Plugin
|
||||||
|
LA bool `json:"la,omitempty"`
|
||||||
|
|
||||||
|
Mem bool `json:"mem,omitempty"`
|
||||||
|
|
||||||
|
Clock struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
|
||||||
|
LeftClick struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Cmd []string `json:"cmd,omitempty"`
|
||||||
|
} `json:"left_click,omitempty"`
|
||||||
|
|
||||||
|
RightClick struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Cmd []string `json:"cmd,omitempty"`
|
||||||
|
} `json:"right_click,omitempty"`
|
||||||
|
} `json:"clock,omitempty"`
|
||||||
|
|
||||||
|
Battery struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Color struct {
|
||||||
|
Full string `json:"full,omitempty"`
|
||||||
|
Empty string `json:"empty,omitempty"`
|
||||||
|
AlmostFull string `json:"almost_full,omitempty"`
|
||||||
|
AlmostEmpty string `json:"almost_empty,omitempty"`
|
||||||
|
} `json:"color,omitempty"`
|
||||||
|
} `json:"battery,omitempty"`
|
||||||
|
|
||||||
|
CPUTemp struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
File []string `json:"file,omitempty"`
|
||||||
|
} `json:"cpu_temp,omitempty"`
|
||||||
|
|
||||||
|
CapsLock struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Background string `json:"background,omitempty"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
} `json:"capslock,omitempty"`
|
||||||
|
|
||||||
|
Vpn struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
StatusFile string `json:"statusFile,omitempty"`
|
||||||
|
MtimeThreshold int `json:"mtime_threshold,omitempty"`
|
||||||
|
DownColor string `json:"down_color,omitempty"`
|
||||||
|
UpColor string `json:"up_color,omitempty"`
|
||||||
|
|
||||||
|
TCPCheck struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Timeout int `json:"timeout,omitempty"`
|
||||||
|
} `json:"tcp_check,omitempty"`
|
||||||
|
} `json:"vpn,omitempty"`
|
||||||
|
|
||||||
|
SimpleVolumePa struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
Step int `json:"step,omitempty"`
|
||||||
|
RightClickCmd []string `json:"right_click_cmd,omitempty"`
|
||||||
|
WheelUp int `json:"wheel_up,omitempty"`
|
||||||
|
WheelDown int `json:"wheel_down,omitempty"`
|
||||||
|
MaxVolumeLimit int `json:"max_volume_limit,omitempty"`
|
||||||
|
} `json:"simple-volume-pa,omitempty"`
|
||||||
|
|
||||||
|
NetIf struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
DownColor string `json:"down_color,omitempty"`
|
||||||
|
UpColor string `json:"up_color,omitempty"`
|
||||||
|
|
||||||
|
If []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Dir string `json:"dir,omitempty"`
|
||||||
|
} `json:"if,omitempty"`
|
||||||
|
} `json:"net-if,omitempty"`
|
||||||
|
|
||||||
|
Cron struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
TimeZone string `json:"timezone,omitempty"`
|
||||||
|
|
||||||
|
Tasks []struct {
|
||||||
|
Time string `json:"time,omitempty"`
|
||||||
|
Cmd []string `json:"cmd,omitempty"`
|
||||||
|
} `json:"tasks,omitempty"`
|
||||||
|
} `json:"cron,omitempty"`
|
||||||
|
|
||||||
|
AppButtons bool `json:"app_buttons,omitempty"`
|
||||||
|
|
||||||
|
Apps []struct {
|
||||||
|
FullText string `json:"full_text,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Cmd string `json:"cmd,omitempty"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
Instance string `json:"instance,omitempty"`
|
||||||
|
Class string `json:"class,omitempty"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
Border string `json:"border,omitempty"`
|
||||||
|
BorderActive string `json:"border_active,omitempty"`
|
||||||
|
Separator bool `json:"separator,omitempty"`
|
||||||
|
SeparatorBlockWidth int `json:"separator_block_width,omitempty"`
|
||||||
|
} `json:"apps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conf global variable with application config.
|
||||||
|
var Conf MyConfig
|
||||||
|
|
||||||
|
// readConf reads and validates confg if config does not exist, it puts default config to the same dir where i3 config
|
||||||
|
// is located.
|
||||||
|
func readConf() (MyConfig, error) {
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
config MyConfig
|
||||||
|
err error
|
||||||
|
buf []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
path, err = LocateConfFile()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
|
||||||
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
buf = DefaultConfig
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, buf, 0644); err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Конфиг-файл длинноват для конфига.
|
||||||
|
if fileInfo.Size() > 65535 {
|
||||||
|
err := fmt.Errorf("config file %s is too long for config", path) //nolint: goerr113
|
||||||
|
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err = os.ReadFile(path)
|
||||||
|
|
||||||
|
// Не удалось прочитать
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to read config file %s: %w", path, err)
|
||||||
|
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to docs, hjson seems can parse "quirky" json, but parses it to the map.
|
||||||
|
// We interested in struct as output product: so we parse config to intermediate map then marshal it to json and
|
||||||
|
// then produced json unmarshal to struct. Not very effective way, but in app lifetime it happens only once.
|
||||||
|
var (
|
||||||
|
sampleConfig MyConfig
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("unable to parse config file %s: %w", path, err)
|
||||||
|
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("unable to parse config file %s: %w", path, err)
|
||||||
|
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
|
||||||
|
err := fmt.Errorf("unable to parse config file %s: %w", path, err)
|
||||||
|
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done with marshal-unmarshal config data, it is time to validate config.
|
||||||
|
if sampleConfig.Color == "" {
|
||||||
|
sampleConfig.Color = "#3e78fd"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Background == "" {
|
||||||
|
sampleConfig.Background = "#edeceb"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.La will be false if not set in config
|
||||||
|
// sampleConfig.Mem will be false if not set in config
|
||||||
|
// sampleConfig.Clock.Enabled will be false if not set in config
|
||||||
|
|
||||||
|
if sampleConfig.Clock.Color == "" {
|
||||||
|
sampleConfig.Clock.Color = "#666666"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Clock.LeftClick.Enabled will be false if not set in config
|
||||||
|
|
||||||
|
if len(sampleConfig.Clock.LeftClick.Cmd) == 0 {
|
||||||
|
sampleConfig.Clock.LeftClick.Cmd = append(sampleConfig.Clock.LeftClick.Cmd, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Clock.RightClick.Enabled will be false if not set in config
|
||||||
|
|
||||||
|
if len(sampleConfig.Clock.RightClick.Cmd) == 0 {
|
||||||
|
sampleConfig.Clock.RightClick.Cmd = append(sampleConfig.Clock.RightClick.Cmd, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.battery.Enabled will be false if not set in config
|
||||||
|
// sampleConfig.Battery.Color.Full will be empty string if not set
|
||||||
|
// sampleConfig.Battery.Color.Empty will be empty string if not set
|
||||||
|
// sampleConfig.Battery.Color.AlmostFull will be empty string if not set
|
||||||
|
// sampleConfig.Battery.Color.AlmostEmpty will be empty string if not set
|
||||||
|
|
||||||
|
// sampleConfig.CpuTemp.Enabled will be false if not set in config
|
||||||
|
|
||||||
|
// No files configured - disable plugin
|
||||||
|
if len(sampleConfig.CPUTemp.File) == 0 {
|
||||||
|
sampleConfig.CPUTemp.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.CapsLock.Enabled will false if not set in config
|
||||||
|
|
||||||
|
if sampleConfig.CapsLock.Color == "" {
|
||||||
|
sampleConfig.CapsLock.Color = "#3e78fd"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.CapsLock.Background == "" {
|
||||||
|
sampleConfig.CapsLock.Background = "#edeceb"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Vpn.Enabled will false if not set in config
|
||||||
|
|
||||||
|
// No status file - disable plugin
|
||||||
|
if sampleConfig.Vpn.StatusFile == "" {
|
||||||
|
sampleConfig.Vpn.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status file at least once per 3 seconds
|
||||||
|
if sampleConfig.Vpn.MtimeThreshold < 3 {
|
||||||
|
if sampleConfig.Vpn.Enabled {
|
||||||
|
log.Printf("vpn.mtime_threshold not set, using 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleConfig.Vpn.MtimeThreshold = 3 //nocritic: wsl
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Vpn.DownColor will be empty string if no value set in config
|
||||||
|
// sampleConfig.Vpn.UpColor will be empty string if no value set in config
|
||||||
|
// sampleConfig.Vpn.TcpCheck.Enabled will false if not set in config
|
||||||
|
|
||||||
|
// Disable plugin if value indaquate
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Port > 65535 {
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Enabled {
|
||||||
|
log.Printf("vpn.tcp_check.port > 65535, disabling vpn.tcp_check.enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleConfig.Vpn.TCPCheck.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable plugin if value indaquate
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Host == "" {
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Enabled {
|
||||||
|
log.Printf("vpn.tcp_check.host not set, disabling vpn.tcp_check.enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleConfig.Vpn.TCPCheck.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Timeout < 3 {
|
||||||
|
if sampleConfig.Vpn.TCPCheck.Enabled {
|
||||||
|
log.Printf("vpn.tcp_check.timeout < 3, using 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleConfig.Vpn.TCPCheck.Timeout = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: appbuttons, etc..
|
||||||
|
// sampleConfig.SimpleVolumePa.Enabled will false if not set in config
|
||||||
|
|
||||||
|
if sampleConfig.SimpleVolumePa.Symbol == "" {
|
||||||
|
sampleConfig.SimpleVolumePa.Symbol = `🔊`
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.SimpleVolumePa.Step == 0 {
|
||||||
|
sampleConfig.SimpleVolumePa.Step = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.SimpleVolumePa.WheelUp == 0 {
|
||||||
|
sampleConfig.SimpleVolumePa.WheelUp = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.SimpleVolumePa.WheelDown == 0 {
|
||||||
|
sampleConfig.SimpleVolumePa.WheelDown = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.SimpleVolumePa.MaxVolumeLimit == 0 {
|
||||||
|
sampleConfig.SimpleVolumePa.MaxVolumeLimit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sampleConfig.SimpleVolumePa.RightClickCmd) > 0 {
|
||||||
|
if sampleConfig.SimpleVolumePa.RightClickCmd[0] == "" {
|
||||||
|
sampleConfig.SimpleVolumePa.RightClickCmd[0] = "true"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sampleConfig.SimpleVolumePa.RightClickCmd = append(sampleConfig.SimpleVolumePa.RightClickCmd, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Cron.Enabled will false if not set in config
|
||||||
|
|
||||||
|
if sampleConfig.Cron.TimeZone == "" {
|
||||||
|
sampleConfig.Cron.TimeZone = "GMT+0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sampleConfig.Cron.Tasks) == 0 {
|
||||||
|
sampleConfig.Cron.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.NetIf.Enabled will false if not set in config
|
||||||
|
|
||||||
|
if len(sampleConfig.NetIf.If) == 0 {
|
||||||
|
sampleConfig.NetIf.Enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.NetIf.DownColor == "" {
|
||||||
|
sampleConfig.NetIf.DownColor = "red"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.NetIf.UpColor == "" {
|
||||||
|
sampleConfig.NetIf.UpColor = "green"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.AppButtons will false if not set in config
|
||||||
|
|
||||||
|
if len(sampleConfig.Apps) == 0 {
|
||||||
|
sampleConfig.AppButtons = false
|
||||||
|
} else {
|
||||||
|
for num, app := range sampleConfig.Apps {
|
||||||
|
// app.Instance can be missing.
|
||||||
|
// app.Class can be missing.
|
||||||
|
|
||||||
|
if app.Name == "" {
|
||||||
|
app.Name = fmt.Sprintf("app%d", num)
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.Args can be empty slice. In that case command will be run without aruments.
|
||||||
|
|
||||||
|
if app.Cmd == "" {
|
||||||
|
app.Cmd = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Color == "" {
|
||||||
|
app.Color = Conf.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Border == "" {
|
||||||
|
app.Border = Conf.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.BorderActive == "" {
|
||||||
|
app.BorderActive = Conf.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.FullText == "" {
|
||||||
|
app.FullText = fmt.Sprintf(" %d ", num)
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.Separator can be omitted, in that case it is false
|
||||||
|
// app.SeparatorBlockWidth can be missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config = sampleConfig
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocateConfFile make a try to locate i3 config dir.
|
||||||
|
func LocateConfFile() (string, error) {
|
||||||
|
i3ConfigPath, err := xdg.SearchConfigFile("i3/config")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to find i3 config: %w", err)
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Dir(i3ConfigPath) + "/i3status-go.json", nil
|
||||||
|
}
|
||||||
80
cputemp.go
Normal file
80
cputemp.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CPUTemperature stores average temperature among all cores.
|
||||||
|
var CPUTemperature int64
|
||||||
|
|
||||||
|
// UpdateCPUTemperature gets and updates average CPU cores temperature.
|
||||||
|
func UpdateCPUTemperature() {
|
||||||
|
n := len(Conf.CPUTemp.File)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
temperature = make([]int64, n)
|
||||||
|
tSum int64
|
||||||
|
tAvg int64
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, filename := range Conf.CPUTemp.File {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to open %s: %s", filename, err)
|
||||||
|
} else {
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
line, _, err := reader.ReadLine()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to read from %s: %s", filename, err)
|
||||||
|
err = file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to close %s: %s", filename, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to close %s: %s", filename, err)
|
||||||
|
} else {
|
||||||
|
temp, err := strconv.ParseInt(string(line), 10, 32)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to convert string to number from file %s: %s", filename, err)
|
||||||
|
} else {
|
||||||
|
if temp > 1000 {
|
||||||
|
temp /= 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
temperature[i] = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(temperature) == 1 {
|
||||||
|
tAvg = temperature[0]
|
||||||
|
} else {
|
||||||
|
for _, t := range temperature {
|
||||||
|
tSum += t
|
||||||
|
}
|
||||||
|
|
||||||
|
tAvg = tSum / int64(len(temperature))
|
||||||
|
}
|
||||||
|
|
||||||
|
if CPUTemperature != tAvg {
|
||||||
|
CPUTemperature = tAvg
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
cron.go
Normal file
45
cron.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// myCron contains cron handler.
|
||||||
|
var myCron gocron.Scheduler
|
||||||
|
|
||||||
|
// RunCron parses config part, related to cron tasks and kicks cron to run.
|
||||||
|
func RunCron() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
myCron, err = gocron.NewScheduler()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to create built-in cron: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range Conf.Cron.Tasks {
|
||||||
|
_, err := myCron.NewJob(
|
||||||
|
gocron.CronJob(job.Time, false),
|
||||||
|
gocron.NewTask(
|
||||||
|
// Сука, просто заспавнить бинарь, ну почему это надо делать через жопу-то?
|
||||||
|
any(
|
||||||
|
func() bool {
|
||||||
|
RunChan <- job.Cmd
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to add cron job: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
myCron.Start()
|
||||||
|
}
|
||||||
26
go.mod
Normal file
26
go.mod
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module github.com/eleksir/i3status-go
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/adrg/xdg v0.4.0
|
||||||
|
github.com/distatus/battery v0.11.0
|
||||||
|
github.com/go-co-op/gocron/v2 v2.1.2
|
||||||
|
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
|
github.com/mafik/pulseaudio v0.0.0-20200511091429-8449222912dd
|
||||||
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
|
go.i3wm.org/i3 v0.0.0-20190720062127-36e6ec85cc5a
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
|
||||||
|
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
howett.net/plist v1.0.1 // indirect
|
||||||
|
)
|
||||||
64
go.sum
Normal file
64
go.sum
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
|
||||||
|
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/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||||
|
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=
|
||||||
|
github.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.1.2 h1:+6tTOA9aBaKXpDWExw07hYoGEBzT+4CkGSVAiJ7WSXs=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.1.2/go.mod h1:0MfNAXEchzeSH1vtkZrTAcSMWqyL435kL6CA4b0bjrg=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
||||||
|
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
|
github.com/mafik/pulseaudio v0.0.0-20200511091429-8449222912dd h1:gn6oLpDCn3wY+1WfbbP0OwxXL+2eHi3mxF/DsNyPHUM=
|
||||||
|
github.com/mafik/pulseaudio v0.0.0-20200511091429-8449222912dd/go.mod h1:dlpd1fnLAhI6g9tM/aCobgN/Yka1/SkHrBTfAgDdb9Q=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
|
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.i3wm.org/i3 v0.0.0-20190720062127-36e6ec85cc5a h1:yeQv6GWN8DsA2f3b+u1zhRIJg2HmsGYqXsd+W7ULr7Q=
|
||||||
|
go.i3wm.org/i3 v0.0.0-20190720062127-36e6ec85cc5a/go.mod h1:7w17+r1F28Yxb1pHu8SxARJo+RGvJCAGOEf+GLw+2aQ=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
|
||||||
|
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||||
|
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
227
i3.go
Normal file
227
i3.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"go.i3wm.org/i3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WinList struct {
|
||||||
|
Instance Collection
|
||||||
|
Class Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// WL contains collections of all windows currently running, collection of their classes and collection of their
|
||||||
|
// instances.
|
||||||
|
var WL WinList
|
||||||
|
|
||||||
|
// UpdateI3WinList subsribes to all window-related events and send them to I3EventParser() for more detailed parsing.
|
||||||
|
func UpdateI3WinList() {
|
||||||
|
// TODO: Здесь получить начальный список окон, управляемых i3wm (через GetTree()) и сложить их атрибутику в переменную WL
|
||||||
|
Tree, err := i3.GetTree()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to get list of windows: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtractProps(Tree.Root)
|
||||||
|
|
||||||
|
i3wm := i3.Subscribe(i3.WindowEventType)
|
||||||
|
|
||||||
|
for i3wm.Next() {
|
||||||
|
e := i3wm.Event().(*i3.WindowEvent)
|
||||||
|
go I3EventParser(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatal(i3wm.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
// I3EventParser parses i3wm events and updates WL (aka Windows List).
|
||||||
|
func I3EventParser(e *i3.WindowEvent) {
|
||||||
|
switch e.Change {
|
||||||
|
case "new":
|
||||||
|
var (
|
||||||
|
count uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
countI, exist := WL.Class.Get(e.Container.WindowProperties.Class)
|
||||||
|
|
||||||
|
if exist {
|
||||||
|
count = interfaceToUint64(countI)
|
||||||
|
} else {
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
WL.Class.Set(e.Container.WindowProperties.Class, count)
|
||||||
|
|
||||||
|
countI, exist = WL.Instance.Get(e.Container.WindowProperties.Instance)
|
||||||
|
|
||||||
|
if exist {
|
||||||
|
count = interfaceToUint64(countI)
|
||||||
|
} else {
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
WL.Instance.Set(e.Container.WindowProperties.Instance, count)
|
||||||
|
|
||||||
|
UpdateReady <- true
|
||||||
|
|
||||||
|
case "close":
|
||||||
|
countI, exist := WL.Class.Get(e.Container.WindowProperties.Class)
|
||||||
|
|
||||||
|
if exist {
|
||||||
|
count := interfaceToUint64(countI)
|
||||||
|
|
||||||
|
if count > 1 {
|
||||||
|
count--
|
||||||
|
|
||||||
|
WL.Class.Set(e.Container.WindowProperties.Class, count)
|
||||||
|
} else {
|
||||||
|
WL.Class.Delete(e.Container.WindowProperties.Class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countI, exist = WL.Instance.Get(e.Container.WindowProperties.Instance)
|
||||||
|
|
||||||
|
if exist {
|
||||||
|
count := interfaceToUint64(countI)
|
||||||
|
|
||||||
|
if count > 1 {
|
||||||
|
count--
|
||||||
|
|
||||||
|
WL.Instance.Set(e.Container.WindowProperties.Instance, count)
|
||||||
|
} else {
|
||||||
|
WL.Instance.Delete(e.Container.WindowProperties.Instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interfaceToUint превращает данный интерфейс в Uint64.
|
||||||
|
// Если может, конечно :) .
|
||||||
|
func interfaceToUint64(iface interface{}) uint64 {
|
||||||
|
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
|
||||||
|
// Поскольку interface{} может быть чем угодно, перестрахуемся
|
||||||
|
if reflect.TypeOf(iface).Kind() == reflect.Uint64 {
|
||||||
|
return reflect.ValueOf(iface).Uint()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasWindows returns true if given Window Class and/or Instance has more than 0 windows according our observations.
|
||||||
|
func HasWindows(className string, instanceName string) bool {
|
||||||
|
var (
|
||||||
|
ret bool
|
||||||
|
myclass bool
|
||||||
|
myinstance bool
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case className != "" && instanceName != "":
|
||||||
|
// Since we agree that we use regexp for searching window class and instance, but not yet decide what to do
|
||||||
|
// with exact match, comment it out and leave it here.
|
||||||
|
/*
|
||||||
|
_, myclass = WL.Class.Get(className)
|
||||||
|
_, myinstance = WL.Instance.Get(instanceName)
|
||||||
|
*/
|
||||||
|
myclass = FindWindowClass(className)
|
||||||
|
myinstance = FindWindowInstance(instanceName)
|
||||||
|
ret = myclass && myinstance
|
||||||
|
|
||||||
|
case className != "":
|
||||||
|
// _, ret = WL.Class.Get(className)
|
||||||
|
ret = FindWindowClass(className)
|
||||||
|
|
||||||
|
case instanceName != "":
|
||||||
|
// _, ret = WL.Instance.Get(instanceName)
|
||||||
|
ret = FindWindowInstance(instanceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindWindowInstance returns true if given regex matces at least one pattern in WL.Class collection.
|
||||||
|
// Otherwise, returns false.
|
||||||
|
func FindWindowInstance(regexpStr string) bool {
|
||||||
|
var found = false
|
||||||
|
|
||||||
|
f := func(key interface{}, value interface{}) bool {
|
||||||
|
keyStr := reflect.ValueOf(key).String()
|
||||||
|
|
||||||
|
re, err := regexp.Compile(regexpStr) //nolint:nolintlint, gocritic, we do not want shit our pants and panic on
|
||||||
|
// erroneous regexp, we want log it to stderr, that it.
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to perform regexp match: %s", err)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if re.Match([]byte(keyStr)) { //nolint:mirror
|
||||||
|
found = true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
WL.Instance.Range(f)
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindWindowClass returns true if given regex matces at least one pattern in WL.Class collection.
|
||||||
|
// Otherwise, returns false.
|
||||||
|
func FindWindowClass(regexpStr string) bool {
|
||||||
|
var found = false
|
||||||
|
|
||||||
|
f := func(key interface{}, value interface{}) bool {
|
||||||
|
keyStr := reflect.ValueOf(key).String()
|
||||||
|
|
||||||
|
re, err := regexp.Compile(regexpStr) //nolint:nolintlint, gocritic, we do not want shit our pants and panic on
|
||||||
|
// erroneous regexp, we want log it to stderr, that it.
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to perform regexp match: %s", err)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if re.Match([]byte(keyStr)) { //nolint:mirror
|
||||||
|
found = true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
WL.Class.Range(f)
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractProps fills WL stricture collections with instance name and class name of given node (x11, that is).
|
||||||
|
func ExtractProps(n *i3.Node) {
|
||||||
|
var e i3.WindowEvent
|
||||||
|
e.Change = "new"
|
||||||
|
e.Container = *n
|
||||||
|
|
||||||
|
I3EventParser(&e)
|
||||||
|
|
||||||
|
for _, node := range n.Nodes {
|
||||||
|
ExtractProps(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range n.FloatingNodes {
|
||||||
|
ExtractProps(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
247
i3status-go-example.json
Normal file
247
i3status-go-example.json
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
{
|
||||||
|
// Default text and *block* background colors
|
||||||
|
"color" : "#3e78fd",
|
||||||
|
"background" : "#000000",
|
||||||
|
|
||||||
|
// Load Average
|
||||||
|
"la" : true,
|
||||||
|
|
||||||
|
// Memory stats
|
||||||
|
"mem" : true,
|
||||||
|
|
||||||
|
// Just clock
|
||||||
|
"clock": {
|
||||||
|
"enabled" : true,
|
||||||
|
// Text color in html (actually, pango) notation
|
||||||
|
"color" : "#666666",
|
||||||
|
"left_click" : {
|
||||||
|
"enabled" : false,
|
||||||
|
// cmd is array of command itself and its arguments
|
||||||
|
"cmd" : [ "orage" ]
|
||||||
|
},
|
||||||
|
"right_click" : {
|
||||||
|
"enabled" : false,
|
||||||
|
"cmd" : [ "true" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Battery, useful for laptops
|
||||||
|
"battery": {
|
||||||
|
"enabled": false,
|
||||||
|
"color": {
|
||||||
|
// colors in html notation
|
||||||
|
"full": "green",
|
||||||
|
"empty": "red",
|
||||||
|
"almost_full": "yellow",
|
||||||
|
"almost_empty": "orange"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// CPU Temperature monitor, shows average value among given sources
|
||||||
|
"cpu_temp": {
|
||||||
|
"enabled": false,
|
||||||
|
// Files in sysfs that contains cpu temperatures
|
||||||
|
"file": [
|
||||||
|
"/sys/class/hwmon/hwmon4/temp2_input",
|
||||||
|
"/sys/class/hwmon/hwmon4/temp3_input",
|
||||||
|
"/sys/class/hwmon/hwmon4/temp4_input",
|
||||||
|
"/sys/class/hwmon/hwmon4/temp5_input",
|
||||||
|
"/sys/class/hwmon/hwmon4/temp6_input",
|
||||||
|
"/sys/class/hwmon/hwmon4/temp7_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Caps Lock status
|
||||||
|
"capslock": {
|
||||||
|
"enabled" : false,
|
||||||
|
// Colors in html notation
|
||||||
|
"background" : "#000000",
|
||||||
|
"color" : "#3e78fd"
|
||||||
|
},
|
||||||
|
|
||||||
|
// OpenVPN status. Note that directive status in openvpn.conf should point to
|
||||||
|
// state file and *update* its timestamp at least as frequent as mtime_threshold in seconds
|
||||||
|
"vpn": {
|
||||||
|
"enabled": false,
|
||||||
|
"statusfile" : "/tmp/openvpn-status.stat",
|
||||||
|
"mtime_threshold": 10,
|
||||||
|
// Color in html notation
|
||||||
|
"down_color" : "red",
|
||||||
|
"up_color" : "green",
|
||||||
|
// Also we can tcp-ping some service inside vpn network to ensure that connection is not stoned
|
||||||
|
"tcp_check" : {
|
||||||
|
"enabled": false,
|
||||||
|
"host": "somehost.tld",
|
||||||
|
"port": 443,
|
||||||
|
"timeout" : 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Simple sound volume indicator
|
||||||
|
"simple-volume-pa": {
|
||||||
|
"enabled": false,
|
||||||
|
"symbol": "🔊",
|
||||||
|
// If not defined - will be "true"
|
||||||
|
"right_click_cmd": ["pavucontrol"],
|
||||||
|
// Change volume step in %, if not configured it is 5
|
||||||
|
"step": 3,
|
||||||
|
// We'll change volume level with mouse wheel. Under X11 it is mapped to some buttons, typically 4 and 5 (z-axis).
|
||||||
|
// If omited, default value is 4
|
||||||
|
"wheel_up": 4,
|
||||||
|
// If omited, default value is 5
|
||||||
|
"wheel_down": 5,
|
||||||
|
// Maximum sound volume, can be over 100%, but sould not be :) Default value 100 if not set.
|
||||||
|
"max_volume_limit": 120
|
||||||
|
},
|
||||||
|
|
||||||
|
"net-if" : {
|
||||||
|
"enabled" : false,
|
||||||
|
// In html notation
|
||||||
|
"down_color" : "red",
|
||||||
|
"up_color" : "green",
|
||||||
|
"if" : [
|
||||||
|
{
|
||||||
|
// Name as it displayed on i3bar
|
||||||
|
"name" : "eth0",
|
||||||
|
// Directory containing net if status files
|
||||||
|
"dir" : "/sys/class/net/eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Intended mainly for use with desktop notifications
|
||||||
|
// Because it inherits all env vars from i3, it can intercat with dbus and other
|
||||||
|
// such user session services.
|
||||||
|
"cron" : {
|
||||||
|
"enabled" : false,
|
||||||
|
"tasks" : [
|
||||||
|
{
|
||||||
|
// Time in crontab notation
|
||||||
|
"time" : "55 23 * * *",
|
||||||
|
"cmd" : [
|
||||||
|
"notify-send", "Sleeping Time!", "Take a nap."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// time in crontab format
|
||||||
|
"time" : "55 12 * * 2,4",
|
||||||
|
// commands as it should be invoked in bash script
|
||||||
|
"cmd" : [
|
||||||
|
"notify-send", "Pizza time!", "M-m-m, tasty!"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Weather to display Application Buttons
|
||||||
|
"app_buttons": true,
|
||||||
|
|
||||||
|
// Buttons - application launch "buttons"
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
// Text dispalyed on button
|
||||||
|
"full_text" : "📂",
|
||||||
|
// Internal name, can be any string, but unique for each app button
|
||||||
|
"name" : "thunar",
|
||||||
|
// Command itself
|
||||||
|
"cmd" : "thunar",
|
||||||
|
// Array of command args, optional
|
||||||
|
"args": [ "." ],
|
||||||
|
// Regexp that match with Instance component of WM_CLASS(STRING) of xprop output
|
||||||
|
"instance": "^thunar$",
|
||||||
|
// Regexp that match with Class component of WM_CLASS(STRING) of xprop output
|
||||||
|
"class": "^Thunar$",
|
||||||
|
// text color in html notation
|
||||||
|
"color": "#ffa500",
|
||||||
|
// border color in html notation
|
||||||
|
"border": "#666666",
|
||||||
|
// border color if there is at least one window with given Instance and/or Class exist
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " ♬ ",
|
||||||
|
"name" : "deadbeef",
|
||||||
|
"cmd" : "deadbeef",
|
||||||
|
"instance" : "^deadbeef$",
|
||||||
|
"class" : "^Deadbeef$",
|
||||||
|
"color" : "#00aa33",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " Term ",
|
||||||
|
"name" : "xterm",
|
||||||
|
"cmd" : "xterm",
|
||||||
|
"instance" : "^xterm$",
|
||||||
|
"class" : "^XTerm$",
|
||||||
|
"color" : "#666666",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " Ff ",
|
||||||
|
"name" : "firefox",
|
||||||
|
"cmd" : "firefox",
|
||||||
|
"instance" : "^Navigator$",
|
||||||
|
"class" : "^Firefox$",
|
||||||
|
"color" : "#ffa500",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " C ",
|
||||||
|
"name" : "chrome",
|
||||||
|
"cmd" : "chrome",
|
||||||
|
"instance" : "^google-chrome$",
|
||||||
|
"class" : "^Google-chrome$",
|
||||||
|
"color" : "#ffa500",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " @ ",
|
||||||
|
"name" : "thunderbird",
|
||||||
|
"cmd" : "thunderbird",
|
||||||
|
"instance" : "^Mail$",
|
||||||
|
"class" : "^Thunderbird$",
|
||||||
|
"color" : "#006994",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " Tg ",
|
||||||
|
"name" : "telegram",
|
||||||
|
"cmd" : "Telegram",
|
||||||
|
"instance" : "^Telegram$",
|
||||||
|
"class" : "^TelegramDesktop$",
|
||||||
|
"color" : "#5a8850",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full_text" : " T ",
|
||||||
|
"name" : "mousepad",
|
||||||
|
"cmd" : "mousepad",
|
||||||
|
"instance" : "^mousepad$",
|
||||||
|
"class" : "^Mousepad$",
|
||||||
|
"border": "#666666",
|
||||||
|
"border_active": "#3e78fd",
|
||||||
|
"separator": false,
|
||||||
|
"separator_block_width": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
ifmon.go
Normal file
62
ifmon.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IfStatus global var conaining cumulative status of all monitored net intefaces.
|
||||||
|
var IfStatus string
|
||||||
|
|
||||||
|
// UpdateIfStatus updates network interfaces status for i3bar.
|
||||||
|
func UpdateIfStatus() {
|
||||||
|
for {
|
||||||
|
var statusSum string
|
||||||
|
|
||||||
|
for _, item := range Conf.NetIf.If {
|
||||||
|
var (
|
||||||
|
name = item.Name
|
||||||
|
)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = filepath.Base(item.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
operstate, err := os.ReadFile(item.Dir + "/operstate")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
operstate = []byte("?")
|
||||||
|
|
||||||
|
log.Printf("Unable to get net if status from file %s: %s", item.Dir+"/operstate", err)
|
||||||
|
} else {
|
||||||
|
switch strings.TrimSpace(string(operstate)) {
|
||||||
|
case "up":
|
||||||
|
StatusStr := fmt.Sprintf("<span foreground=\"%s\">⍋</span>", Conf.NetIf.UpColor)
|
||||||
|
operstate = []byte(StatusStr)
|
||||||
|
case "down":
|
||||||
|
statusStr := fmt.Sprintf("<span foreground=\"%s\">⍒</span>", Conf.NetIf.DownColor)
|
||||||
|
operstate = []byte(statusStr)
|
||||||
|
default:
|
||||||
|
operstate = []byte(`?`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusSum != "" {
|
||||||
|
statusSum += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
statusSum += fmt.Sprintf("%s:%s", name, operstate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if IfStatus != statusSum {
|
||||||
|
IfStatus = statusSum
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
la.go
Normal file
27
la.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/load"
|
||||||
|
)
|
||||||
|
|
||||||
|
// La переменная, хранящая значение LA за последнюю минуту.
|
||||||
|
var La = "-1"
|
||||||
|
|
||||||
|
// UpdateLaStats вытаскивает показание LA за последнюю минуту.
|
||||||
|
func UpdateLaStats() {
|
||||||
|
for {
|
||||||
|
l, _ := load.Avg()
|
||||||
|
|
||||||
|
lav := fmt.Sprintf("%.2f", l.Load1)
|
||||||
|
|
||||||
|
if La != lav {
|
||||||
|
La = lav
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
231
main.go
Normal file
231
main.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// I3BarOutBlock is structure element for I3BarOut, it represents i3bar output json block format.
|
||||||
|
type I3BarOutBlock struct {
|
||||||
|
FullText string `json:"full_text"`
|
||||||
|
// ShortText will be shown if not enough room for FullText, threshold width defined in MinWidth
|
||||||
|
ShortText string `json:"short_text,omitempty"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
Background string `json:"background,omitempty"`
|
||||||
|
Border string `json:"border,omitempty"`
|
||||||
|
BorderTop int `json:"border_top,omitempty"`
|
||||||
|
BorderRight int `json:"border_right,omitempty"`
|
||||||
|
BorderBottom int `json:"border_bottom,omitempty"`
|
||||||
|
BorderLeft int `json:"border_left,omitempty"`
|
||||||
|
// measured either in pixels or in characters, so either int or string, let's make it string :)
|
||||||
|
MinWidth string `json:"min_width,omitempty"`
|
||||||
|
Align string `json:"align,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Instance string `json:"instance,omitempty"`
|
||||||
|
Urgent bool `json:"urgent,omitempty"`
|
||||||
|
Separator bool `json:"separator,omitempty"`
|
||||||
|
SeparatorBlockWidth int `json:"separator_block_width,omitempty"`
|
||||||
|
Markup string `json:"markup,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReady channel that "refreshes" i3bar (generates stdout json line).
|
||||||
|
var UpdateReady = make(chan bool)
|
||||||
|
|
||||||
|
//go:embed i3status-go-example.json
|
||||||
|
var EmbeddedDefaultConfig embed.FS
|
||||||
|
var DefaultConfig []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultConfig, _ = EmbeddedDefaultConfig.ReadFile("i3status-go-example.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program entry point.
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
Conf, err = readConf()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go Spawner()
|
||||||
|
go ParseStdin()
|
||||||
|
go CleanZombies()
|
||||||
|
|
||||||
|
if Conf.AppButtons {
|
||||||
|
go UpdateI3WinList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick signal handler
|
||||||
|
go sigHandler()
|
||||||
|
signal.Notify(sigChan,
|
||||||
|
syscall.SIGUSR1,
|
||||||
|
syscall.SIGUSR2)
|
||||||
|
|
||||||
|
// Populate memory stats
|
||||||
|
if Conf.Mem {
|
||||||
|
go UpdateMemStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate LA stats
|
||||||
|
if Conf.LA {
|
||||||
|
go UpdateLaStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate CPUTemperature
|
||||||
|
if Conf.CPUTemp.Enabled {
|
||||||
|
go UpdateCPUTemperature()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate Clock
|
||||||
|
if Conf.Clock.Enabled {
|
||||||
|
go UpdateClock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Battery.Enabled {
|
||||||
|
go UpdateBatteryInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.SimpleVolumePa.Enabled {
|
||||||
|
go UpdateVolumeInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.NetIf.Enabled {
|
||||||
|
go UpdateIfStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Cron.Enabled {
|
||||||
|
go RunCron()
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Vpn.Enabled {
|
||||||
|
go UpdateVPNStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
I3bar documentation pretends that message protocol must be valid json. In practice, we only have to print valid
|
||||||
|
header, empty json array and (potentially infinite) json lines (line that is valid json by itself) that is
|
||||||
|
actually json arrays. We do not need to *close* this json at all.
|
||||||
|
Gracefully closed json required when i3bar initiates our program to stop|quit, this (should) happens just before
|
||||||
|
i3bar itself terminating. So we don't care.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Print header and one empty message and wait for updates
|
||||||
|
fmt.Printf(
|
||||||
|
"{\"version\": 1, \"stop_signal\": %d, \"cont_signal\": %d, \"click_events\": true}\n",
|
||||||
|
syscall.SIGUSR1,
|
||||||
|
syscall.SIGUSR2,
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println("[ [],")
|
||||||
|
|
||||||
|
for {
|
||||||
|
if <-UpdateReady {
|
||||||
|
// Actually build json struct, marshal it and print result
|
||||||
|
var j []I3BarOutBlock
|
||||||
|
|
||||||
|
if Conf.AppButtons {
|
||||||
|
for _, app := range Conf.Apps {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
|
||||||
|
b.FullText = app.FullText
|
||||||
|
b.Background = Conf.Background
|
||||||
|
b.Color = app.Color
|
||||||
|
b.Instance = app.Instance
|
||||||
|
b.Markup = `pango`
|
||||||
|
b.Separator = app.Separator
|
||||||
|
b.SeparatorBlockWidth = app.SeparatorBlockWidth
|
||||||
|
b.Name = app.Name
|
||||||
|
|
||||||
|
if HasWindows(app.Class, app.Instance) {
|
||||||
|
b.Border = app.BorderActive
|
||||||
|
} else {
|
||||||
|
b.Border = app.Border
|
||||||
|
}
|
||||||
|
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.CPUTemp.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = fmt.Sprintf("CPU: %d°", CPUTemperature)
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Mem {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = fmt.Sprintf("M:%d%% SHM:%dM SW:%dM", Memory.Usedpct, Memory.Shared, Memory.Swap)
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.LA {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = fmt.Sprintf("La: %s", La)
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.NetIf.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = IfStatus
|
||||||
|
b.Markup = "pango"
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Vpn.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = VPNStatus
|
||||||
|
b.Markup = "pango"
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Battery.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.FullText = Batt
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.SimpleVolumePa.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.Name = "simple-volume-pa"
|
||||||
|
b.FullText = SoundVolume
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Conf.Clock.Enabled {
|
||||||
|
var b I3BarOutBlock
|
||||||
|
b.Name = `wallclock`
|
||||||
|
b.FullText = Clock
|
||||||
|
b.Markup = "pango"
|
||||||
|
b.Color = Conf.Clock.Color
|
||||||
|
j = append(j, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if PrintOutput && len(j) > 0 {
|
||||||
|
PrintToI3bar(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintToI3bar prints info to stdout according to ipc docs (https://i3wm.org/docs/i3bar-protocol.html)
|
||||||
|
func PrintToI3bar(message []I3BarOutBlock) {
|
||||||
|
// we do not need to html-encode output, json.Marshal does this forcefully, so invent our own Marshal
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buf)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
err := encoder.Encode(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to json-encode message, %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(strings.TrimSuffix(buf.String(), "\n") + ",")
|
||||||
|
}
|
||||||
34
memory.go
Normal file
34
memory.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mem struct with mem stats.
|
||||||
|
type Mem struct {
|
||||||
|
Usedpct int64
|
||||||
|
Shared int64
|
||||||
|
Swap int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory stores string with mem stats for i3bar.
|
||||||
|
var Memory Mem
|
||||||
|
|
||||||
|
// UpdateMemStats parses mem info stats.
|
||||||
|
func UpdateMemStats() {
|
||||||
|
for {
|
||||||
|
v, _ := mem.VirtualMemory()
|
||||||
|
|
||||||
|
if Memory.Usedpct != int64(v.UsedPercent) || Memory.Shared != int64(v.Shared/1024/1024) ||
|
||||||
|
Memory.Swap != int64(v.SwapTotal-v.SwapFree) {
|
||||||
|
Memory.Usedpct = int64(v.UsedPercent)
|
||||||
|
Memory.Shared = int64(v.Shared / 1024 / 1024)
|
||||||
|
Memory.Swap = int64(v.SwapTotal - v.SwapFree)
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
102
pulseaudio.go
Normal file
102
pulseaudio.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
p "github.com/mafik/pulseaudio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SoundVolume = "🔊:0%"
|
||||||
|
var pa *p.Client
|
||||||
|
|
||||||
|
// UpdateVolumeInfo updates info about current Sound Volume.
|
||||||
|
func UpdateVolumeInfo() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pa, err = p.NewClient()
|
||||||
|
|
||||||
|
// It can happen if no pulseaudio server running for current user.
|
||||||
|
// If no server running we have to run one.
|
||||||
|
if err != nil {
|
||||||
|
if err := PaReinit(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vol, err := pa.Volume()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable get volume from pulseaudio server: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundVolume = fmt.Sprintf("%s:%d%%", Conf.SimpleVolumePa.Symbol, int64(vol*100))
|
||||||
|
|
||||||
|
UpdateReady <- true
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Subscribe to update notification channel, to get info that volume changed
|
||||||
|
pulseUpdate, err := pa.Updates()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to subscribe to pulseaudio updates: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rake update events
|
||||||
|
for range pulseUpdate {
|
||||||
|
vol, err = pa.Volume()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable get volume from pulseaudio server: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundVolume = fmt.Sprintf("%s:%d%%", Conf.SimpleVolumePa.Symbol, int64(vol*100))
|
||||||
|
|
||||||
|
UpdateReady <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := PaReinit(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This code is unreachable :(
|
||||||
|
pa.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaReinit re-inits pulseaudio and connection to it.
|
||||||
|
func PaReinit() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// If we out of updates, it seems that someone killed pulseaudio server. Restart it.
|
||||||
|
cmd := exec.Command("pulseaudio", "--check")
|
||||||
|
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
return fmt.Errorf("pulseaudio is already running, but we have troubles contacting it: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("pulseaudio", "--start")
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize pulseaudio server instance: %w", err)
|
||||||
|
} //nolint:wsl
|
||||||
|
|
||||||
|
pa, err = p.NewClient()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to make client connection to pulseaudio: %s", err)
|
||||||
|
} //nolint:wsl
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
33
signal.go
Normal file
33
signal.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var PrintOutput = true
|
||||||
|
var sigChan = make(chan os.Signal, 1)
|
||||||
|
|
||||||
|
// sigHandler OS signal handler.
|
||||||
|
func sigHandler() {
|
||||||
|
for s := range sigChan {
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGUSR1:
|
||||||
|
log.Print("Got SIGUSR1, stopping output")
|
||||||
|
|
||||||
|
PrintOutput = false
|
||||||
|
|
||||||
|
case syscall.SIGUSR2:
|
||||||
|
log.Print("Got SIGUSR2, resuming output")
|
||||||
|
|
||||||
|
PrintOutput = true
|
||||||
|
|
||||||
|
UpdateReady <- true
|
||||||
|
|
||||||
|
// We have signal that we're not interested in, so make a new loop iteration.
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
spawn.go
Normal file
58
spawn.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunChan to this channel we send command and its argv to execute as separate process.
|
||||||
|
var RunChan = make(chan []string, 128)
|
||||||
|
|
||||||
|
// Spawn forks and execs given program, and also detaches form its control tty.
|
||||||
|
func Spawner() {
|
||||||
|
for prg := range RunChan {
|
||||||
|
devnullR, _ := os.Open(os.DevNull)
|
||||||
|
devnullW, _ := os.OpenFile(os.DevNull, os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
|
||||||
|
spa := syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
Foreground: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
pa := syscall.ProcAttr{
|
||||||
|
Dir: os.Getenv(`HOME`),
|
||||||
|
Env: os.Environ(),
|
||||||
|
Files: []uintptr{devnullR.Fd(), devnullW.Fd(), devnullW.Fd()},
|
||||||
|
Sys: &spa,
|
||||||
|
}
|
||||||
|
|
||||||
|
bin, err := exec.LookPath(prg[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to spawn %s: %s", prg[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = syscall.ForkExec(bin, prg, &pa)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to spawn %s: %s", prg[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close file errors we don't handle :) at all.
|
||||||
|
_ = devnullR.Close()
|
||||||
|
_ = devnullW.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanZombies reaps processes spawned by Spawner() and already exited.
|
||||||
|
func CleanZombies() {
|
||||||
|
r := syscall.Rusage{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, _ = syscall.Wait4(-1, nil, 0, &r)
|
||||||
|
time.Sleep(1 * time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
175
stdin.go
Normal file
175
stdin.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClickEvent struct as described at https://i3wm.org/docs/i3bar-protocol.html
|
||||||
|
type ClickEvent struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Instance string `json:"instance,omitempty"`
|
||||||
|
Button int `json:"button"`
|
||||||
|
Modifiers []string `json:"modifiers"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
RelativeX int `json:"relative_x"`
|
||||||
|
RelativeY int `json:"relative_y"`
|
||||||
|
OutputX int `json:"output_x"`
|
||||||
|
OutputY int `json:"output_y"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseStdin tries to parse text that i3bar prints to our stdin. Currently - it is mouse click events on different
|
||||||
|
// area names of i3bar.
|
||||||
|
func ParseStdin() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
// De-facto it is jsonl, except first line is garbage. Also, first symbol in each strint garbage too.
|
||||||
|
firstelem := true
|
||||||
|
|
||||||
|
for {
|
||||||
|
var e ClickEvent
|
||||||
|
|
||||||
|
buf, err := reader.ReadString('\n')
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !firstelem {
|
||||||
|
log.Printf("Unable to read from stdin: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
log.Print("String from stdin has zero length, skipping")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
if strings.HasPrefix(buf, "{") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
log.Print("String from stdin has zero length, skipping")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(buf), &e)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if firstelem {
|
||||||
|
firstelem = false
|
||||||
|
} else {
|
||||||
|
log.Printf("Unable to decode json line from stdin: %s, buf: %s", err, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just in case.
|
||||||
|
if firstelem {
|
||||||
|
firstelem = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock clicks parse.
|
||||||
|
if e.Name == "wallclock" {
|
||||||
|
if Conf.Clock.LeftClick.Enabled && e.Button == 1 {
|
||||||
|
RunChan <- Conf.Clock.LeftClick.Cmd
|
||||||
|
} else if Conf.Clock.RightClick.Enabled && e.Button == 3 {
|
||||||
|
RunChan <- Conf.Clock.RightClick.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Name == "simple-volume-pa" {
|
||||||
|
if Conf.SimpleVolumePa.Enabled {
|
||||||
|
if e.Button == 3 {
|
||||||
|
RunChan <- Conf.SimpleVolumePa.RightClickCmd
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vol, err := pa.Volume()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err := PaReinit(); err != nil {
|
||||||
|
log.Printf("Unable to get pulseaudio volume: %s", err)
|
||||||
|
} else {
|
||||||
|
vol, err = pa.Volume()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to get volume pulseaudio server behaves weirdly: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Button {
|
||||||
|
case Conf.SimpleVolumePa.WheelUp:
|
||||||
|
vol += float32(Conf.SimpleVolumePa.Step) / 100
|
||||||
|
|
||||||
|
if vol > (float32(Conf.SimpleVolumePa.MaxVolumeLimit) / 100) {
|
||||||
|
vol = float32(Conf.SimpleVolumePa.MaxVolumeLimit) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pa.SetVolume(vol); err != nil {
|
||||||
|
log.Printf("Unable to set pulseaudio volume: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Conf.SimpleVolumePa.WheelDown:
|
||||||
|
vol -= float32(Conf.SimpleVolumePa.Step) / 100
|
||||||
|
|
||||||
|
if vol < 0 {
|
||||||
|
vol = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pa.SetVolume(vol); err != nil {
|
||||||
|
log.Printf("Unable to set pulseaudio volume: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Conf.AppButtons {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range Conf.Apps {
|
||||||
|
switch {
|
||||||
|
case app.Name != "" && app.Instance != "":
|
||||||
|
if app.Name == e.Name && app.Instance == e.Instance {
|
||||||
|
prg := append([]string{}, app.Cmd)
|
||||||
|
prg = append(prg, app.Args...)
|
||||||
|
|
||||||
|
RunChan <- prg
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case app.Name != "" && e.Name == app.Name:
|
||||||
|
prg := append([]string{}, app.Cmd)
|
||||||
|
prg = append(prg, app.Args...)
|
||||||
|
|
||||||
|
RunChan <- prg
|
||||||
|
|
||||||
|
case app.Instance != "" && e.Instance == app.Instance:
|
||||||
|
prg := append([]string{}, app.Cmd)
|
||||||
|
prg = append(prg, app.Args...)
|
||||||
|
|
||||||
|
RunChan <- prg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
vendor/github.com/BurntSushi/xgb/.gitignore
generated
vendored
Normal file
2
vendor/github.com/BurntSushi/xgb/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
xgbgen/xgbgen
|
||||||
|
.*.swp
|
||||||
18
vendor/github.com/BurntSushi/xgb/AUTHORS
generated
vendored
Normal file
18
vendor/github.com/BurntSushi/xgb/AUTHORS
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Andrew Gallant is the maintainer of this fork. What follows is the original
|
||||||
|
list of authors for the x-go-binding.
|
||||||
|
|
||||||
|
# This is the official list of XGB authors for copyright purposes.
|
||||||
|
# This file is distinct from the CONTRIBUTORS files.
|
||||||
|
# See the latter for an explanation.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name or Organization <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Anthony Martin <ality@pbrane.org>
|
||||||
|
Firmansyah Adiputra <frm.adiputra@gmail.com>
|
||||||
|
Google Inc.
|
||||||
|
Scott Lawrence <bytbox@gmail.com>
|
||||||
|
Tor Andersson <tor.andersson@gmail.com>
|
||||||
39
vendor/github.com/BurntSushi/xgb/CONTRIBUTORS
generated
vendored
Normal file
39
vendor/github.com/BurntSushi/xgb/CONTRIBUTORS
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
Andrew Gallant is the maintainer of this fork. What follows is the original
|
||||||
|
list of contributors for the x-go-binding.
|
||||||
|
|
||||||
|
# This is the official list of people who can contribute
|
||||||
|
# (and typically have contributed) code to the XGB repository.
|
||||||
|
# The AUTHORS file lists the copyright holders; this file
|
||||||
|
# lists people. For example, Google employees are listed here
|
||||||
|
# but not in AUTHORS, because Google holds the copyright.
|
||||||
|
#
|
||||||
|
# The submission process automatically checks to make sure
|
||||||
|
# that people submitting code are listed in this file (by email address).
|
||||||
|
#
|
||||||
|
# Names should be added to this file only after verifying that
|
||||||
|
# the individual or the individual's organization has agreed to
|
||||||
|
# the appropriate Contributor License Agreement, found here:
|
||||||
|
#
|
||||||
|
# http://code.google.com/legal/individual-cla-v1.0.html
|
||||||
|
# http://code.google.com/legal/corporate-cla-v1.0.html
|
||||||
|
#
|
||||||
|
# The agreement for individuals can be filled out on the web.
|
||||||
|
#
|
||||||
|
# When adding J Random Contributor's name to this file,
|
||||||
|
# either J's name or J's organization's name should be
|
||||||
|
# added to the AUTHORS file, depending on whether the
|
||||||
|
# individual or corporate CLA was used.
|
||||||
|
|
||||||
|
# Names should be added to this file like so:
|
||||||
|
# Name <email address>
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Anthony Martin <ality@pbrane.org>
|
||||||
|
Firmansyah Adiputra <frm.adiputra@gmail.com>
|
||||||
|
Ian Lance Taylor <iant@golang.org>
|
||||||
|
Nigel Tao <nigeltao@golang.org>
|
||||||
|
Robert Griesemer <gri@golang.org>
|
||||||
|
Russ Cox <rsc@golang.org>
|
||||||
|
Scott Lawrence <bytbox@gmail.com>
|
||||||
|
Tor Andersson <tor.andersson@gmail.com>
|
||||||
42
vendor/github.com/BurntSushi/xgb/LICENSE
generated
vendored
Normal file
42
vendor/github.com/BurntSushi/xgb/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2009 The XGB Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following disclaimer
|
||||||
|
// in the documentation and/or other materials provided with the
|
||||||
|
// distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
//
|
||||||
|
// Subject to the terms and conditions of this License, Google hereby
|
||||||
|
// grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||||
|
// royalty-free, irrevocable (except as stated in this section) patent
|
||||||
|
// license to make, have made, use, offer to sell, sell, import, and
|
||||||
|
// otherwise transfer this implementation of XGB, where such license
|
||||||
|
// applies only to those patent claims licensable by Google that are
|
||||||
|
// necessarily infringed by use of this implementation of XGB. If You
|
||||||
|
// institute patent litigation against any entity (including a
|
||||||
|
// cross-claim or counterclaim in a lawsuit) alleging that this
|
||||||
|
// implementation of XGB or a Contribution incorporated within this
|
||||||
|
// implementation of XGB constitutes direct or contributory patent
|
||||||
|
// infringement, then any patent licenses granted to You under this
|
||||||
|
// License for this implementation of XGB shall terminate as of the date
|
||||||
|
// such litigation is filed.
|
||||||
78
vendor/github.com/BurntSushi/xgb/Makefile
generated
vendored
Normal file
78
vendor/github.com/BurntSushi/xgb/Makefile
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# This Makefile is used by the developer. It is not needed in any way to build
|
||||||
|
# a checkout of the XGB repository.
|
||||||
|
# It will be useful, however, if you are hacking at the code generator.
|
||||||
|
# i.e., after making a change to the code generator, run 'make' in the
|
||||||
|
# xgb directory. This will build xgbgen and regenerate each sub-package.
|
||||||
|
# 'make test' will then run any appropriate tests (just tests xproto right now).
|
||||||
|
# 'make bench' will test a couple of benchmarks.
|
||||||
|
# 'make build-all' will then try to build each extension. This isn't strictly
|
||||||
|
# necessary, but it's a good idea to make sure each sub-package is a valid
|
||||||
|
# Go package.
|
||||||
|
|
||||||
|
# My path to the X protocol XML descriptions.
|
||||||
|
XPROTO=/usr/share/xcb
|
||||||
|
|
||||||
|
# All of the XML files in my /usr/share/xcb directory EXCEPT XKB. -_-
|
||||||
|
# This is intended to build xgbgen and generate Go code for each supported
|
||||||
|
# extension.
|
||||||
|
all: build-xgbgen \
|
||||||
|
bigreq.xml composite.xml damage.xml dpms.xml dri2.xml \
|
||||||
|
ge.xml glx.xml randr.xml record.xml render.xml res.xml \
|
||||||
|
screensaver.xml shape.xml shm.xml xc_misc.xml \
|
||||||
|
xevie.xml xf86dri.xml xf86vidmode.xml xfixes.xml xinerama.xml \
|
||||||
|
xprint.xml xproto.xml xselinux.xml xtest.xml \
|
||||||
|
xvmc.xml xv.xml
|
||||||
|
|
||||||
|
build-xgbgen:
|
||||||
|
(cd xgbgen && go build)
|
||||||
|
|
||||||
|
# Builds each individual sub-package to make sure its valid Go code.
|
||||||
|
build-all: bigreq.b composite.b damage.b dpms.b dri2.b ge.b glx.b randr.b \
|
||||||
|
record.b render.b res.b screensaver.b shape.b shm.b xcmisc.b \
|
||||||
|
xevie.b xf86dri.b xf86vidmode.b xfixes.b xinerama.b \
|
||||||
|
xprint.b xproto.b xselinux.b xtest.b xv.b xvmc.b
|
||||||
|
|
||||||
|
%.b:
|
||||||
|
(cd $* ; go build)
|
||||||
|
|
||||||
|
# Installs each individual sub-package.
|
||||||
|
install: bigreq.i composite.i damage.i dpms.i dri2.i ge.i glx.i randr.i \
|
||||||
|
record.i render.i res.i screensaver.i shape.i shm.i xcmisc.i \
|
||||||
|
xevie.i xf86dri.i xf86vidmode.i xfixes.i xinerama.i \
|
||||||
|
xprint.i xproto.i xselinux.i xtest.i xv.i xvmc.i
|
||||||
|
go install
|
||||||
|
|
||||||
|
%.i:
|
||||||
|
(cd $* ; go install)
|
||||||
|
|
||||||
|
# xc_misc is special because it has an underscore.
|
||||||
|
# There's probably a way to do this better, but Makefiles aren't my strong suit.
|
||||||
|
xc_misc.xml: build-xgbgen
|
||||||
|
mkdir -p xcmisc
|
||||||
|
xgbgen/xgbgen --proto-path $(XPROTO) $(XPROTO)/xc_misc.xml > xcmisc/xcmisc.go
|
||||||
|
|
||||||
|
%.xml: build-xgbgen
|
||||||
|
mkdir -p $*
|
||||||
|
xgbgen/xgbgen --proto-path $(XPROTO) $(XPROTO)/$*.xml > $*/$*.go
|
||||||
|
|
||||||
|
# Just test the xproto core protocol for now.
|
||||||
|
test:
|
||||||
|
(cd xproto ; go test)
|
||||||
|
|
||||||
|
# Force all xproto benchmarks to run and no tests.
|
||||||
|
bench:
|
||||||
|
(cd xproto ; go test -run 'nomatch' -bench '.*' -cpu 1,2,3,6)
|
||||||
|
|
||||||
|
# gofmt all non-auto-generated code.
|
||||||
|
# (auto-generated code is already gofmt'd.)
|
||||||
|
# Also do a column check (80 cols) after a gofmt.
|
||||||
|
# But don't check columns on auto-generated code, since I don't care if they
|
||||||
|
# break 80 cols.
|
||||||
|
gofmt:
|
||||||
|
gofmt -w *.go xgbgen/*.go examples/*.go examples/*/*.go xproto/xproto_test.go
|
||||||
|
colcheck *.go xgbgen/*.go examples/*.go examples/*/*.go xproto/xproto_test.go
|
||||||
|
|
||||||
|
push:
|
||||||
|
git push origin master
|
||||||
|
git push github master
|
||||||
|
|
||||||
59
vendor/github.com/BurntSushi/xgb/README
generated
vendored
Normal file
59
vendor/github.com/BurntSushi/xgb/README
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
Note that this project is largely unmaintained as I don't have the time to do
|
||||||
|
or support more development. Please consider using this fork instead:
|
||||||
|
https://github.com/jezek/xgb
|
||||||
|
|
||||||
|
|
||||||
|
XGB is the X Go Binding, which is a low-level API to communicate with the
|
||||||
|
core X protocol and many of the X extensions. It is closely modeled after
|
||||||
|
XCB and xpyb.
|
||||||
|
|
||||||
|
It is thread safe and gets immediate improvement from parallelism when
|
||||||
|
GOMAXPROCS > 1. (See the benchmarks in xproto/xproto_test.go for evidence.)
|
||||||
|
|
||||||
|
Please see doc.go for more info.
|
||||||
|
|
||||||
|
Note that unless you know you need XGB, you can probably make your life
|
||||||
|
easier by using a slightly higher level library: xgbutil.
|
||||||
|
|
||||||
|
Quick Usage
|
||||||
|
===========
|
||||||
|
go get github.com/BurntSushi/xgb
|
||||||
|
go run go/path/src/github.com/BurntSushi/xgb/examples/create-window/main.go
|
||||||
|
|
||||||
|
BurntSushi's Fork
|
||||||
|
=================
|
||||||
|
I've forked the XGB repository from Google Code due to inactivty upstream.
|
||||||
|
|
||||||
|
Godoc documentation can be found here:
|
||||||
|
https://godoc.org/github.com/BurntSushi/xgb
|
||||||
|
|
||||||
|
Much of the code has been rewritten in an effort to support thread safety
|
||||||
|
and multiple extensions. Namely, go_client.py has been thrown away in favor
|
||||||
|
of an xgbgen package.
|
||||||
|
|
||||||
|
The biggest parts that *haven't* been rewritten by me are the connection and
|
||||||
|
authentication handshakes. They're inherently messy, and there's really no
|
||||||
|
reason to re-work them. The rest of XGB has been completely rewritten.
|
||||||
|
|
||||||
|
I like to release my code under the WTFPL, but since I'm starting with someone
|
||||||
|
else's work, I'm leaving the original license/contributor/author information
|
||||||
|
in tact.
|
||||||
|
|
||||||
|
I suppose I can legitimately release xgbgen under the WTFPL. To be fair, it is
|
||||||
|
at least as complex as XGB itself. *sigh*
|
||||||
|
|
||||||
|
What follows is the original README:
|
||||||
|
|
||||||
|
XGB README
|
||||||
|
==========
|
||||||
|
XGB is the X protocol Go language Binding.
|
||||||
|
|
||||||
|
It is the Go equivalent of XCB, the X protocol C-language Binding
|
||||||
|
(http://xcb.freedesktop.org/).
|
||||||
|
|
||||||
|
Unless otherwise noted, the XGB source files are distributed
|
||||||
|
under the BSD-style license found in the LICENSE file.
|
||||||
|
|
||||||
|
Contributions should follow the same procedure as for the Go project:
|
||||||
|
http://golang.org/doc/contribute.html
|
||||||
|
|
||||||
29
vendor/github.com/BurntSushi/xgb/STYLE
generated
vendored
Normal file
29
vendor/github.com/BurntSushi/xgb/STYLE
generated
vendored
Normal file
@@ -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.
|
||||||
|
|
||||||
110
vendor/github.com/BurntSushi/xgb/auth.go
generated
vendored
Normal file
110
vendor/github.com/BurntSushi/xgb/auth.go
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
/*
|
||||||
|
auth.go contains functions to facilitate the parsing of .Xauthority files.
|
||||||
|
|
||||||
|
It is largely unmodified from the original XGB package that I forked.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readAuthority reads the X authority file for the DISPLAY.
|
||||||
|
// If hostname == "" or hostname == "localhost",
|
||||||
|
// then use the system's hostname (as returned by os.Hostname) instead.
|
||||||
|
func readAuthority(hostname, display string) (
|
||||||
|
name string, data []byte, err error) {
|
||||||
|
|
||||||
|
// b is a scratch buffer to use and should be at least 256 bytes long
|
||||||
|
// (i.e. it should be able to hold a hostname).
|
||||||
|
b := make([]byte, 256)
|
||||||
|
|
||||||
|
// As per /usr/include/X11/Xauth.h.
|
||||||
|
const familyLocal = 256
|
||||||
|
const familyWild = 65535
|
||||||
|
|
||||||
|
if len(hostname) == 0 || hostname == "localhost" {
|
||||||
|
hostname, err = os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fname := os.Getenv("XAUTHORITY")
|
||||||
|
if len(fname) == 0 {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if len(home) == 0 {
|
||||||
|
err = errors.New("Xauthority not found: $XAUTHORITY, $HOME not set")
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
fname = home + "/.Xauthority"
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var family uint16
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &family); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := getString(r, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
disp, err := getString(r, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name0, err := getString(r, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data0, err := getBytes(r, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrmatch := (family == familyWild) ||
|
||||||
|
(family == familyLocal && addr == hostname)
|
||||||
|
dispmatch := (disp == "") || (disp == display)
|
||||||
|
|
||||||
|
if addrmatch && dispmatch {
|
||||||
|
return name0, data0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBytes(r io.Reader, b []byte) ([]byte, error) {
|
||||||
|
var n uint16
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if n > uint16(len(b)) {
|
||||||
|
return nil, errors.New("bytes too long for buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(r, b[0:n]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b[0:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getString(r io.Reader, b []byte) (string, error) {
|
||||||
|
b, err := getBytes(r, b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
186
vendor/github.com/BurntSushi/xgb/conn.go
generated
vendored
Normal file
186
vendor/github.com/BurntSushi/xgb/conn.go
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
/*
|
||||||
|
conn.go contains a couple of functions that do some real dirty work related
|
||||||
|
to the initial connection handshake with X.
|
||||||
|
|
||||||
|
This code is largely unmodified from the original XGB package that I forked.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connect connects to the X server given in the 'display' string,
|
||||||
|
// and does all the necessary setup handshaking.
|
||||||
|
// If 'display' is empty it will be taken from os.Getenv("DISPLAY").
|
||||||
|
// Note that you should read and understand the "Connection Setup" of the
|
||||||
|
// X Protocol Reference Manual before changing this function:
|
||||||
|
// http://goo.gl/4zGQg
|
||||||
|
func (c *Conn) connect(display string) error {
|
||||||
|
err := c.dial(display)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.postConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect init from to the net.Conn,
|
||||||
|
func (c *Conn) connectNet(netConn net.Conn) error {
|
||||||
|
c.conn = netConn
|
||||||
|
return c.postConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the postConnect action after Conn get it's underly net.Conn
|
||||||
|
func (c *Conn) postConnect() error {
|
||||||
|
// Get authentication data
|
||||||
|
authName, authData, err := readAuthority(c.host, c.display)
|
||||||
|
noauth := false
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("Could not get authority info: %v", err)
|
||||||
|
Logger.Println("Trying connection without authority info...")
|
||||||
|
authName = ""
|
||||||
|
authData = []byte{}
|
||||||
|
noauth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume that the authentication protocol is "MIT-MAGIC-COOKIE-1".
|
||||||
|
if !noauth && (authName != "MIT-MAGIC-COOKIE-1" || len(authData) != 16) {
|
||||||
|
return errors.New("unsupported auth protocol " + authName)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 12+Pad(len(authName))+Pad(len(authData)))
|
||||||
|
buf[0] = 0x6c
|
||||||
|
buf[1] = 0
|
||||||
|
Put16(buf[2:], 11)
|
||||||
|
Put16(buf[4:], 0)
|
||||||
|
Put16(buf[6:], uint16(len(authName)))
|
||||||
|
Put16(buf[8:], uint16(len(authData)))
|
||||||
|
Put16(buf[10:], 0)
|
||||||
|
copy(buf[12:], []byte(authName))
|
||||||
|
copy(buf[12+Pad(len(authName)):], authData)
|
||||||
|
if _, err = c.conn.Write(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
head := make([]byte, 8)
|
||||||
|
if _, err = io.ReadFull(c.conn, head[0:8]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code := head[0]
|
||||||
|
reasonLen := head[1]
|
||||||
|
major := Get16(head[2:])
|
||||||
|
minor := Get16(head[4:])
|
||||||
|
dataLen := Get16(head[6:])
|
||||||
|
|
||||||
|
if major != 11 || minor != 0 {
|
||||||
|
return fmt.Errorf("x protocol version mismatch: %d.%d", major, minor)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = make([]byte, int(dataLen)*4+8, int(dataLen)*4+8)
|
||||||
|
copy(buf, head)
|
||||||
|
if _, err = io.ReadFull(c.conn, buf[8:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == 0 {
|
||||||
|
reason := buf[8 : 8+reasonLen]
|
||||||
|
return fmt.Errorf("x protocol authentication refused: %s",
|
||||||
|
string(reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfortunately, it isn't really feasible to read the setup bytes here,
|
||||||
|
// since the code to do so is in a different package.
|
||||||
|
// Users must call 'xproto.Setup(X)' to get the setup info.
|
||||||
|
c.SetupBytes = buf
|
||||||
|
|
||||||
|
// But also read stuff that we *need* to get started.
|
||||||
|
c.setupResourceIdBase = Get32(buf[12:])
|
||||||
|
c.setupResourceIdMask = Get32(buf[16:])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dial initializes the actual net connection with X.
|
||||||
|
func (c *Conn) dial(display string) error {
|
||||||
|
if len(display) == 0 {
|
||||||
|
display = os.Getenv("DISPLAY")
|
||||||
|
}
|
||||||
|
|
||||||
|
display0 := display
|
||||||
|
if len(display) == 0 {
|
||||||
|
return errors.New("empty display string")
|
||||||
|
}
|
||||||
|
|
||||||
|
colonIdx := strings.LastIndex(display, ":")
|
||||||
|
if colonIdx < 0 {
|
||||||
|
return errors.New("bad display string: " + display0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol, socket string
|
||||||
|
|
||||||
|
if display[0] == '/' {
|
||||||
|
socket = display[0:colonIdx]
|
||||||
|
} else {
|
||||||
|
slashIdx := strings.LastIndex(display, "/")
|
||||||
|
if slashIdx >= 0 {
|
||||||
|
protocol = display[0:slashIdx]
|
||||||
|
c.host = display[slashIdx+1 : colonIdx]
|
||||||
|
} else {
|
||||||
|
c.host = display[0:colonIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display = display[colonIdx+1 : len(display)]
|
||||||
|
if len(display) == 0 {
|
||||||
|
return errors.New("bad display string: " + display0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scr string
|
||||||
|
dotIdx := strings.LastIndex(display, ".")
|
||||||
|
if dotIdx < 0 {
|
||||||
|
c.display = display[0:]
|
||||||
|
} else {
|
||||||
|
c.display = display[0:dotIdx]
|
||||||
|
scr = display[dotIdx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
c.DisplayNumber, err = strconv.Atoi(c.display)
|
||||||
|
if err != nil || c.DisplayNumber < 0 {
|
||||||
|
return errors.New("bad display string: " + display0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scr) != 0 {
|
||||||
|
c.DefaultScreen, err = strconv.Atoi(scr)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("bad display string: " + display0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
if len(socket) != 0 {
|
||||||
|
c.conn, err = net.Dial("unix", socket+":"+c.display)
|
||||||
|
} else if len(c.host) != 0 && c.host != "unix" {
|
||||||
|
if protocol == "" {
|
||||||
|
protocol = "tcp"
|
||||||
|
}
|
||||||
|
c.conn, err = net.Dial(protocol,
|
||||||
|
c.host+":"+strconv.Itoa(6000+c.DisplayNumber))
|
||||||
|
} else {
|
||||||
|
c.host = ""
|
||||||
|
c.conn, err = net.Dial("unix", "/tmp/.X11-unix/X"+c.display)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("cannot connect to " + display0 + ": " + err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
165
vendor/github.com/BurntSushi/xgb/cookie.go
generated
vendored
Normal file
165
vendor/github.com/BurntSushi/xgb/cookie.go
generated
vendored
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cookie is the internal representation of a cookie, where one is generated
|
||||||
|
// for *every* request sent by XGB.
|
||||||
|
// 'cookie' is most frequently used by embedding it into a more specific
|
||||||
|
// kind of cookie, i.e., 'GetInputFocusCookie'.
|
||||||
|
type Cookie struct {
|
||||||
|
conn *Conn
|
||||||
|
Sequence uint16
|
||||||
|
replyChan chan []byte
|
||||||
|
errorChan chan error
|
||||||
|
pingChan chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCookie creates a new cookie with the correct channels initialized
|
||||||
|
// depending upon the values of 'checked' and 'reply'. Together, there are
|
||||||
|
// four different kinds of cookies. (See more detailed comments in the
|
||||||
|
// function for more info on those.)
|
||||||
|
// Note that a sequence number is not set until just before the request
|
||||||
|
// corresponding to this cookie is sent over the wire.
|
||||||
|
//
|
||||||
|
// Unless you're building requests from bytes by hand, this method should
|
||||||
|
// not be used.
|
||||||
|
func (c *Conn) NewCookie(checked, reply bool) *Cookie {
|
||||||
|
cookie := &Cookie{
|
||||||
|
conn: c,
|
||||||
|
Sequence: 0, // we add the sequence id just before sending a request
|
||||||
|
replyChan: nil,
|
||||||
|
errorChan: nil,
|
||||||
|
pingChan: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are four different kinds of cookies:
|
||||||
|
// Checked requests with replies get a reply channel and an error channel.
|
||||||
|
// Unchecked requests with replies get a reply channel and a ping channel.
|
||||||
|
// Checked requests w/o replies get a ping channel and an error channel.
|
||||||
|
// Unchecked requests w/o replies get no channels.
|
||||||
|
// The reply channel is used to send reply data.
|
||||||
|
// The error channel is used to send error data.
|
||||||
|
// The ping channel is used when one of the 'reply' or 'error' channels
|
||||||
|
// is missing but the other is present. The ping channel is way to force
|
||||||
|
// the blocking to stop and basically say "the error has been received
|
||||||
|
// in the main event loop" (when the ping channel is coupled with a reply
|
||||||
|
// channel) or "the request you made that has no reply was successful"
|
||||||
|
// (when the ping channel is coupled with an error channel).
|
||||||
|
if checked {
|
||||||
|
cookie.errorChan = make(chan error, 1)
|
||||||
|
if !reply {
|
||||||
|
cookie.pingChan = make(chan bool, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reply {
|
||||||
|
cookie.replyChan = make(chan []byte, 1)
|
||||||
|
if !checked {
|
||||||
|
cookie.pingChan = make(chan bool, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply detects whether this is a checked or unchecked cookie, and calls
|
||||||
|
// 'replyChecked' or 'replyUnchecked' appropriately.
|
||||||
|
//
|
||||||
|
// Unless you're building requests from bytes by hand, this method should
|
||||||
|
// not be used.
|
||||||
|
func (c Cookie) Reply() ([]byte, error) {
|
||||||
|
// checked
|
||||||
|
if c.errorChan != nil {
|
||||||
|
return c.replyChecked()
|
||||||
|
}
|
||||||
|
return c.replyUnchecked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// replyChecked waits for a response on either the replyChan or errorChan
|
||||||
|
// channels. If the former arrives, the bytes are returned with a nil error.
|
||||||
|
// If the latter arrives, no bytes are returned (nil) and the error received
|
||||||
|
// is returned.
|
||||||
|
//
|
||||||
|
// Unless you're building requests from bytes by hand, this method should
|
||||||
|
// not be used.
|
||||||
|
func (c Cookie) replyChecked() ([]byte, error) {
|
||||||
|
if c.replyChan == nil {
|
||||||
|
return nil, errors.New("Cannot call 'replyChecked' on a cookie that " +
|
||||||
|
"is not expecting a *reply* or an error.")
|
||||||
|
}
|
||||||
|
if c.errorChan == nil {
|
||||||
|
return nil, errors.New("Cannot call 'replyChecked' on a cookie that " +
|
||||||
|
"is not expecting a reply or an *error*.")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case reply := <-c.replyChan:
|
||||||
|
return reply, nil
|
||||||
|
case err := <-c.errorChan:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replyUnchecked waits for a response on either the replyChan or pingChan
|
||||||
|
// channels. If the former arrives, the bytes are returned with a nil error.
|
||||||
|
// If the latter arrives, no bytes are returned (nil) and a nil error
|
||||||
|
// is returned. (In the latter case, the corresponding error can be retrieved
|
||||||
|
// from (Wait|Poll)ForEvent asynchronously.)
|
||||||
|
// In all honesty, you *probably* don't want to use this method.
|
||||||
|
//
|
||||||
|
// Unless you're building requests from bytes by hand, this method should
|
||||||
|
// not be used.
|
||||||
|
func (c Cookie) replyUnchecked() ([]byte, error) {
|
||||||
|
if c.replyChan == nil {
|
||||||
|
return nil, errors.New("Cannot call 'replyUnchecked' on a cookie " +
|
||||||
|
"that is not expecting a *reply*.")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case reply := <-c.replyChan:
|
||||||
|
return reply, nil
|
||||||
|
case <-c.pingChan:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is used for checked requests that have no replies. It is a mechanism
|
||||||
|
// by which to report "success" or "error" in a synchronous fashion. (Therefore,
|
||||||
|
// unchecked requests without replies cannot use this method.)
|
||||||
|
// If the request causes an error, it is sent to this cookie's errorChan.
|
||||||
|
// If the request was successful, there is no response from the server.
|
||||||
|
// Thus, pingChan is sent a value when the *next* reply is read.
|
||||||
|
// If no more replies are being processed, we force a round trip request with
|
||||||
|
// GetInputFocus.
|
||||||
|
//
|
||||||
|
// Unless you're building requests from bytes by hand, this method should
|
||||||
|
// not be used.
|
||||||
|
func (c Cookie) Check() error {
|
||||||
|
if c.replyChan != nil {
|
||||||
|
return errors.New("Cannot call 'Check' on a cookie that is " +
|
||||||
|
"expecting a *reply*. Use 'Reply' instead.")
|
||||||
|
}
|
||||||
|
if c.errorChan == nil {
|
||||||
|
return errors.New("Cannot call 'Check' on a cookie that is " +
|
||||||
|
"not expecting a possible *error*.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First do a quick non-blocking check to see if we've been pinged.
|
||||||
|
select {
|
||||||
|
case err := <-c.errorChan:
|
||||||
|
return err
|
||||||
|
case <-c.pingChan:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now force a round trip and try again, but block this time.
|
||||||
|
c.conn.Sync()
|
||||||
|
select {
|
||||||
|
case err := <-c.errorChan:
|
||||||
|
return err
|
||||||
|
case <-c.pingChan:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
146
vendor/github.com/BurntSushi/xgb/doc.go
generated
vendored
Normal file
146
vendor/github.com/BurntSushi/xgb/doc.go
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
Package XGB provides the X Go Binding, which is a low-level API to communicate
|
||||||
|
with the core X protocol and many of the X extensions.
|
||||||
|
|
||||||
|
It is *very* closely modeled on XCB, so that experience with XCB (or xpyb) is
|
||||||
|
easily translatable to XGB. That is, it uses the same cookie/reply model
|
||||||
|
and is thread safe. There are otherwise no major differences (in the API).
|
||||||
|
|
||||||
|
Most uses of XGB typically fall under the realm of window manager and GUI kit
|
||||||
|
development, but other applications (like pagers, panels, tilers, etc.) may
|
||||||
|
also require XGB. Moreover, it is a near certainty that if you need to work
|
||||||
|
with X, xgbutil will be of great use to you as well:
|
||||||
|
https://github.com/BurntSushi/xgbutil
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
This is an extremely terse example that demonstrates how to connect to X,
|
||||||
|
create a window, listen to StructureNotify events and Key{Press,Release}
|
||||||
|
events, map the window, and print out all events received. An example with
|
||||||
|
accompanying documentation can be found in examples/create-window.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/BurntSushi/xgb"
|
||||||
|
"github.com/BurntSushi/xgb/xproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
X, err := xgb.NewConn()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wid, _ := xproto.NewWindowId(X)
|
||||||
|
screen := xproto.Setup(X).DefaultScreen(X)
|
||||||
|
xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root,
|
||||||
|
0, 0, 500, 500, 0,
|
||||||
|
xproto.WindowClassInputOutput, screen.RootVisual,
|
||||||
|
xproto.CwBackPixel | xproto.CwEventMask,
|
||||||
|
[]uint32{ // values must be in the order defined by the protocol
|
||||||
|
0xffffffff,
|
||||||
|
xproto.EventMaskStructureNotify |
|
||||||
|
xproto.EventMaskKeyPress |
|
||||||
|
xproto.EventMaskKeyRelease})
|
||||||
|
|
||||||
|
xproto.MapWindow(X, wid)
|
||||||
|
for {
|
||||||
|
ev, xerr := X.WaitForEvent()
|
||||||
|
if ev == nil && xerr == nil {
|
||||||
|
fmt.Println("Both event and error are nil. Exiting...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev != nil {
|
||||||
|
fmt.Printf("Event: %s\n", ev)
|
||||||
|
}
|
||||||
|
if xerr != nil {
|
||||||
|
fmt.Printf("Error: %s\n", xerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Xinerama Example
|
||||||
|
|
||||||
|
This is another small example that shows how to query Xinerama for geometry
|
||||||
|
information of each active head. Accompanying documentation for this example
|
||||||
|
can be found in examples/xinerama.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"github.com/BurntSushi/xgb"
|
||||||
|
"github.com/BurntSushi/xgb/xinerama"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
X, err := xgb.NewConn()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the Xinerama extension.
|
||||||
|
// The appropriate 'Init' function must be run for *every*
|
||||||
|
// extension before any of its requests can be used.
|
||||||
|
err = xinerama.Init(X)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := xinerama.QueryScreens(X).Reply()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Number of heads: %d\n", reply.Number)
|
||||||
|
for i, screen := range reply.ScreenInfo {
|
||||||
|
fmt.Printf("%d :: X: %d, Y: %d, Width: %d, Height: %d\n",
|
||||||
|
i, screen.XOrg, screen.YOrg, screen.Width, screen.Height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Parallelism
|
||||||
|
|
||||||
|
XGB can benefit greatly from parallelism due to its concurrent design. For
|
||||||
|
evidence of this claim, please see the benchmarks in xproto/xproto_test.go.
|
||||||
|
|
||||||
|
Tests
|
||||||
|
|
||||||
|
xproto/xproto_test.go contains a number of contrived tests that stress
|
||||||
|
particular corners of XGB that I presume could be problem areas. Namely:
|
||||||
|
requests with no replies, requests with replies, checked errors, unchecked
|
||||||
|
errors, sequence number wrapping, cookie buffer flushing (i.e., forcing a round
|
||||||
|
trip every N requests made that don't have a reply), getting/setting properties
|
||||||
|
and creating a window and listening to StructureNotify events.
|
||||||
|
|
||||||
|
Code Generator
|
||||||
|
|
||||||
|
Both XCB and xpyb use the same Python module (xcbgen) for a code generator. XGB
|
||||||
|
(before this fork) used the same code generator as well, but in my attempt to
|
||||||
|
add support for more extensions, I found the code generator extremely difficult
|
||||||
|
to work with. Therefore, I re-wrote the code generator in Go. It can be found
|
||||||
|
in its own sub-package, xgbgen, of xgb. My design of xgbgen includes a rough
|
||||||
|
consideration that it could be used for other languages.
|
||||||
|
|
||||||
|
What works
|
||||||
|
|
||||||
|
I am reasonably confident that the core X protocol is in full working form. I've
|
||||||
|
also tested the Xinerama and RandR extensions sparingly. Many of the other
|
||||||
|
existing extensions have Go source generated (and are compilable) and are
|
||||||
|
included in this package, but I am currently unsure of their status. They
|
||||||
|
*should* work.
|
||||||
|
|
||||||
|
What does not work
|
||||||
|
|
||||||
|
XKB is the only extension that intentionally does not work, although I suspect
|
||||||
|
that GLX also does not work (however, there is Go source code for GLX that
|
||||||
|
compiles, unlike XKB). I don't currently have any intention of getting XKB
|
||||||
|
working, due to its complexity and my current mental incapacity to test it.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package xgb
|
||||||
105
vendor/github.com/BurntSushi/xgb/help.go
generated
vendored
Normal file
105
vendor/github.com/BurntSushi/xgb/help.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
/*
|
||||||
|
help.go is meant to contain a rough hodge podge of functions that are mainly
|
||||||
|
used in the auto generated code. Indeed, several functions here are simple
|
||||||
|
wrappers so that the sub-packages don't need to be smart about which stdlib
|
||||||
|
packages to import.
|
||||||
|
|
||||||
|
Also, the 'Get..' and 'Put..' functions are used through the core xgb package
|
||||||
|
too. (xgbutil uses them too.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringsJoin is an alias to strings.Join. It allows us to avoid having to
|
||||||
|
// import 'strings' in each of the generated Go files.
|
||||||
|
func StringsJoin(ss []string, sep string) string {
|
||||||
|
return strings.Join(ss, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is so we don't need to import 'fmt' in the generated Go files.
|
||||||
|
func Sprintf(format string, v ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf is just a wrapper for fmt.Errorf. Exists for the same reason
|
||||||
|
// that 'stringsJoin' and 'sprintf' exists.
|
||||||
|
func Errorf(format string, v ...interface{}) error {
|
||||||
|
return fmt.Errorf(format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad a length to align on 4 bytes.
|
||||||
|
func Pad(n int) int {
|
||||||
|
return (n + 3) & ^3
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopCount counts the number of bits set in a value list mask.
|
||||||
|
func PopCount(mask0 int) int {
|
||||||
|
mask := uint32(mask0)
|
||||||
|
n := 0
|
||||||
|
for i := uint32(0); i < 32; i++ {
|
||||||
|
if mask&(1<<i) != 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put16 takes a 16 bit integer and copies it into a byte slice.
|
||||||
|
func Put16(buf []byte, v uint16) {
|
||||||
|
buf[0] = byte(v)
|
||||||
|
buf[1] = byte(v >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put32 takes a 32 bit integer and copies it into a byte slice.
|
||||||
|
func Put32(buf []byte, v uint32) {
|
||||||
|
buf[0] = byte(v)
|
||||||
|
buf[1] = byte(v >> 8)
|
||||||
|
buf[2] = byte(v >> 16)
|
||||||
|
buf[3] = byte(v >> 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put64 takes a 64 bit integer and copies it into a byte slice.
|
||||||
|
func Put64(buf []byte, v uint64) {
|
||||||
|
buf[0] = byte(v)
|
||||||
|
buf[1] = byte(v >> 8)
|
||||||
|
buf[2] = byte(v >> 16)
|
||||||
|
buf[3] = byte(v >> 24)
|
||||||
|
buf[4] = byte(v >> 32)
|
||||||
|
buf[5] = byte(v >> 40)
|
||||||
|
buf[6] = byte(v >> 48)
|
||||||
|
buf[7] = byte(v >> 56)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get16 constructs a 16 bit integer from the beginning of a byte slice.
|
||||||
|
func Get16(buf []byte) uint16 {
|
||||||
|
v := uint16(buf[0])
|
||||||
|
v |= uint16(buf[1]) << 8
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get32 constructs a 32 bit integer from the beginning of a byte slice.
|
||||||
|
func Get32(buf []byte) uint32 {
|
||||||
|
v := uint32(buf[0])
|
||||||
|
v |= uint32(buf[1]) << 8
|
||||||
|
v |= uint32(buf[2]) << 16
|
||||||
|
v |= uint32(buf[3]) << 24
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get64 constructs a 64 bit integer from the beginning of a byte slice.
|
||||||
|
func Get64(buf []byte) uint64 {
|
||||||
|
v := uint64(buf[0])
|
||||||
|
v |= uint64(buf[1]) << 8
|
||||||
|
v |= uint64(buf[2]) << 16
|
||||||
|
v |= uint64(buf[3]) << 24
|
||||||
|
v |= uint64(buf[4]) << 32
|
||||||
|
v |= uint64(buf[5]) << 40
|
||||||
|
v |= uint64(buf[6]) << 48
|
||||||
|
v |= uint64(buf[7]) << 56
|
||||||
|
return v
|
||||||
|
}
|
||||||
29
vendor/github.com/BurntSushi/xgb/sync.go
generated
vendored
Normal file
29
vendor/github.com/BurntSushi/xgb/sync.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
// Sync sends a round trip request and waits for the response.
|
||||||
|
// This forces all pending cookies to be dealt with.
|
||||||
|
// You actually shouldn't need to use this like you might with Xlib. Namely,
|
||||||
|
// buffers are automatically flushed using Go's channels and round trip requests
|
||||||
|
// are forced where appropriate automatically.
|
||||||
|
func (c *Conn) Sync() {
|
||||||
|
cookie := c.NewCookie(true, true)
|
||||||
|
c.NewRequest(c.getInputFocusRequest(), cookie)
|
||||||
|
cookie.Reply() // wait for the buffer to clear
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInputFocusRequest writes the raw bytes to a buffer.
|
||||||
|
// It is duplicated from xproto/xproto.go.
|
||||||
|
func (c *Conn) getInputFocusRequest() []byte {
|
||||||
|
size := 4
|
||||||
|
b := 0
|
||||||
|
buf := make([]byte, size)
|
||||||
|
|
||||||
|
buf[b] = 43 // request opcode
|
||||||
|
b += 1
|
||||||
|
|
||||||
|
b += 1 // padding
|
||||||
|
Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
|
||||||
|
b += 2
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
554
vendor/github.com/BurntSushi/xgb/xgb.go
generated
vendored
Normal file
554
vendor/github.com/BurntSushi/xgb/xgb.go
generated
vendored
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
package xgb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Where to log error-messages. Defaults to stderr.
|
||||||
|
// To disable logging, just set this to log.New(ioutil.Discard, "", 0)
|
||||||
|
Logger = log.New(os.Stderr, "XGB: ", log.Lshortfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// cookieBuffer represents the queue size of cookies existing at any
|
||||||
|
// point in time. The size of the buffer is really only important when
|
||||||
|
// there are many requests without replies made in sequence. Once the
|
||||||
|
// buffer fills, a round trip request is made to clear the buffer.
|
||||||
|
cookieBuffer = 1000
|
||||||
|
|
||||||
|
// xidBuffer represents the queue size of the xid channel.
|
||||||
|
// I don't think this value matters much, since xid generation is not
|
||||||
|
// that expensive.
|
||||||
|
xidBuffer = 5
|
||||||
|
|
||||||
|
// seqBuffer represents the queue size of the sequence number channel.
|
||||||
|
// I don't think this value matters much, since sequence number generation
|
||||||
|
// is not that expensive.
|
||||||
|
seqBuffer = 5
|
||||||
|
|
||||||
|
// reqBuffer represents the queue size of the number of requests that
|
||||||
|
// can be made until new ones block. This value seems OK.
|
||||||
|
reqBuffer = 100
|
||||||
|
|
||||||
|
// eventBuffer represents the queue size of the number of events or errors
|
||||||
|
// that can be loaded off the wire and not grabbed with WaitForEvent
|
||||||
|
// until reading an event blocks. This value should be big enough to handle
|
||||||
|
// bursts of events.
|
||||||
|
eventBuffer = 5000
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Conn represents a connection to an X server.
|
||||||
|
type Conn struct {
|
||||||
|
host string
|
||||||
|
conn net.Conn
|
||||||
|
display string
|
||||||
|
DisplayNumber int
|
||||||
|
DefaultScreen int
|
||||||
|
SetupBytes []byte
|
||||||
|
|
||||||
|
setupResourceIdBase uint32
|
||||||
|
setupResourceIdMask uint32
|
||||||
|
|
||||||
|
eventChan chan eventOrError
|
||||||
|
cookieChan chan *Cookie
|
||||||
|
xidChan chan xid
|
||||||
|
seqChan chan uint16
|
||||||
|
reqChan chan *request
|
||||||
|
closing chan chan struct{}
|
||||||
|
|
||||||
|
// ExtLock is a lock used whenever new extensions are initialized.
|
||||||
|
// It should not be used. It is exported for use in the extension
|
||||||
|
// sub-packages.
|
||||||
|
ExtLock sync.RWMutex
|
||||||
|
|
||||||
|
// Extensions is a map from extension name to major opcode. It should
|
||||||
|
// not be used. It is exported for use in the extension sub-packages.
|
||||||
|
Extensions map[string]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn creates a new connection instance. It initializes locks, data
|
||||||
|
// structures, and performs the initial handshake. (The code for the handshake
|
||||||
|
// has been relegated to conn.go.)
|
||||||
|
func NewConn() (*Conn, error) {
|
||||||
|
return NewConnDisplay("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnDisplay is just like NewConn, but allows a specific DISPLAY
|
||||||
|
// string to be used.
|
||||||
|
// If 'display' is empty it will be taken from os.Getenv("DISPLAY").
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// 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) (*Conn, error) {
|
||||||
|
conn := &Conn{}
|
||||||
|
|
||||||
|
// First connect. This reads authority, checks DISPLAY environment
|
||||||
|
// variable, and loads the initial Setup info.
|
||||||
|
err := conn.connect(display)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return postNewConn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnDisplay is just like NewConn, but allows a specific net.Conn
|
||||||
|
// to be used.
|
||||||
|
func NewConnNet(netConn net.Conn) (*Conn, error) {
|
||||||
|
conn := &Conn{}
|
||||||
|
|
||||||
|
// First connect. This reads authority, checks DISPLAY environment
|
||||||
|
// variable, and loads the initial Setup info.
|
||||||
|
err := conn.connectNet(netConn)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return postNewConn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postNewConn(conn *Conn) (*Conn, error) {
|
||||||
|
conn.Extensions = make(map[string]byte)
|
||||||
|
|
||||||
|
conn.cookieChan = make(chan *Cookie, cookieBuffer)
|
||||||
|
conn.xidChan = make(chan xid, xidBuffer)
|
||||||
|
conn.seqChan = make(chan uint16, seqBuffer)
|
||||||
|
conn.reqChan = make(chan *request, reqBuffer)
|
||||||
|
conn.eventChan = make(chan eventOrError, eventBuffer)
|
||||||
|
conn.closing = make(chan chan struct{}, 1)
|
||||||
|
|
||||||
|
go conn.generateXIds()
|
||||||
|
go conn.generateSeqIds()
|
||||||
|
go conn.sendRequests()
|
||||||
|
go conn.readResponses()
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close gracefully closes the connection to the X server.
|
||||||
|
func (c *Conn) Close() {
|
||||||
|
close(c.reqChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event is an interface that can contain any of the events returned by the
|
||||||
|
// server. Use a type assertion switch to extract the Event structs.
|
||||||
|
type Event interface {
|
||||||
|
Bytes() []byte
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventFun is the type of function use to construct events from raw bytes.
|
||||||
|
// It should not be used. It is exported for use in the extension sub-packages.
|
||||||
|
type NewEventFun func(buf []byte) Event
|
||||||
|
|
||||||
|
// NewEventFuncs is a map from event numbers to functions that create
|
||||||
|
// the corresponding event. It should not be used. It is exported for use
|
||||||
|
// in the extension sub-packages.
|
||||||
|
var NewEventFuncs = make(map[int]NewEventFun)
|
||||||
|
|
||||||
|
// NewExtEventFuncs is a temporary map that stores event constructor functions
|
||||||
|
// for each extension. When an extension is initialized, each event for that
|
||||||
|
// extension is added to the 'NewEventFuncs' map. It should not be used. It is
|
||||||
|
// exported for use in the extension sub-packages.
|
||||||
|
var NewExtEventFuncs = make(map[string]map[int]NewEventFun)
|
||||||
|
|
||||||
|
// Error is an interface that can contain any of the errors returned by
|
||||||
|
// the server. Use a type assertion switch to extract the Error structs.
|
||||||
|
type Error interface {
|
||||||
|
SequenceId() uint16
|
||||||
|
BadId() uint32
|
||||||
|
Error() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorFun is the type of function use to construct errors from raw bytes.
|
||||||
|
// It should not be used. It is exported for use in the extension sub-packages.
|
||||||
|
type NewErrorFun func(buf []byte) Error
|
||||||
|
|
||||||
|
// NewErrorFuncs is a map from error numbers to functions that create
|
||||||
|
// the corresponding error. It should not be used. It is exported for use in
|
||||||
|
// the extension sub-packages.
|
||||||
|
var NewErrorFuncs = make(map[int]NewErrorFun)
|
||||||
|
|
||||||
|
// NewExtErrorFuncs is a temporary map that stores error constructor functions
|
||||||
|
// for each extension. When an extension is initialized, each error for that
|
||||||
|
// extension is added to the 'NewErrorFuncs' map. It should not be used. It is
|
||||||
|
// exported for use in the extension sub-packages.
|
||||||
|
var NewExtErrorFuncs = make(map[string]map[int]NewErrorFun)
|
||||||
|
|
||||||
|
// eventOrError corresponds to values that can be either an event or an
|
||||||
|
// error.
|
||||||
|
type eventOrError interface{}
|
||||||
|
|
||||||
|
// NewId generates a new unused ID for use with requests like CreateWindow.
|
||||||
|
// If no new ids can be generated, the id returned is 0 and error is non-nil.
|
||||||
|
// This shouldn't be used directly, and is exported for use in the extension
|
||||||
|
// sub-packages.
|
||||||
|
// If you need identifiers, use the appropriate constructor.
|
||||||
|
// e.g., For a window id, use xproto.NewWindowId. For
|
||||||
|
// a new pixmap id, use xproto.NewPixmapId. And so on.
|
||||||
|
func (c *Conn) NewId() (uint32, error) {
|
||||||
|
xid := <-c.xidChan
|
||||||
|
if xid.err != nil {
|
||||||
|
return 0, xid.err
|
||||||
|
}
|
||||||
|
return xid.id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// xid encapsulates a resource identifier being sent over the Conn.xidChan
|
||||||
|
// channel. If no new resource id can be generated, id is set to 0 and a
|
||||||
|
// non-nil error is set in xid.err.
|
||||||
|
type xid struct {
|
||||||
|
id uint32
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateXids sends new Ids down the channel for NewId to use.
|
||||||
|
// generateXids should be run in its own goroutine.
|
||||||
|
// This needs to be updated to use the XC Misc extension once we run out of
|
||||||
|
// new ids.
|
||||||
|
// Thanks to libxcb/src/xcb_xid.c. This code is greatly inspired by it.
|
||||||
|
func (conn *Conn) generateXIds() {
|
||||||
|
defer close(conn.xidChan)
|
||||||
|
|
||||||
|
// This requires some explanation. From the horse's mouth:
|
||||||
|
// "The resource-id-mask contains a single contiguous set of bits (at least
|
||||||
|
// 18). The client allocates resource IDs for types WINDOW, PIXMAP,
|
||||||
|
// CURSOR, FONT, GCONTEXT, and COLORMAP by choosing a value with only some
|
||||||
|
// subset of these bits set and ORing it with resource-id-base. Only values
|
||||||
|
// constructed in this way can be used to name newly created resources over
|
||||||
|
// this connection."
|
||||||
|
// So for example (using 8 bit integers), the mask might look like:
|
||||||
|
// 00111000
|
||||||
|
// So that valid values would be 00101000, 00110000, 00001000, and so on.
|
||||||
|
// Thus, the idea is to increment it by the place of the last least
|
||||||
|
// significant '1'. In this case, that value would be 00001000. To get
|
||||||
|
// that value, we can AND the original mask with its two's complement:
|
||||||
|
// 00111000 & 11001000 = 00001000.
|
||||||
|
// And we use that value to increment the last resource id to get a new one.
|
||||||
|
// (And then, of course, we OR it with resource-id-base.)
|
||||||
|
inc := conn.setupResourceIdMask & -conn.setupResourceIdMask
|
||||||
|
max := conn.setupResourceIdMask
|
||||||
|
last := uint32(0)
|
||||||
|
for {
|
||||||
|
// TODO: Use the XC Misc extension to look for released ids.
|
||||||
|
if last > 0 && last >= max-inc+1 {
|
||||||
|
conn.xidChan <- xid{
|
||||||
|
id: 0,
|
||||||
|
err: errors.New("There are no more available resource" +
|
||||||
|
"identifiers."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last += inc
|
||||||
|
conn.xidChan <- xid{
|
||||||
|
id: last | conn.setupResourceIdBase,
|
||||||
|
err: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSeqId fetches the next sequence id from the Conn.seqChan channel.
|
||||||
|
func (c *Conn) newSequenceId() uint16 {
|
||||||
|
return <-c.seqChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSeqIds returns new sequence ids. It is meant to be run in its
|
||||||
|
// own goroutine.
|
||||||
|
// A sequence id is generated for *every* request. It's the identifier used
|
||||||
|
// to match up replies with requests.
|
||||||
|
// Since sequence ids can only be 16 bit integers we start over at zero when it
|
||||||
|
// comes time to wrap.
|
||||||
|
// N.B. As long as the cookie buffer is less than 2^16, there are no limitations
|
||||||
|
// on the number (or kind) of requests made in sequence.
|
||||||
|
func (c *Conn) generateSeqIds() {
|
||||||
|
defer close(c.seqChan)
|
||||||
|
|
||||||
|
seqid := uint16(1)
|
||||||
|
for {
|
||||||
|
c.seqChan <- seqid
|
||||||
|
if seqid == uint16((1<<16)-1) {
|
||||||
|
seqid = 0
|
||||||
|
} else {
|
||||||
|
seqid++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// request encapsulates a buffer of raw bytes (containing the request data)
|
||||||
|
// and a cookie, which when combined represents a single request.
|
||||||
|
// The cookie is used to match up the reply/error.
|
||||||
|
type request struct {
|
||||||
|
buf []byte
|
||||||
|
cookie *Cookie
|
||||||
|
|
||||||
|
// seq is closed when the request (cookie) has been sequenced by the Conn.
|
||||||
|
seq chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest takes the bytes and a cookie of a particular request, constructs
|
||||||
|
// a request type, and sends it over the Conn.reqChan channel.
|
||||||
|
// Note that the sequence number is added to the cookie after it is sent
|
||||||
|
// over the request channel, but before it is sent to X.
|
||||||
|
//
|
||||||
|
// Note that you may safely use NewRequest to send arbitrary byte requests
|
||||||
|
// to X. The resulting cookie can be used just like any normal cookie and
|
||||||
|
// abides by the same rules, except that for replies, you'll get back the
|
||||||
|
// raw byte data. This may be useful for performance critical sections where
|
||||||
|
// every allocation counts, since all X requests in XGB allocate a new byte
|
||||||
|
// slice. In contrast, NewRequest allocates one small request struct and
|
||||||
|
// nothing else. (Except when the cookie buffer is full and has to be flushed.)
|
||||||
|
//
|
||||||
|
// If you're using NewRequest manually, you'll need to use NewCookie to create
|
||||||
|
// a new cookie.
|
||||||
|
//
|
||||||
|
// In all likelihood, you should be able to copy and paste with some minor
|
||||||
|
// edits the generated code for the request you want to issue.
|
||||||
|
func (c *Conn) NewRequest(buf []byte, cookie *Cookie) {
|
||||||
|
seq := make(chan struct{})
|
||||||
|
c.reqChan <- &request{buf: buf, cookie: cookie, seq: seq}
|
||||||
|
<-seq
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRequests is run as a single goroutine that takes requests and writes
|
||||||
|
// the bytes to the wire and adds the cookie to the cookie queue.
|
||||||
|
// It is meant to be run as its own goroutine.
|
||||||
|
func (c *Conn) sendRequests() {
|
||||||
|
defer close(c.cookieChan)
|
||||||
|
|
||||||
|
for req := range c.reqChan {
|
||||||
|
// ho there! if the cookie channel is nearly full, force a round
|
||||||
|
// trip to clear out the cookie buffer.
|
||||||
|
// Note that we circumvent the request channel, because we're *in*
|
||||||
|
// the request channel.
|
||||||
|
if len(c.cookieChan) == cookieBuffer-1 {
|
||||||
|
if err := c.noop(); err != nil {
|
||||||
|
// Shut everything down.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.cookie.Sequence = c.newSequenceId()
|
||||||
|
c.cookieChan <- req.cookie
|
||||||
|
c.writeBuffer(req.buf)
|
||||||
|
close(req.seq)
|
||||||
|
}
|
||||||
|
response := make(chan struct{})
|
||||||
|
c.closing <- response
|
||||||
|
c.noop() // Flush the response reading goroutine, ignore error.
|
||||||
|
<-response
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// noop circumvents the usual request sending goroutines and forces a round
|
||||||
|
// trip request manually.
|
||||||
|
func (c *Conn) noop() error {
|
||||||
|
cookie := c.NewCookie(true, true)
|
||||||
|
cookie.Sequence = c.newSequenceId()
|
||||||
|
c.cookieChan <- cookie
|
||||||
|
if err := c.writeBuffer(c.getInputFocusRequest()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cookie.Reply() // wait for the buffer to clear
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBuffer is a convenience function for writing a byte slice to the wire.
|
||||||
|
func (c *Conn) writeBuffer(buf []byte) error {
|
||||||
|
if _, err := c.conn.Write(buf); err != nil {
|
||||||
|
Logger.Printf("A write error is unrecoverable: %s", err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readResponses is a goroutine that reads events, errors and
|
||||||
|
// replies off the wire.
|
||||||
|
// When an event is read, it is always added to the event channel.
|
||||||
|
// When an error is read, if it corresponds to an existing checked cookie,
|
||||||
|
// it is sent to that cookie's error channel. Otherwise it is added to the
|
||||||
|
// event channel.
|
||||||
|
// When a reply is read, it is added to the corresponding cookie's reply
|
||||||
|
// channel. (It is an error if no such cookie exists in this case.)
|
||||||
|
// Finally, cookies that came "before" this reply are always cleaned up.
|
||||||
|
func (c *Conn) readResponses() {
|
||||||
|
defer close(c.eventChan)
|
||||||
|
|
||||||
|
var (
|
||||||
|
err Error
|
||||||
|
seq uint16
|
||||||
|
replyBytes []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case respond := <-c.closing:
|
||||||
|
respond <- struct{}{}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
err, seq = nil, 0
|
||||||
|
if _, err := io.ReadFull(c.conn, buf); err != nil {
|
||||||
|
Logger.Printf("A read error is unrecoverable: %s", err)
|
||||||
|
c.eventChan <- err
|
||||||
|
c.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch buf[0] {
|
||||||
|
case 0: // This is an error
|
||||||
|
// Use the constructor function for this error (that is auto
|
||||||
|
// generated) by looking it up by the error number.
|
||||||
|
newErrFun, ok := NewErrorFuncs[int(buf[1])]
|
||||||
|
if !ok {
|
||||||
|
Logger.Printf("BUG: Could not find error constructor function "+
|
||||||
|
"for error with number %d.", buf[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = newErrFun(buf)
|
||||||
|
seq = err.SequenceId()
|
||||||
|
|
||||||
|
// This error is either sent to the event channel or a specific
|
||||||
|
// cookie's error channel below.
|
||||||
|
case 1: // This is a reply
|
||||||
|
seq = Get16(buf[2:])
|
||||||
|
|
||||||
|
// check to see if this reply has more bytes to be read
|
||||||
|
size := Get32(buf[4:])
|
||||||
|
if size > 0 {
|
||||||
|
byteCount := 32 + size*4
|
||||||
|
biggerBuf := make([]byte, byteCount)
|
||||||
|
copy(biggerBuf[:32], buf)
|
||||||
|
if _, err := io.ReadFull(c.conn, biggerBuf[32:]); err != nil {
|
||||||
|
Logger.Printf("A read error is unrecoverable: %s", err)
|
||||||
|
c.eventChan <- err
|
||||||
|
c.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
replyBytes = biggerBuf
|
||||||
|
} else {
|
||||||
|
replyBytes = buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// This reply is sent to its corresponding cookie below.
|
||||||
|
default: // This is an event
|
||||||
|
// Use the constructor function for this event (like for errors,
|
||||||
|
// and is also auto generated) by looking it up by the event number.
|
||||||
|
// Note that we AND the event number with 127 so that we ignore
|
||||||
|
// the most significant bit (which is set when it was sent from
|
||||||
|
// a SendEvent request).
|
||||||
|
evNum := int(buf[0] & 127)
|
||||||
|
newEventFun, ok := NewEventFuncs[evNum]
|
||||||
|
if !ok {
|
||||||
|
Logger.Printf("BUG: Could not find event construct function "+
|
||||||
|
"for event with number %d.", evNum)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.eventChan <- newEventFun(buf)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have a sequence number and we're either
|
||||||
|
// processing an error or a reply, which are both responses to
|
||||||
|
// requests. So all we have to do is find the cookie corresponding
|
||||||
|
// to this error/reply, and send the appropriate data to it.
|
||||||
|
// In doing so, we make sure that any cookies that came before it
|
||||||
|
// are marked as successful if they are void and checked.
|
||||||
|
// If there's a cookie that requires a reply that is before this
|
||||||
|
// reply, then something is wrong.
|
||||||
|
for cookie := range c.cookieChan {
|
||||||
|
// This is the cookie we're looking for. Process and break.
|
||||||
|
if cookie.Sequence == seq {
|
||||||
|
if err != nil { // this is an error to a request
|
||||||
|
// synchronous processing
|
||||||
|
if cookie.errorChan != nil {
|
||||||
|
cookie.errorChan <- err
|
||||||
|
} else { // asynchronous processing
|
||||||
|
c.eventChan <- err
|
||||||
|
// if this is an unchecked reply, ping the cookie too
|
||||||
|
if cookie.pingChan != nil {
|
||||||
|
cookie.pingChan <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // this is a reply
|
||||||
|
if cookie.replyChan == nil {
|
||||||
|
Logger.Printf("Reply with sequence id %d does not "+
|
||||||
|
"have a cookie with a valid reply channel.", seq)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
cookie.replyChan <- replyBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// Checked requests with replies
|
||||||
|
case cookie.replyChan != nil && cookie.errorChan != nil:
|
||||||
|
Logger.Printf("Found cookie with sequence id %d that is "+
|
||||||
|
"expecting a reply but will never get it. Currently "+
|
||||||
|
"on sequence number %d", cookie.Sequence, seq)
|
||||||
|
// Unchecked requests with replies
|
||||||
|
case cookie.replyChan != nil && cookie.pingChan != nil:
|
||||||
|
Logger.Printf("Found cookie with sequence id %d that is "+
|
||||||
|
"expecting a reply (and not an error) but will never "+
|
||||||
|
"get it. Currently on sequence number %d",
|
||||||
|
cookie.Sequence, seq)
|
||||||
|
// Checked requests without replies
|
||||||
|
case cookie.pingChan != nil && cookie.errorChan != nil:
|
||||||
|
cookie.pingChan <- true
|
||||||
|
// Unchecked requests without replies don't have any channels,
|
||||||
|
// so we can't do anything with them except let them pass by.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processEventOrError takes an eventOrError, type switches on it,
|
||||||
|
// and returns it in Go idiomatic style.
|
||||||
|
func processEventOrError(everr eventOrError) (Event, Error) {
|
||||||
|
switch ee := everr.(type) {
|
||||||
|
case Event:
|
||||||
|
return ee, nil
|
||||||
|
case Error:
|
||||||
|
return nil, ee
|
||||||
|
default:
|
||||||
|
Logger.Printf("Invalid event/error type: %T", everr)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForEvent returns the next event from the server.
|
||||||
|
// It will block until an event is available.
|
||||||
|
// WaitForEvent returns either an Event or an Error. (Returning both
|
||||||
|
// is a bug.) Note than an Error here is an X error and not an XGB error. That
|
||||||
|
// is, X errors are sometimes completely expected (and you may want to ignore
|
||||||
|
// them in some cases).
|
||||||
|
//
|
||||||
|
// If both the event and error are nil, then the connection has been closed.
|
||||||
|
func (c *Conn) WaitForEvent() (Event, Error) {
|
||||||
|
return processEventOrError(<-c.eventChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollForEvent returns the next event from the server if one is available in
|
||||||
|
// the internal queue without blocking. Note that unlike WaitForEvent, both
|
||||||
|
// Event and Error could be nil. Indeed, they are both nil when the event queue
|
||||||
|
// is empty.
|
||||||
|
func (c *Conn) PollForEvent() (Event, Error) {
|
||||||
|
select {
|
||||||
|
case everr := <-c.eventChan:
|
||||||
|
return processEventOrError(everr)
|
||||||
|
default:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
718
vendor/github.com/BurntSushi/xgb/xinerama/xinerama.go
generated
vendored
Normal file
718
vendor/github.com/BurntSushi/xgb/xinerama/xinerama.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
14910
vendor/github.com/BurntSushi/xgb/xproto/xproto.go
generated
vendored
Normal file
14910
vendor/github.com/BurntSushi/xgb/xproto/xproto.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
vendor/github.com/BurntSushi/xgbutil/.gitignore
generated
vendored
Normal file
6
vendor/github.com/BurntSushi/xgbutil/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*.swp
|
||||||
|
*.png
|
||||||
|
tst_first
|
||||||
|
tst_graphics
|
||||||
|
TAGS
|
||||||
|
|
||||||
13
vendor/github.com/BurntSushi/xgbutil/COPYING
generated
vendored
Normal file
13
vendor/github.com/BurntSushi/xgbutil/COPYING
generated
vendored
Normal file
@@ -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.
|
||||||
36
vendor/github.com/BurntSushi/xgbutil/Makefile
generated
vendored
Normal file
36
vendor/github.com/BurntSushi/xgbutil/Makefile
generated
vendored
Normal file
@@ -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
|
||||||
|
|
||||||
55
vendor/github.com/BurntSushi/xgbutil/README
generated
vendored
Normal file
55
vendor/github.com/BurntSushi/xgbutil/README
generated
vendored
Normal file
@@ -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
|
||||||
|
|
||||||
29
vendor/github.com/BurntSushi/xgbutil/STYLE
generated
vendored
Normal file
29
vendor/github.com/BurntSushi/xgbutil/STYLE
generated
vendored
Normal file
@@ -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.
|
||||||
|
|
||||||
67
vendor/github.com/BurntSushi/xgbutil/doc.go
generated
vendored
Normal file
67
vendor/github.com/BurntSushi/xgbutil/doc.go
generated
vendored
Normal file
@@ -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
|
||||||
1
vendor/github.com/BurntSushi/xgbutil/session.vim
generated
vendored
Normal file
1
vendor/github.com/BurntSushi/xgbutil/session.vim
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
au BufWritePost *.go silent!make tags > /dev/null 2>&1
|
||||||
167
vendor/github.com/BurntSushi/xgbutil/types.go
generated
vendored
Normal file
167
vendor/github.com/BurntSushi/xgbutil/types.go
generated
vendored
Normal file
@@ -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)
|
||||||
348
vendor/github.com/BurntSushi/xgbutil/xgbutil.go
generated
vendored
Normal file
348
vendor/github.com/BurntSushi/xgbutil/xgbutil.go
generated
vendored
Normal file
@@ -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())
|
||||||
|
}
|
||||||
103
vendor/github.com/BurntSushi/xgbutil/xprop/atom.go
generated
vendored
Normal file
103
vendor/github.com/BurntSushi/xgbutil/xprop/atom.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
40
vendor/github.com/BurntSushi/xgbutil/xprop/doc.go
generated
vendored
Normal file
40
vendor/github.com/BurntSushi/xgbutil/xprop/doc.go
generated
vendored
Normal file
@@ -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
|
||||||
270
vendor/github.com/BurntSushi/xgbutil/xprop/xprop.go
generated
vendored
Normal file
270
vendor/github.com/BurntSushi/xgbutil/xprop/xprop.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
77
vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md
generated
vendored
Normal file
77
vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age,
|
||||||
|
body size, disability, ethnicity, sex characteristics, gender identity and
|
||||||
|
expression, level of experience, education, socio-economic status, nationality,
|
||||||
|
personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behaviour that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behaviour by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behaviour and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behaviour.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviour that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
|
||||||
|
reported by contacting the project team at adrg@epistack.com. All complaints
|
||||||
|
will be reviewed and investigated and will result in a response that is deemed
|
||||||
|
necessary and appropriate to the circumstances. The project team is obligated to
|
||||||
|
maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 1.4, available at
|
||||||
|
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
135
vendor/github.com/adrg/xdg/CONTRIBUTING.md
generated
vendored
Normal file
135
vendor/github.com/adrg/xdg/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Contributing to this project
|
||||||
|
|
||||||
|
Contributions in the form of pull requests, issues or just general feedback,
|
||||||
|
are always welcome. Please take a moment to review this document in order to
|
||||||
|
make the contribution process easy and effective for everyone involved.
|
||||||
|
|
||||||
|
Following these guidelines helps to communicate that you respect the time of
|
||||||
|
the developers managing and developing this open source project. In return,
|
||||||
|
they should reciprocate that respect in addressing your issue or assessing
|
||||||
|
patches and features.
|
||||||
|
|
||||||
|
## Using the issue tracker
|
||||||
|
|
||||||
|
The issue tracker is the preferred channel for [bug reports](#bugs),
|
||||||
|
[features requests](#features) and [submitting pull
|
||||||
|
requests](#pull-requests), but please respect the following restrictions:
|
||||||
|
|
||||||
|
* Please **do not** use the issue tracker for personal support requests (use
|
||||||
|
[Stack Overflow](http://stackoverflow.com) or IRC).
|
||||||
|
* Please **do not** derail or troll issues. Keep the discussion on topic and
|
||||||
|
respect the opinions of others.
|
||||||
|
|
||||||
|
<a name="bugs"></a>
|
||||||
|
## Bug reports
|
||||||
|
|
||||||
|
A bug is a _demonstrable problem_ that is caused by the code in the repository.
|
||||||
|
Good bug reports are extremely helpful - thank you!
|
||||||
|
|
||||||
|
Guidelines for bug reports:
|
||||||
|
|
||||||
|
1. **Use the GitHub issue search** — check if the issue has already been
|
||||||
|
reported.
|
||||||
|
2. **Check if the issue has been fixed** — try to reproduce it using the
|
||||||
|
latest `master` or development branch in the repository.
|
||||||
|
3. **Isolate the problem** — create a reduced test case.
|
||||||
|
|
||||||
|
A good bug report shouldn't leave others needing to chase you up for more
|
||||||
|
information. Please try to be as detailed as possible in your report. What is
|
||||||
|
your environment? What steps will reproduce the issue? What browser(s) and OS
|
||||||
|
experience the problem? What would you expect to be the outcome? All these
|
||||||
|
details will help people to fix any potential bugs.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
> Short and descriptive example bug report title
|
||||||
|
>
|
||||||
|
> A summary of the issue and the browser/OS environment in which it occurs. If
|
||||||
|
> suitable, include the steps required to reproduce the bug.
|
||||||
|
>
|
||||||
|
> 1. This is the first step
|
||||||
|
> 2. This is the second step
|
||||||
|
> 3. Further steps, etc.
|
||||||
|
>
|
||||||
|
> `<url>` - a link to the reduced test case
|
||||||
|
>
|
||||||
|
> Any other information you want to share that is relevant to the issue being
|
||||||
|
> reported. This might include the lines of code that you have identified as
|
||||||
|
> causing the bug, and potential solutions (and your opinions on their
|
||||||
|
> merits).
|
||||||
|
|
||||||
|
|
||||||
|
<a name="features"></a>
|
||||||
|
## Feature requests
|
||||||
|
|
||||||
|
Feature requests are welcome. But take a moment to find out whether your idea
|
||||||
|
fits with the scope and aims of the project. It's up to *you* to make a strong
|
||||||
|
case to convince the project's developers of the merits of this feature. Please
|
||||||
|
provide as much detail and context as possible.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="pull-requests"></a>
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Good pull requests - patches, improvements, new features - are a fantastic
|
||||||
|
help. They should remain focused in scope and avoid containing unrelated
|
||||||
|
commits.
|
||||||
|
|
||||||
|
**Please ask first** before embarking on any significant pull request (e.g.
|
||||||
|
implementing features, refactoring code, porting to a different language),
|
||||||
|
otherwise you risk spending a lot of time working on something that the
|
||||||
|
project's developers might not want to merge into the project.
|
||||||
|
|
||||||
|
Please adhere to the coding conventions used throughout a project (indentation,
|
||||||
|
accurate comments, etc.) and any other requirements (such as test coverage).
|
||||||
|
|
||||||
|
Follow this process if you'd like your work considered for inclusion in the
|
||||||
|
project:
|
||||||
|
|
||||||
|
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
|
||||||
|
and configure the remotes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone your fork of the repo into the current directory
|
||||||
|
git clone https://github.com/<your-username>/<repo-name>
|
||||||
|
# Navigate to the newly cloned directory
|
||||||
|
cd <repo-name>
|
||||||
|
# Assign the original repo to a remote called "upstream"
|
||||||
|
git remote add upstream https://github.com/<upstream-owner>/<repo-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If you cloned a while ago, get the latest changes from upstream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout <dev-branch>
|
||||||
|
git pull upstream <dev-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a new topic branch (off the main project development branch) to
|
||||||
|
contain your feature, change, or fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b <topic-branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Commit your changes in logical chunks and use descriptive commit messages.
|
||||||
|
Use [interactive rebase](https://help.github.com/articles/interactive-rebase)
|
||||||
|
to tidy up your commits before making them public.
|
||||||
|
|
||||||
|
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull [--rebase] upstream <dev-branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Push your topic branch up to your fork:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin <topic-branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
|
||||||
|
with a clear title and description.
|
||||||
|
|
||||||
|
**IMPORTANT**: By submitting a patch, you agree to allow the project owner to
|
||||||
|
license your work under the same license as that used by the project.
|
||||||
21
vendor/github.com/adrg/xdg/LICENSE
generated
vendored
Normal file
21
vendor/github.com/adrg/xdg/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Adrian-George Bostan <adrg@epistack.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
280
vendor/github.com/adrg/xdg/README.md
generated
vendored
Normal file
280
vendor/github.com/adrg/xdg/README.md
generated
vendored
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<div>
|
||||||
|
<img src="https://raw.githubusercontent.com/adrg/adrg.github.io/master/assets/projects/xdg/logo.svg" alt="xdg logo"/>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h4 align="center">Go implementation of the XDG Base Directory Specification and XDG user directories.</h4>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/adrg/xdg/actions?query=workflow%3ACI">
|
||||||
|
<img alt="Build status" src="https://github.com/adrg/xdg/workflows/CI/badge.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://app.codecov.io/gh/adrg/xdg">
|
||||||
|
<img alt="Code coverage" src="https://codecov.io/gh/adrg/xdg/branch/master/graphs/badge.svg?branch=master">
|
||||||
|
</a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/adrg/xdg">
|
||||||
|
<img alt="pkg.go.dev documentation" src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white">
|
||||||
|
</a>
|
||||||
|
<a href="https://opensource.org/licenses/MIT" rel="nofollow">
|
||||||
|
<img alt="MIT license" src="https://img.shields.io/github/license/adrg/xdg">
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<a href="https://goreportcard.com/report/github.com/adrg/xdg">
|
||||||
|
<img alt="Go report card" src="https://goreportcard.com/badge/github.com/adrg/xdg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/avelino/awesome-go#configuration">
|
||||||
|
<img alt="Awesome Go" src="https://awesome.re/mentioned-badge.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/adrg/xdg/graphs/contributors">
|
||||||
|
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/adrg/xdg" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/adrg/xdg/issues">
|
||||||
|
<img alt="GitHub open issues" src="https://img.shields.io/github/issues-raw/adrg/xdg">
|
||||||
|
</a>
|
||||||
|
<a href="https://ko-fi.com/T6T72WATK">
|
||||||
|
<img alt="Buy me a coffee" src="https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=579fbf&logo=buy%20me%20a%20coffee&logoColor=white">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Provides an implementation of the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
||||||
|
The specification defines a set of standard paths for storing application files,
|
||||||
|
including data and configuration files. For portability and flexibility reasons,
|
||||||
|
applications should use the XDG defined locations instead of hardcoding paths.
|
||||||
|
The package also includes the locations of well known [user directories](https://wiki.archlinux.org/index.php/XDG_user_directories), as well as
|
||||||
|
other common directories such as fonts and applications.
|
||||||
|
|
||||||
|
The current implementation supports **most flavors of Unix**, **Windows**, **macOS** and **Plan 9**.
|
||||||
|
On Windows, where XDG environment variables are not usually set, the package uses [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders)
|
||||||
|
as defaults. Therefore, appropriate locations are used for common [folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid) which may have been redirected.
|
||||||
|
|
||||||
|
See usage [examples](#usage) below. Full documentation can be found at https://pkg.go.dev/github.com/adrg/xdg.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
go get github.com/adrg/xdg
|
||||||
|
|
||||||
|
## Default locations
|
||||||
|
|
||||||
|
The package defines sensible defaults for XDG variables which are empty or not
|
||||||
|
present in the environment.
|
||||||
|
|
||||||
|
- On Unix-like operating systems, XDG environment variables are tipically defined.
|
||||||
|
Appropriate default locations are used for the environment variables which are not set.
|
||||||
|
- On Windows, XDG environment variables are usually not set. If that is the case,
|
||||||
|
the package relies on the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
|
||||||
|
Sensible fallback locations are used for the folders which are not set.
|
||||||
|
|
||||||
|
### XDG Base Directory
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Unix-like operating systems</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#xdg-base-directory"><img width="400" height="0"></a> | <a href="#xdg-base-directory"><img width="500" height="0"></a><p>Unix</p> | <a href="#xdg-base-directory"><img width="600" height="0"></a><p>macOS</p> | <a href="#xdg-base-directory"><img width="500" height="0"></a><p>Plan 9</p> |
|
||||||
|
| :------------------------------------------------------------: | :-----------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>XDG_DATA_HOME</b></kbd> | <kbd>~/.local/share</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib</kbd> |
|
||||||
|
| <kbd><b>XDG_DATA_DIRS</b></kbd> | <kbd>/usr/local/share</kbd><br/><kbd>/usr/share</kbd> | <kbd>/Library/Application Support</kbd> | <kbd>/lib</kbd> |
|
||||||
|
| <kbd><b>XDG_CONFIG_HOME</b></kbd> | <kbd>~/.config</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib</kbd> |
|
||||||
|
| <kbd><b>XDG_CONFIG_DIRS</b></kbd> | <kbd>/etc/xdg</kbd> | <kbd>~/Library/Preferences</kbd><br/><kbd>/Library/Application Support</kbd><br/><kbd>/Library/Preferences</kbd> | <kbd>/lib</kbd> |
|
||||||
|
| <kbd><b>XDG_STATE_HOME</b></kbd> | <kbd>~/.local/state</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>$home/lib/state</kbd> |
|
||||||
|
| <kbd><b>XDG_CACHE_HOME</b></kbd> | <kbd>~/.cache</kbd> | <kbd>~/Library/Caches</kbd> | <kbd>$home/lib/cache</kbd> |
|
||||||
|
| <kbd><b>XDG_RUNTIME_DIR</b></kbd> | <kbd>/run/user/UID</kbd> | <kbd>~/Library/Application Support</kbd> | <kbd>/tmp</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Microsoft Windows</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#xdg-base-directory"><img width="400" height="0"></a> | <a href="#xdg-base-directory"><img width="700" height="0"></a><p>Known Folder(s)</p> | <a href="#xdg-base-directory"><img width="900" height="0"></a><p>Fallback(s)</p> |
|
||||||
|
| :------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>XDG_DATA_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
||||||
|
| <kbd><b>XDG_DATA_DIRS</b></kbd> | <kbd>RoamingAppData</kbd><br/><kbd>ProgramData</kbd> | <kbd>%APPADATA%</kbd><br/><kbd>%ProgramData%</kbd> |
|
||||||
|
| <kbd><b>XDG_CONFIG_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
||||||
|
| <kbd><b>XDG_CONFIG_DIRS</b></kbd> | <kbd>ProgramData</kbd><br/><kbd>RoamingAppData</kbd> | <kbd>%ProgramData%</kbd><br/><kbd>%APPDATA%</kbd> |
|
||||||
|
| <kbd><b>XDG_STATE_HOME</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
||||||
|
| <kbd><b>XDG_CACHE_HOME</b></kbd> | <kbd>LocalAppData\cache</kbd> | <kbd>%LOCALAPPDATA%\cache</kbd> |
|
||||||
|
| <kbd><b>XDG_RUNTIME_DIR</b></kbd> | <kbd>LocalAppData</kbd> | <kbd>%LOCALAPPDATA%</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### XDG user directories
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Unix-like operating systems</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#xdg-user-directories"><img width="500" height="0"></a> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>Unix</p> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>macOS</p> | <a href="#xdg-user-directories"><img width="500" height="0"></a><p>Plan 9</p> |
|
||||||
|
| :--------------------------------------------------------------: | :-------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | :---------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>XDG_DESKTOP_DIR</b></kbd> | <kbd>~/Desktop</kbd> | <kbd>~/Desktop</kbd> | <kbd>$home/desktop</kbd> |
|
||||||
|
| <kbd><b>XDG_DOWNLOAD_DIR</b></kbd> | <kbd>~/Downloads</kbd> | <kbd>~/Downloads</kbd> | <kbd>$home/downloads</kbd> |
|
||||||
|
| <kbd><b>XDG_DOCUMENTS_DIR</b></kbd> | <kbd>~/Documents</kbd> | <kbd>~/Documents</kbd> | <kbd>$home/documents</kbd> |
|
||||||
|
| <kbd><b>XDG_MUSIC_DIR</b></kbd> | <kbd>~/Music</kbd> | <kbd>~/Music</kbd> | <kbd>$home/music</kbd> |
|
||||||
|
| <kbd><b>XDG_PICTURES_DIR</b></kbd> | <kbd>~/Pictures</kbd> | <kbd>~/Pictures</kbd> | <kbd>$home/pictures</kbd> |
|
||||||
|
| <kbd><b>XDG_VIDEOS_DIR</b></kbd> | <kbd>~/Videos</kbd> | <kbd>~/Movies</kbd> | <kbd>$home/videos</kbd> |
|
||||||
|
| <kbd><b>XDG_TEMPLATES_DIR</b></kbd> | <kbd>~/Templates</kbd> | <kbd>~/Templates</kbd> | <kbd>$home/templates</kbd> |
|
||||||
|
| <kbd><b>XDG_PUBLICSHARE_DIR</b></kbd> | <kbd>~/Public</kbd> | <kbd>~/Public</kbd> | <kbd>$home/public</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Microsoft Windows</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#xdg-user-directories"><img width="500" height="0"></a> | <a href="#xdg-user-directories"><img width="600" height="0"></a><p>Known Folder(s)</p> | <a href="#xdg-user-directories"><img width="900" height="0"></a><p>Fallback(s)</p> |
|
||||||
|
| :--------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>XDG_DESKTOP_DIR</b></kbd> | <kbd>Desktop</kbd> | <kbd>%USERPROFILE%\Desktop</kbd> |
|
||||||
|
| <kbd><b>XDG_DOWNLOAD_DIR</b></kbd> | <kbd>Downloads</kbd> | <kbd>%USERPROFILE%\Downloads</kbd> |
|
||||||
|
| <kbd><b>XDG_DOCUMENTS_DIR</b></kbd> | <kbd>Documents</kbd> | <kbd>%USERPROFILE%\Documents</kbd> |
|
||||||
|
| <kbd><b>XDG_MUSIC_DIR</b></kbd> | <kbd>Music</kbd> | <kbd>%USERPROFILE%\Music</kbd> |
|
||||||
|
| <kbd><b>XDG_PICTURES_DIR</b></kbd> | <kbd>Pictures</kbd> | <kbd>%USERPROFILE%\Pictures</kbd> |
|
||||||
|
| <kbd><b>XDG_VIDEOS_DIR</b></kbd> | <kbd>Videos</kbd> | <kbd>%USERPROFILE%\Videos</kbd> |
|
||||||
|
| <kbd><b>XDG_TEMPLATES_DIR</b></kbd> | <kbd>Templates</kbd> | <kbd>%APPDATA%\Microsoft\Windows\Templates</kbd> |
|
||||||
|
| <kbd><b>XDG_PUBLICSHARE_DIR</b></kbd> | <kbd>Public</kbd> | <kbd>%PUBLIC%</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Other directories
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Unix-like operating systems</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#other-directories"><img width="400" height="0"></a> | <a href="#other-directories"><img width="600" height="0"></a><p>Unix</p> | <a href="#other-directories"><img width="600" height="0"></a><p>macOS</p> | <a href="#other-directories"><img width="400" height="0"></a><p>Plan 9</p> |
|
||||||
|
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>Home</b></kbd> | <kbd>$HOME</kbd> | <kbd>$HOME</kbd> | <kbd>$home</kbd> |
|
||||||
|
| <kbd><b>Applications</b></kbd> | <kbd>$XDG_DATA_HOME/applications</kbd><br/><kbd>~/.local/share/applications</kbd><br/><kbd>/usr/local/share/applications</kbd><br/><kbd>/usr/share/applications</kbd><br/><kbd>$XDG_DATA_DIRS/applications</kbd> | <kbd>/Applications</kbd> | <kbd>$home/bin</kbd><br/><kbd>/bin</kbd> |
|
||||||
|
| <kbd><b>Fonts</b></kbd> | <kbd>$XDG_DATA_HOME/fonts</kbd><br/><kbd>~/.fonts</kbd><br/><kbd>~/.local/share/fonts</kbd><br/><kbd>/usr/local/share/fonts</kbd><br/><kbd>/usr/share/fonts</kbd><br/><kbd>$XDG_DATA_DIRS/fonts</kbd> | <kbd>~/Library/Fonts</kbd><br/><kbd>/Library/Fonts</kbd><br/><kbd>/System/Library/Fonts</kbd><br/><kbd>/Network/Library/Fonts</kbd> | <kbd>$home/lib/font</kbd><br/><kbd>/lib/font</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>Microsoft Windows</strong></summary>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
| <a href="#other-directories"><img width="400" height="0"></a> | <a href="#other-directories"><img width="300" height="0"></a><p>Known Folder(s)</p> | <a href="#other-directories"><img width="1300" height="0"></a><p>Fallback(s)</p> |
|
||||||
|
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: |
|
||||||
|
| <kbd><b>Home</b></kbd> | <kbd>Profile</kbd> | <kbd>%USERPROFILE%</kbd> |
|
||||||
|
| <kbd><b>Applications</b></kbd> | <kbd>Programs</kbd><br/><kbd>CommonPrograms</kbd> | <kbd>%APPDATA%\Microsoft\Windows\Start Menu\Programs</kbd><br/><kbd>%ProgramData%\Microsoft\Windows\Start Menu\Programs</kbd> |
|
||||||
|
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd><br/><kbd>-</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
#### XDG Base Directory
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// XDG Base Directory paths.
|
||||||
|
log.Println("Home data directory:", xdg.DataHome)
|
||||||
|
log.Println("Data directories:", xdg.DataDirs)
|
||||||
|
log.Println("Home config directory:", xdg.ConfigHome)
|
||||||
|
log.Println("Config directories:", xdg.ConfigDirs)
|
||||||
|
log.Println("Home state directory:", xdg.StateHome)
|
||||||
|
log.Println("Cache directory:", xdg.CacheHome)
|
||||||
|
log.Println("Runtime directory:", xdg.RuntimeDir)
|
||||||
|
|
||||||
|
// Other common directories.
|
||||||
|
log.Println("Home directory:", xdg.Home)
|
||||||
|
log.Println("Application directories:", xdg.ApplicationDirs)
|
||||||
|
log.Println("Font directories:", xdg.FontDirs)
|
||||||
|
|
||||||
|
// Obtain a suitable location for application config files.
|
||||||
|
// ConfigFile takes one parameter which must contain the name of the file,
|
||||||
|
// but it can also contain a set of parent directories. If the directories
|
||||||
|
// don't exist, they will be created relative to the base config directory.
|
||||||
|
configFilePath, err := xdg.ConfigFile("appname/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Save the config file at:", configFilePath)
|
||||||
|
|
||||||
|
// For other types of application files use:
|
||||||
|
// xdg.DataFile()
|
||||||
|
// xdg.StateFile()
|
||||||
|
// xdg.CacheFile()
|
||||||
|
// xdg.RuntimeFile()
|
||||||
|
|
||||||
|
// Finding application config files.
|
||||||
|
// SearchConfigFile takes one parameter which must contain the name of
|
||||||
|
// the file, but it can also contain a set of parent directories relative
|
||||||
|
// to the config search paths (xdg.ConfigHome and xdg.ConfigDirs).
|
||||||
|
configFilePath, err = xdg.SearchConfigFile("appname/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Config file was found at:", configFilePath)
|
||||||
|
|
||||||
|
// For other types of application files use:
|
||||||
|
// xdg.SearchDataFile()
|
||||||
|
// xdg.SearchStateFile()
|
||||||
|
// xdg.SearchCacheFile()
|
||||||
|
// xdg.SearchRuntimeFile()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### XDG user directories
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// XDG user directories.
|
||||||
|
log.Println("Desktop directory:", xdg.UserDirs.Desktop)
|
||||||
|
log.Println("Download directory:", xdg.UserDirs.Download)
|
||||||
|
log.Println("Documents directory:", xdg.UserDirs.Documents)
|
||||||
|
log.Println("Music directory:", xdg.UserDirs.Music)
|
||||||
|
log.Println("Pictures directory:", xdg.UserDirs.Pictures)
|
||||||
|
log.Println("Videos directory:", xdg.UserDirs.Videos)
|
||||||
|
log.Println("Templates directory:", xdg.UserDirs.Templates)
|
||||||
|
log.Println("Public directory:", xdg.UserDirs.PublicShare)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stargazers over time
|
||||||
|
|
||||||
|
[](https://starchart.cc/adrg/xdg)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions in the form of pull requests, issues or just general feedback,
|
||||||
|
are always welcome.
|
||||||
|
See [CONTRIBUTING.MD](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
**Contributors**:
|
||||||
|
[adrg](https://github.com/adrg),
|
||||||
|
[wichert](https://github.com/wichert),
|
||||||
|
[bouncepaw](https://github.com/bouncepaw),
|
||||||
|
[gabriel-vasile](https://github.com/gabriel-vasile),
|
||||||
|
[KalleDK](https://github.com/KalleDK),
|
||||||
|
[nvkv](https://github.com/nvkv),
|
||||||
|
[djdv](https://github.com/djdv).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
For more information see:
|
||||||
|
* [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
|
||||||
|
* [XDG user directories](https://wiki.archlinux.org/index.php/XDG_user_directories)
|
||||||
|
* [Windows Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2014 Adrian-George Bostan.
|
||||||
|
|
||||||
|
This project is licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
See [LICENSE](LICENSE) for more details.
|
||||||
68
vendor/github.com/adrg/xdg/base_dirs.go
generated
vendored
Normal file
68
vendor/github.com/adrg/xdg/base_dirs.go
generated
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
import "github.com/adrg/xdg/internal/pathutil"
|
||||||
|
|
||||||
|
// XDG Base Directory environment variables.
|
||||||
|
const (
|
||||||
|
envDataHome = "XDG_DATA_HOME"
|
||||||
|
envDataDirs = "XDG_DATA_DIRS"
|
||||||
|
envConfigHome = "XDG_CONFIG_HOME"
|
||||||
|
envConfigDirs = "XDG_CONFIG_DIRS"
|
||||||
|
envStateHome = "XDG_STATE_HOME"
|
||||||
|
envCacheHome = "XDG_CACHE_HOME"
|
||||||
|
envRuntimeDir = "XDG_RUNTIME_DIR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type baseDirectories struct {
|
||||||
|
dataHome string
|
||||||
|
data []string
|
||||||
|
configHome string
|
||||||
|
config []string
|
||||||
|
stateHome string
|
||||||
|
cacheHome string
|
||||||
|
runtime string
|
||||||
|
|
||||||
|
// Non-standard directories.
|
||||||
|
fonts []string
|
||||||
|
applications []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) dataFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Create(relPath, append([]string{bd.dataHome}, bd.data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) configFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Create(relPath, append([]string{bd.configHome}, bd.config...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) stateFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Create(relPath, []string{bd.stateHome})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) cacheFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Create(relPath, []string{bd.cacheHome})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) runtimeFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Create(relPath, []string{bd.runtime})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) searchDataFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Search(relPath, append([]string{bd.dataHome}, bd.data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) searchConfigFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Search(relPath, append([]string{bd.configHome}, bd.config...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) searchStateFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Search(relPath, []string{bd.stateHome})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) searchCacheFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Search(relPath, []string{bd.cacheHome})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd baseDirectories) searchRuntimeFile(relPath string) (string, error) {
|
||||||
|
return pathutil.Search(relPath, []string{bd.runtime})
|
||||||
|
}
|
||||||
11
vendor/github.com/adrg/xdg/codecov.yml
generated
vendored
Normal file
11
vendor/github.com/adrg/xdg/codecov.yml
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: 90%
|
||||||
|
threshold: 1%
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 100%
|
||||||
|
ignore:
|
||||||
|
- "paths_plan9.go"
|
||||||
99
vendor/github.com/adrg/xdg/doc.go
generated
vendored
Normal file
99
vendor/github.com/adrg/xdg/doc.go
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Package xdg provides an implementation of the XDG Base Directory Specification.
|
||||||
|
The specification defines a set of standard paths for storing application files
|
||||||
|
including data and configuration files. For portability and flexibility reasons,
|
||||||
|
applications should use the XDG defined locations instead of hardcoding paths.
|
||||||
|
The package also includes the locations of well known user directories.
|
||||||
|
|
||||||
|
The current implementation supports most flavors of Unix, Windows, Mac OS and Plan 9.
|
||||||
|
|
||||||
|
For more information regarding the XDG Base Directory Specification see:
|
||||||
|
https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||||
|
|
||||||
|
For more information regarding the XDG user directories see:
|
||||||
|
https://wiki.archlinux.org/index.php/XDG_user_directories
|
||||||
|
|
||||||
|
For more information regarding the Windows Known Folders see:
|
||||||
|
https://docs.microsoft.com/en-us/windows/win32/shell/known-folders
|
||||||
|
|
||||||
|
Usage
|
||||||
|
|
||||||
|
XDG Base Directory
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// XDG Base Directory paths.
|
||||||
|
log.Println("Home data directory:", xdg.DataHome)
|
||||||
|
log.Println("Data directories:", xdg.DataDirs)
|
||||||
|
log.Println("Home config directory:", xdg.ConfigHome)
|
||||||
|
log.Println("Config directories:", xdg.ConfigDirs)
|
||||||
|
log.Println("Home state directory:", xdg.StateHome)
|
||||||
|
log.Println("Cache directory:", xdg.CacheHome)
|
||||||
|
log.Println("Runtime directory:", xdg.RuntimeDir)
|
||||||
|
|
||||||
|
// Other common directories.
|
||||||
|
log.Println("Home directory:", xdg.Home)
|
||||||
|
log.Println("Application directories:", xdg.ApplicationDirs)
|
||||||
|
log.Println("Font directories:", xdg.FontDirs)
|
||||||
|
|
||||||
|
// Obtain a suitable location for application config files.
|
||||||
|
// ConfigFile takes one parameter which must contain the name of the file,
|
||||||
|
// but it can also contain a set of parent directories. If the directories
|
||||||
|
// don't exist, they will be created relative to the base config directory.
|
||||||
|
configFilePath, err := xdg.ConfigFile("appname/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Save the config file at:", configFilePath)
|
||||||
|
|
||||||
|
// For other types of application files use:
|
||||||
|
// xdg.DataFile()
|
||||||
|
// xdg.StateFile()
|
||||||
|
// xdg.CacheFile()
|
||||||
|
// xdg.RuntimeFile()
|
||||||
|
|
||||||
|
// Finding application config files.
|
||||||
|
// SearchConfigFile takes one parameter which must contain the name of
|
||||||
|
// the file, but it can also contain a set of parent directories relative
|
||||||
|
// to the config search paths (xdg.ConfigHome and xdg.ConfigDirs).
|
||||||
|
configFilePath, err = xdg.SearchConfigFile("appname/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Config file was found at:", configFilePath)
|
||||||
|
|
||||||
|
// For other types of application files use:
|
||||||
|
// xdg.SearchDataFile()
|
||||||
|
// xdg.SearchStateFile()
|
||||||
|
// xdg.SearchCacheFile()
|
||||||
|
// xdg.SearchRuntimeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
XDG user directories
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// XDG user directories.
|
||||||
|
log.Println("Desktop directory:", xdg.UserDirs.Desktop)
|
||||||
|
log.Println("Download directory:", xdg.UserDirs.Download)
|
||||||
|
log.Println("Documents directory:", xdg.UserDirs.Documents)
|
||||||
|
log.Println("Music directory:", xdg.UserDirs.Music)
|
||||||
|
log.Println("Pictures directory:", xdg.UserDirs.Pictures)
|
||||||
|
log.Println("Videos directory:", xdg.UserDirs.Videos)
|
||||||
|
log.Println("Templates directory:", xdg.UserDirs.Templates)
|
||||||
|
log.Println("Public directory:", xdg.UserDirs.PublicShare)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package xdg
|
||||||
78
vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go
generated
vendored
Normal file
78
vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package pathutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unique eliminates the duplicate paths from the provided slice and returns
|
||||||
|
// the result. The items in the output slice are in the order in which they
|
||||||
|
// occur in the input slice. If a `home` location is provided, the paths are
|
||||||
|
// expanded using the `ExpandHome` function.
|
||||||
|
func Unique(paths []string, home string) []string {
|
||||||
|
var (
|
||||||
|
uniq []string
|
||||||
|
registry = map[string]struct{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
p = ExpandHome(p, home)
|
||||||
|
if p != "" && filepath.IsAbs(p) {
|
||||||
|
if _, ok := registry[p]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
registry[p] = struct{}{}
|
||||||
|
uniq = append(uniq, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniq
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create returns a suitable location relative to which the file with the
|
||||||
|
// specified `name` can be written. The first path from the provided `paths`
|
||||||
|
// slice which is successfully created (or already exists) is used as a base
|
||||||
|
// path for the file. The `name` parameter should contain the name of the file
|
||||||
|
// which is going to be written in the location returned by this function, but
|
||||||
|
// it can also contain a set of parent directories, which will be created
|
||||||
|
// relative to the selected parent path.
|
||||||
|
func Create(name string, paths []string) (string, error) {
|
||||||
|
var searchedPaths []string
|
||||||
|
for _, p := range paths {
|
||||||
|
p = filepath.Join(p, name)
|
||||||
|
|
||||||
|
dir := filepath.Dir(p)
|
||||||
|
if Exists(dir) {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, os.ModeDir|0700); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
searchedPaths = append(searchedPaths, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not create any of the following paths: %s",
|
||||||
|
strings.Join(searchedPaths, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search searches for the file with the specified `name` in the provided
|
||||||
|
// slice of `paths`. The `name` parameter must contain the name of the file,
|
||||||
|
// but it can also contain a set of parent directories.
|
||||||
|
func Search(name string, paths []string) (string, error) {
|
||||||
|
var searchedPaths []string
|
||||||
|
for _, p := range paths {
|
||||||
|
p = filepath.Join(p, name)
|
||||||
|
if Exists(p) {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
searchedPaths = append(searchedPaths, filepath.Dir(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s",
|
||||||
|
filepath.Base(name), strings.Join(searchedPaths, ", "))
|
||||||
|
}
|
||||||
29
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go
generated
vendored
Normal file
29
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package pathutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists returns true if the specified path exists.
|
||||||
|
func Exists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil || os.IsExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandHome substitutes `~` and `$home` at the start of the specified
|
||||||
|
// `path` using the provided `home` location.
|
||||||
|
func ExpandHome(path, home string) string {
|
||||||
|
if path == "" || home == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if path[0] == '~' {
|
||||||
|
return filepath.Join(home, path[1:])
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "$home") {
|
||||||
|
return filepath.Join(home, path[5:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
32
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go
generated
vendored
Normal file
32
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
|
||||||
|
// +build aix darwin dragonfly freebsd js,wasm nacl linux netbsd openbsd solaris
|
||||||
|
|
||||||
|
package pathutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists returns true if the specified path exists.
|
||||||
|
func Exists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil || os.IsExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandHome substitutes `~` and `$HOME` at the start of the specified
|
||||||
|
// `path` using the provided `home` location.
|
||||||
|
func ExpandHome(path, home string) string {
|
||||||
|
if path == "" || home == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if path[0] == '~' {
|
||||||
|
return filepath.Join(home, path[1:])
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "$HOME") {
|
||||||
|
return filepath.Join(home, path[5:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
64
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go
generated
vendored
Normal file
64
vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package pathutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists returns true if the specified path exists.
|
||||||
|
func Exists(path string) bool {
|
||||||
|
fi, err := os.Lstat(path)
|
||||||
|
if fi != nil && fi.Mode()&os.ModeSymlink != 0 {
|
||||||
|
_, err = filepath.EvalSymlinks(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err == nil || os.IsExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandHome substitutes `%USERPROFILE%` at the start of the specified
|
||||||
|
// `path` using the provided `home` location.
|
||||||
|
func ExpandHome(path, home string) string {
|
||||||
|
if path == "" || home == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, `%USERPROFILE%`) {
|
||||||
|
return filepath.Join(home, path[13:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownFolder returns the location of the folder with the specified ID.
|
||||||
|
// If that fails, the folder location is determined by reading the provided
|
||||||
|
// environment variables (the first non-empty read value is returned).
|
||||||
|
// If that fails as well, the first non-empty fallback is returned.
|
||||||
|
// If all of the above fails, the function returns an empty string.
|
||||||
|
func KnownFolder(id *windows.KNOWNFOLDERID, envVars []string, fallbacks []string) string {
|
||||||
|
if id != nil {
|
||||||
|
flags := []uint32{windows.KF_FLAG_DEFAULT, windows.KF_FLAG_DEFAULT_PATH}
|
||||||
|
for _, flag := range flags {
|
||||||
|
p, _ := windows.KnownFolderPath(id, flag|windows.KF_FLAG_DONT_VERIFY)
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, envVar := range envVars {
|
||||||
|
p := os.Getenv(envVar)
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fallback := range fallbacks {
|
||||||
|
if fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
60
vendor/github.com/adrg/xdg/paths_darwin.go
generated
vendored
Normal file
60
vendor/github.com/adrg/xdg/paths_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func homeDir() string {
|
||||||
|
if home := os.Getenv("HOME"); home != "" {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDirs(home string) {
|
||||||
|
initBaseDirs(home)
|
||||||
|
initUserDirs(home)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBaseDirs(home string) {
|
||||||
|
homeAppSupport := filepath.Join(home, "Library", "Application Support")
|
||||||
|
rootAppSupport := "/Library/Application Support"
|
||||||
|
|
||||||
|
// Initialize standard directories.
|
||||||
|
baseDirs.dataHome = xdgPath(envDataHome, homeAppSupport)
|
||||||
|
baseDirs.data = xdgPaths(envDataDirs, rootAppSupport)
|
||||||
|
baseDirs.configHome = xdgPath(envConfigHome, homeAppSupport)
|
||||||
|
baseDirs.config = xdgPaths(envConfigDirs,
|
||||||
|
filepath.Join(home, "Library", "Preferences"),
|
||||||
|
rootAppSupport,
|
||||||
|
"/Library/Preferences",
|
||||||
|
)
|
||||||
|
baseDirs.stateHome = xdgPath(envStateHome, homeAppSupport)
|
||||||
|
baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, "Library", "Caches"))
|
||||||
|
baseDirs.runtime = xdgPath(envRuntimeDir, homeAppSupport)
|
||||||
|
|
||||||
|
// Initialize non-standard directories.
|
||||||
|
baseDirs.applications = []string{
|
||||||
|
"/Applications",
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDirs.fonts = []string{
|
||||||
|
filepath.Join(home, "Library/Fonts"),
|
||||||
|
"/Library/Fonts",
|
||||||
|
"/System/Library/Fonts",
|
||||||
|
"/Network/Library/Fonts",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUserDirs(home string) {
|
||||||
|
UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop"))
|
||||||
|
UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads"))
|
||||||
|
UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents"))
|
||||||
|
UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music"))
|
||||||
|
UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures"))
|
||||||
|
UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Movies"))
|
||||||
|
UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates"))
|
||||||
|
UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public"))
|
||||||
|
}
|
||||||
55
vendor/github.com/adrg/xdg/paths_plan9.go
generated
vendored
Normal file
55
vendor/github.com/adrg/xdg/paths_plan9.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func homeDir() string {
|
||||||
|
if home := os.Getenv("home"); home != "" {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDirs(home string) {
|
||||||
|
initBaseDirs(home)
|
||||||
|
initUserDirs(home)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBaseDirs(home string) {
|
||||||
|
homeLibDir := filepath.Join(home, "lib")
|
||||||
|
rootLibDir := "/lib"
|
||||||
|
|
||||||
|
// Initialize standard directories.
|
||||||
|
baseDirs.dataHome = xdgPath(envDataHome, homeLibDir)
|
||||||
|
baseDirs.data = xdgPaths(envDataDirs, rootLibDir)
|
||||||
|
baseDirs.configHome = xdgPath(envConfigHome, homeLibDir)
|
||||||
|
baseDirs.config = xdgPaths(envConfigDirs, rootLibDir)
|
||||||
|
baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(homeLibDir, "state"))
|
||||||
|
baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(homeLibDir, "cache"))
|
||||||
|
baseDirs.runtime = xdgPath(envRuntimeDir, "/tmp")
|
||||||
|
|
||||||
|
// Initialize non-standard directories.
|
||||||
|
baseDirs.applications = []string{
|
||||||
|
filepath.Join(home, "bin"),
|
||||||
|
"/bin",
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDirs.fonts = []string{
|
||||||
|
filepath.Join(homeLibDir, "font"),
|
||||||
|
"/lib/font",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUserDirs(home string) {
|
||||||
|
UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "desktop"))
|
||||||
|
UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "downloads"))
|
||||||
|
UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "documents"))
|
||||||
|
UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "music"))
|
||||||
|
UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "pictures"))
|
||||||
|
UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "videos"))
|
||||||
|
UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "templates"))
|
||||||
|
UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "public"))
|
||||||
|
}
|
||||||
71
vendor/github.com/adrg/xdg/paths_unix.go
generated
vendored
Normal file
71
vendor/github.com/adrg/xdg/paths_unix.go
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris
|
||||||
|
// +build aix dragonfly freebsd js,wasm nacl linux netbsd openbsd solaris
|
||||||
|
|
||||||
|
package xdg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg/internal/pathutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func homeDir() string {
|
||||||
|
if home := os.Getenv("HOME"); home != "" {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDirs(home string) {
|
||||||
|
initBaseDirs(home)
|
||||||
|
initUserDirs(home)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBaseDirs(home string) {
|
||||||
|
// Initialize standard directories.
|
||||||
|
baseDirs.dataHome = xdgPath(envDataHome, filepath.Join(home, ".local", "share"))
|
||||||
|
baseDirs.data = xdgPaths(envDataDirs, "/usr/local/share", "/usr/share")
|
||||||
|
baseDirs.configHome = xdgPath(envConfigHome, filepath.Join(home, ".config"))
|
||||||
|
baseDirs.config = xdgPaths(envConfigDirs, "/etc/xdg")
|
||||||
|
baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(home, ".local", "state"))
|
||||||
|
baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, ".cache"))
|
||||||
|
baseDirs.runtime = xdgPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid())))
|
||||||
|
|
||||||
|
// Initialize non-standard directories.
|
||||||
|
appDirs := []string{
|
||||||
|
filepath.Join(baseDirs.dataHome, "applications"),
|
||||||
|
filepath.Join(home, ".local/share/applications"),
|
||||||
|
"/usr/local/share/applications",
|
||||||
|
"/usr/share/applications",
|
||||||
|
}
|
||||||
|
|
||||||
|
fontDirs := []string{
|
||||||
|
filepath.Join(baseDirs.dataHome, "fonts"),
|
||||||
|
filepath.Join(home, ".fonts"),
|
||||||
|
filepath.Join(home, ".local/share/fonts"),
|
||||||
|
"/usr/local/share/fonts",
|
||||||
|
"/usr/share/fonts",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range baseDirs.data {
|
||||||
|
appDirs = append(appDirs, filepath.Join(dir, "applications"))
|
||||||
|
fontDirs = append(fontDirs, filepath.Join(dir, "fonts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDirs.applications = pathutil.Unique(appDirs, Home)
|
||||||
|
baseDirs.fonts = pathutil.Unique(fontDirs, Home)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUserDirs(home string) {
|
||||||
|
UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop"))
|
||||||
|
UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads"))
|
||||||
|
UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents"))
|
||||||
|
UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music"))
|
||||||
|
UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures"))
|
||||||
|
UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Videos"))
|
||||||
|
UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates"))
|
||||||
|
UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public"))
|
||||||
|
}
|
||||||
168
vendor/github.com/adrg/xdg/paths_windows.go
generated
vendored
Normal file
168
vendor/github.com/adrg/xdg/paths_windows.go
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg/internal/pathutil"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func homeDir() string {
|
||||||
|
return pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Profile,
|
||||||
|
[]string{"USERPROFILE"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDirs(home string) {
|
||||||
|
kf := initKnownFolders(home)
|
||||||
|
initBaseDirs(home, kf)
|
||||||
|
initUserDirs(home, kf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBaseDirs(home string, kf *knownFolders) {
|
||||||
|
// Initialize standard directories.
|
||||||
|
baseDirs.dataHome = xdgPath(envDataHome, kf.localAppData)
|
||||||
|
baseDirs.data = xdgPaths(envDataDirs, kf.roamingAppData, kf.programData)
|
||||||
|
baseDirs.configHome = xdgPath(envConfigHome, kf.localAppData)
|
||||||
|
baseDirs.config = xdgPaths(envConfigDirs, kf.programData, kf.roamingAppData)
|
||||||
|
baseDirs.stateHome = xdgPath(envStateHome, kf.localAppData)
|
||||||
|
baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(kf.localAppData, "cache"))
|
||||||
|
baseDirs.runtime = xdgPath(envRuntimeDir, kf.localAppData)
|
||||||
|
|
||||||
|
// Initialize non-standard directories.
|
||||||
|
baseDirs.applications = []string{
|
||||||
|
kf.programs,
|
||||||
|
kf.commonPrograms,
|
||||||
|
}
|
||||||
|
baseDirs.fonts = []string{
|
||||||
|
kf.fonts,
|
||||||
|
filepath.Join(kf.localAppData, "Microsoft", "Windows", "Fonts"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUserDirs(home string, kf *knownFolders) {
|
||||||
|
UserDirs.Desktop = xdgPath(envDesktopDir, kf.desktop)
|
||||||
|
UserDirs.Download = xdgPath(envDownloadDir, kf.downloads)
|
||||||
|
UserDirs.Documents = xdgPath(envDocumentsDir, kf.documents)
|
||||||
|
UserDirs.Music = xdgPath(envMusicDir, kf.music)
|
||||||
|
UserDirs.Pictures = xdgPath(envPicturesDir, kf.pictures)
|
||||||
|
UserDirs.Videos = xdgPath(envVideosDir, kf.videos)
|
||||||
|
UserDirs.Templates = xdgPath(envTemplatesDir, kf.templates)
|
||||||
|
UserDirs.PublicShare = xdgPath(envPublicShareDir, kf.public)
|
||||||
|
}
|
||||||
|
|
||||||
|
type knownFolders struct {
|
||||||
|
systemDrive string
|
||||||
|
systemRoot string
|
||||||
|
programData string
|
||||||
|
userProfile string
|
||||||
|
userProfiles string
|
||||||
|
roamingAppData string
|
||||||
|
localAppData string
|
||||||
|
desktop string
|
||||||
|
downloads string
|
||||||
|
documents string
|
||||||
|
music string
|
||||||
|
pictures string
|
||||||
|
videos string
|
||||||
|
templates string
|
||||||
|
public string
|
||||||
|
fonts string
|
||||||
|
programs string
|
||||||
|
commonPrograms string
|
||||||
|
}
|
||||||
|
|
||||||
|
func initKnownFolders(home string) *knownFolders {
|
||||||
|
kf := &knownFolders{
|
||||||
|
userProfile: home,
|
||||||
|
}
|
||||||
|
kf.systemDrive = filepath.VolumeName(pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Windows,
|
||||||
|
[]string{"SystemDrive", "SystemRoot", "windir"},
|
||||||
|
[]string{home, `C:`},
|
||||||
|
)) + string(filepath.Separator)
|
||||||
|
kf.systemRoot = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Windows,
|
||||||
|
[]string{"SystemRoot", "windir"},
|
||||||
|
[]string{filepath.Join(kf.systemDrive, "Windows")},
|
||||||
|
)
|
||||||
|
kf.programData = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_ProgramData,
|
||||||
|
[]string{"ProgramData", "ALLUSERSPROFILE"},
|
||||||
|
[]string{filepath.Join(kf.systemDrive, "ProgramData")},
|
||||||
|
)
|
||||||
|
kf.userProfiles = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_UserProfiles,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(kf.systemDrive, "Users")},
|
||||||
|
)
|
||||||
|
kf.roamingAppData = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_RoamingAppData,
|
||||||
|
[]string{"APPDATA"},
|
||||||
|
[]string{filepath.Join(home, "AppData", "Roaming")},
|
||||||
|
)
|
||||||
|
kf.localAppData = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_LocalAppData,
|
||||||
|
[]string{"LOCALAPPDATA"},
|
||||||
|
[]string{filepath.Join(home, "AppData", "Local")},
|
||||||
|
)
|
||||||
|
kf.desktop = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Desktop,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Desktop")},
|
||||||
|
)
|
||||||
|
kf.downloads = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Downloads,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Downloads")},
|
||||||
|
)
|
||||||
|
kf.documents = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Documents,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Documents")},
|
||||||
|
)
|
||||||
|
kf.music = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Music,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Music")},
|
||||||
|
)
|
||||||
|
kf.pictures = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Pictures,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Pictures")},
|
||||||
|
)
|
||||||
|
kf.videos = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Videos,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(home, "Videos")},
|
||||||
|
)
|
||||||
|
kf.templates = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Templates,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Templates")},
|
||||||
|
)
|
||||||
|
kf.public = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Public,
|
||||||
|
[]string{"PUBLIC"},
|
||||||
|
[]string{filepath.Join(kf.userProfiles, "Public")},
|
||||||
|
)
|
||||||
|
kf.fonts = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Fonts,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(kf.systemRoot, "Fonts")},
|
||||||
|
)
|
||||||
|
kf.programs = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_Programs,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Start Menu", "Programs")},
|
||||||
|
)
|
||||||
|
kf.commonPrograms = pathutil.KnownFolder(
|
||||||
|
windows.FOLDERID_CommonPrograms,
|
||||||
|
nil,
|
||||||
|
[]string{filepath.Join(kf.programData, "Microsoft", "Windows", "Start Menu", "Programs")},
|
||||||
|
)
|
||||||
|
|
||||||
|
return kf
|
||||||
|
}
|
||||||
40
vendor/github.com/adrg/xdg/user_dirs.go
generated
vendored
Normal file
40
vendor/github.com/adrg/xdg/user_dirs.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
// XDG user directories environment variables.
|
||||||
|
const (
|
||||||
|
envDesktopDir = "XDG_DESKTOP_DIR"
|
||||||
|
envDownloadDir = "XDG_DOWNLOAD_DIR"
|
||||||
|
envDocumentsDir = "XDG_DOCUMENTS_DIR"
|
||||||
|
envMusicDir = "XDG_MUSIC_DIR"
|
||||||
|
envPicturesDir = "XDG_PICTURES_DIR"
|
||||||
|
envVideosDir = "XDG_VIDEOS_DIR"
|
||||||
|
envTemplatesDir = "XDG_TEMPLATES_DIR"
|
||||||
|
envPublicShareDir = "XDG_PUBLICSHARE_DIR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserDirectories defines the locations of well known user directories.
|
||||||
|
type UserDirectories struct {
|
||||||
|
// Desktop defines the location of the user's desktop directory.
|
||||||
|
Desktop string
|
||||||
|
|
||||||
|
// Download defines a suitable location for user downloaded files.
|
||||||
|
Download string
|
||||||
|
|
||||||
|
// Documents defines a suitable location for user document files.
|
||||||
|
Documents string
|
||||||
|
|
||||||
|
// Music defines a suitable location for user audio files.
|
||||||
|
Music string
|
||||||
|
|
||||||
|
// Pictures defines a suitable location for user image files.
|
||||||
|
Pictures string
|
||||||
|
|
||||||
|
// VideosDir defines a suitable location for user video files.
|
||||||
|
Videos string
|
||||||
|
|
||||||
|
// Templates defines a suitable location for user template files.
|
||||||
|
Templates string
|
||||||
|
|
||||||
|
// PublicShare defines a suitable location for user shared files.
|
||||||
|
PublicShare string
|
||||||
|
}
|
||||||
218
vendor/github.com/adrg/xdg/xdg.go
generated
vendored
Normal file
218
vendor/github.com/adrg/xdg/xdg.go
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package xdg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg/internal/pathutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Home contains the path of the user's home directory.
|
||||||
|
Home string
|
||||||
|
|
||||||
|
// DataHome defines the base directory relative to which user-specific
|
||||||
|
// data files should be stored. This directory is defined by the
|
||||||
|
// $XDG_DATA_HOME environment variable. If the variable is not set,
|
||||||
|
// a default equal to $HOME/.local/share should be used.
|
||||||
|
DataHome string
|
||||||
|
|
||||||
|
// DataDirs defines the preference-ordered set of base directories to
|
||||||
|
// search for data files in addition to the DataHome base directory.
|
||||||
|
// This set of directories is defined by the $XDG_DATA_DIRS environment
|
||||||
|
// variable. If the variable is not set, the default directories
|
||||||
|
// to be used are /usr/local/share and /usr/share, in that order. The
|
||||||
|
// DataHome directory is considered more important than any of the
|
||||||
|
// directories defined by DataDirs. Therefore, user data files should be
|
||||||
|
// written relative to the DataHome directory, if possible.
|
||||||
|
DataDirs []string
|
||||||
|
|
||||||
|
// ConfigHome defines the base directory relative to which user-specific
|
||||||
|
// configuration files should be written. This directory is defined by
|
||||||
|
// the $XDG_CONFIG_HOME environment variable. If the variable is not
|
||||||
|
// not set, a default equal to $HOME/.config should be used.
|
||||||
|
ConfigHome string
|
||||||
|
|
||||||
|
// ConfigDirs defines the preference-ordered set of base directories to
|
||||||
|
// search for configuration files in addition to the ConfigHome base
|
||||||
|
// directory. This set of directories is defined by the $XDG_CONFIG_DIRS
|
||||||
|
// environment variable. If the variable is not set, a default equal
|
||||||
|
// to /etc/xdg should be used. The ConfigHome directory is considered
|
||||||
|
// more important than any of the directories defined by ConfigDirs.
|
||||||
|
// Therefore, user config files should be written relative to the
|
||||||
|
// ConfigHome directory, if possible.
|
||||||
|
ConfigDirs []string
|
||||||
|
|
||||||
|
// StateHome defines the base directory relative to which user-specific
|
||||||
|
// state files should be stored. This directory is defined by the
|
||||||
|
// $XDG_STATE_HOME environment variable. If the variable is not set,
|
||||||
|
// a default equal to ~/.local/state should be used.
|
||||||
|
StateHome string
|
||||||
|
|
||||||
|
// CacheHome defines the base directory relative to which user-specific
|
||||||
|
// non-essential (cached) data should be written. This directory is
|
||||||
|
// defined by the $XDG_CACHE_HOME environment variable. If the variable
|
||||||
|
// is not set, a default equal to $HOME/.cache should be used.
|
||||||
|
CacheHome string
|
||||||
|
|
||||||
|
// RuntimeDir defines the base directory relative to which user-specific
|
||||||
|
// non-essential runtime files and other file objects (such as sockets,
|
||||||
|
// named pipes, etc.) should be stored. This directory is defined by the
|
||||||
|
// $XDG_RUNTIME_DIR environment variable. If the variable is not set,
|
||||||
|
// applications should fall back to a replacement directory with similar
|
||||||
|
// capabilities. Applications should use this directory for communication
|
||||||
|
// and synchronization purposes and should not place larger files in it,
|
||||||
|
// since it might reside in runtime memory and cannot necessarily be
|
||||||
|
// swapped out to disk.
|
||||||
|
RuntimeDir string
|
||||||
|
|
||||||
|
// UserDirs defines the locations of well known user directories.
|
||||||
|
UserDirs UserDirectories
|
||||||
|
|
||||||
|
// FontDirs defines the common locations where font files are stored.
|
||||||
|
FontDirs []string
|
||||||
|
|
||||||
|
// ApplicationDirs defines the common locations of applications.
|
||||||
|
ApplicationDirs []string
|
||||||
|
|
||||||
|
// baseDirs defines the locations of base directories.
|
||||||
|
baseDirs baseDirectories
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload refreshes base and user directories by reading the environment.
|
||||||
|
// Defaults are applied for XDG variables which are empty or not present
|
||||||
|
// in the environment.
|
||||||
|
func Reload() {
|
||||||
|
// Initialize home directory.
|
||||||
|
Home = homeDir()
|
||||||
|
|
||||||
|
// Initialize base and user directories.
|
||||||
|
initDirs(Home)
|
||||||
|
|
||||||
|
// Set standard directories.
|
||||||
|
DataHome = baseDirs.dataHome
|
||||||
|
DataDirs = baseDirs.data
|
||||||
|
ConfigHome = baseDirs.configHome
|
||||||
|
ConfigDirs = baseDirs.config
|
||||||
|
StateHome = baseDirs.stateHome
|
||||||
|
CacheHome = baseDirs.cacheHome
|
||||||
|
RuntimeDir = baseDirs.runtime
|
||||||
|
|
||||||
|
// Set non-standard directories.
|
||||||
|
FontDirs = baseDirs.fonts
|
||||||
|
ApplicationDirs = baseDirs.applications
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataFile returns a suitable location for the specified data file.
|
||||||
|
// The relPath parameter must contain the name of the data file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.data).
|
||||||
|
// If the specified directories do not exist, they will be created relative
|
||||||
|
// to the base data directory. On failure, an error containing the
|
||||||
|
// attempted paths is returned.
|
||||||
|
func DataFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.dataFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigFile returns a suitable location for the specified config file.
|
||||||
|
// The relPath parameter must contain the name of the config file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.yaml).
|
||||||
|
// If the specified directories do not exist, they will be created relative
|
||||||
|
// to the base config directory. On failure, an error containing the
|
||||||
|
// attempted paths is returned.
|
||||||
|
func ConfigFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.configFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateFile returns a suitable location for the specified state file. State
|
||||||
|
// files are usually volatile data files, not suitable to be stored relative
|
||||||
|
// to the $XDG_DATA_HOME directory.
|
||||||
|
// The relPath parameter must contain the name of the state file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.state).
|
||||||
|
// If the specified directories do not exist, they will be created relative
|
||||||
|
// to the base state directory. On failure, an error containing the
|
||||||
|
// attempted paths is returned.
|
||||||
|
func StateFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.stateFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheFile returns a suitable location for the specified cache file.
|
||||||
|
// The relPath parameter must contain the name of the cache file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.cache).
|
||||||
|
// If the specified directories do not exist, they will be created relative
|
||||||
|
// to the base cache directory. On failure, an error containing the
|
||||||
|
// attempted paths is returned.
|
||||||
|
func CacheFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.cacheFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuntimeFile returns a suitable location for the specified runtime file.
|
||||||
|
// The relPath parameter must contain the name of the runtime file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.pid).
|
||||||
|
// If the specified directories do not exist, they will be created relative
|
||||||
|
// to the base runtime directory. On failure, an error containing the
|
||||||
|
// attempted paths is returned.
|
||||||
|
func RuntimeFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.runtimeFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchDataFile searches for specified file in the data search paths.
|
||||||
|
// The relPath parameter must contain the name of the data file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.data). If the
|
||||||
|
// file cannot be found, an error specifying the searched paths is returned.
|
||||||
|
func SearchDataFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.searchDataFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchConfigFile searches for the specified file in config search paths.
|
||||||
|
// The relPath parameter must contain the name of the config file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.yaml). If the
|
||||||
|
// file cannot be found, an error specifying the searched paths is returned.
|
||||||
|
func SearchConfigFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.searchConfigFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchStateFile searches for the specified file in the state search path.
|
||||||
|
// The relPath parameter must contain the name of the state file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.state). If the
|
||||||
|
// file cannot be found, an error specifying the searched path is returned.
|
||||||
|
func SearchStateFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.searchStateFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchCacheFile searches for the specified file in the cache search path.
|
||||||
|
// The relPath parameter must contain the name of the cache file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.cache). If the
|
||||||
|
// file cannot be found, an error specifying the searched path is returned.
|
||||||
|
func SearchCacheFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.searchCacheFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchRuntimeFile searches for the specified file in the runtime search path.
|
||||||
|
// The relPath parameter must contain the name of the runtime file, and
|
||||||
|
// optionally, a set of parent directories (e.g. appname/app.pid). If the
|
||||||
|
// file cannot be found, an error specifying the searched path is returned.
|
||||||
|
func SearchRuntimeFile(relPath string) (string, error) {
|
||||||
|
return baseDirs.searchRuntimeFile(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func xdgPath(name, defaultPath string) string {
|
||||||
|
dir := pathutil.ExpandHome(os.Getenv(name), Home)
|
||||||
|
if dir != "" && filepath.IsAbs(dir) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func xdgPaths(name string, defaultPaths ...string) []string {
|
||||||
|
dirs := pathutil.Unique(filepath.SplitList(os.Getenv(name)), Home)
|
||||||
|
if len(dirs) != 0 {
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathutil.Unique(defaultPaths, Home)
|
||||||
|
}
|
||||||
1
vendor/github.com/distatus/battery/.gitignore
generated
vendored
Normal file
1
vendor/github.com/distatus/battery/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cmd/battery/battery*
|
||||||
7
vendor/github.com/distatus/battery/BREAKING.md
generated
vendored
Normal file
7
vendor/github.com/distatus/battery/BREAKING.md
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 0.11.0
|
||||||
|
|
||||||
|
The `State` field is no longer just an `int`, but a separate `struct`.
|
||||||
|
|
||||||
|
It still implements the `Stringer` and `GoStringer` interfaces, so if you were just displaying the value, it should continue working as before.
|
||||||
|
|
||||||
|
For comparison purposes, the `State.Raw` field should be used now. It has all the same values as before + 2 new (`Undefined` and `Idle`).
|
||||||
21
vendor/github.com/distatus/battery/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/distatus/battery/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2016 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
73
vendor/github.com/distatus/battery/README.md
generated
vendored
Normal file
73
vendor/github.com/distatus/battery/README.md
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
battery [](https://github.com/distatus/battery/actions/workflows/tests.yml) [](https://goreportcard.com/report/github.com/distatus/battery) [](https://pkg.go.dev/github.com/distatus/battery)
|
||||||
|
=======
|
||||||
|
|
||||||
|
Cross-platform, normalized battery information library.
|
||||||
|
|
||||||
|
Gives access to a system independent, typed battery state, capacity, charge and voltage values recalculated as necessary to be returned in mW, mWh or V units.
|
||||||
|
|
||||||
|
Currently supported systems:
|
||||||
|
|
||||||
|
* Linux 2.6.39+
|
||||||
|
* OS X 10.10+
|
||||||
|
* Windows XP+
|
||||||
|
* FreeBSD
|
||||||
|
* DragonFlyBSD
|
||||||
|
* NetBSD
|
||||||
|
* OpenBSD
|
||||||
|
* Solaris
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go get -u github.com/distatus/battery
|
||||||
|
```
|
||||||
|
|
||||||
|
Code Example
|
||||||
|
------------
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/distatus/battery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
batteries, err := battery.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Could not get battery info!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, battery := range batteries {
|
||||||
|
fmt.Printf("Bat%d: ", i)
|
||||||
|
fmt.Printf("state: %s, ", battery.State.String())
|
||||||
|
fmt.Printf("current capacity: %f mWh, ", battery.Current)
|
||||||
|
fmt.Printf("last full capacity: %f mWh, ", battery.Full)
|
||||||
|
fmt.Printf("design capacity: %f mWh, ", battery.Design)
|
||||||
|
fmt.Printf("charge rate: %f mW, ", battery.ChargeRate)
|
||||||
|
fmt.Printf("voltage: %f V, ", battery.Voltage)
|
||||||
|
fmt.Printf("design voltage: %f V\n", battery.DesignVoltage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI
|
||||||
|
---
|
||||||
|
|
||||||
|
There is also a little utility which - more or less - mimicks the GNU/Linux `acpi -b` command.
|
||||||
|
|
||||||
|
*Installation*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go install github.com/distatus/battery/cmd/battery@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
*Usage*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ battery
|
||||||
|
BAT0: Full, 95.61% [Voltage: 12.15V (design: 12.15V)]
|
||||||
|
```
|
||||||
174
vendor/github.com/distatus/battery/battery.go
generated
vendored
Normal file
174
vendor/github.com/distatus/battery/battery.go
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
// Package battery provides cross-platform, normalized battery information.
|
||||||
|
//
|
||||||
|
// Gives access to a system independent, typed battery state, capacity, charge and voltage values recalculated as necessary to be returned in mW, mWh or V units.
|
||||||
|
//
|
||||||
|
// Currently supported systems:
|
||||||
|
//
|
||||||
|
// Linux 2.6.39+
|
||||||
|
// OS X 10.10+
|
||||||
|
// Windows XP+
|
||||||
|
// FreeBSD
|
||||||
|
// DragonFlyBSD
|
||||||
|
// NetBSD
|
||||||
|
// OpenBSD
|
||||||
|
// Solaris
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgnosticState type enumerates possible battery states, using platform agnostic naming.
|
||||||
|
type AgnosticState int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Undefined specifies a state that was returned by the controller, but there is no
|
||||||
|
// platform agnostic mapping for it.
|
||||||
|
// This generally shouldn't happen, if it does consider opening a report for the library
|
||||||
|
// (ideally with the contents of `State.Explain()` call as well).
|
||||||
|
Undefined AgnosticState = -1
|
||||||
|
// Unknown specifies a literal unknown state returned by the controller.
|
||||||
|
// This state is also considered the "default", therefore it will be set when an Error
|
||||||
|
// was returned from the Get/GetAll call.
|
||||||
|
Unknown AgnosticState = iota - 1
|
||||||
|
Empty
|
||||||
|
Full
|
||||||
|
Charging
|
||||||
|
Discharging
|
||||||
|
// Idle specifies a state where battery is in "capacity saving" mode.
|
||||||
|
// It usually means that it sits idle at around 80% charge while power source is plugged in.
|
||||||
|
Idle
|
||||||
|
)
|
||||||
|
|
||||||
|
var states = map[AgnosticState]string{
|
||||||
|
Undefined: "Undefined",
|
||||||
|
Unknown: "Unknown",
|
||||||
|
Empty: "Empty",
|
||||||
|
Full: "Full",
|
||||||
|
Charging: "Charging",
|
||||||
|
Discharging: "Discharging",
|
||||||
|
Idle: "Idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AgnosticState) String() string {
|
||||||
|
return states[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Raw AgnosticState
|
||||||
|
specific string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s State) Explain() string {
|
||||||
|
return s.specific
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s State) String() string {
|
||||||
|
return s.Raw.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s State) GoString() string {
|
||||||
|
return fmt.Sprintf("%s (%s)", s.Raw, s.specific)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery type represents a single battery entry information.
|
||||||
|
type Battery struct {
|
||||||
|
// Current battery state.
|
||||||
|
State State
|
||||||
|
// Current (momentary) capacity (in mWh).
|
||||||
|
Current float64
|
||||||
|
// Last known full capacity (in mWh).
|
||||||
|
Full float64
|
||||||
|
// Reported design capacity (in mWh).
|
||||||
|
Design float64
|
||||||
|
// Current (momentary) charge rate (in mW).
|
||||||
|
// It is always non-negative, consult .State field to check
|
||||||
|
// whether it means charging or discharging.
|
||||||
|
ChargeRate float64
|
||||||
|
// Current voltage (in V).
|
||||||
|
Voltage float64
|
||||||
|
// Design voltage (in V).
|
||||||
|
// Some systems (e.g. macOS) do not provide a separate
|
||||||
|
// value for this. In such cases, or if getting this fails,
|
||||||
|
// but getting `Voltage` succeeds, this field will have
|
||||||
|
// the same value as `Voltage`, for convenience.
|
||||||
|
DesignVoltage float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Battery) String() string {
|
||||||
|
return fmt.Sprintf("%+v", *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(sg func(idx int) (*Battery, error), idx int) (*Battery, error) {
|
||||||
|
b, err := sg(idx)
|
||||||
|
return b, wrapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns battery information for given index.
|
||||||
|
//
|
||||||
|
// Note that index taken here is normalized, such that GetAll()[idx] == Get(idx).
|
||||||
|
// It does not necessarily represent the "name" or "position" a battery was given
|
||||||
|
// by the underlying system.
|
||||||
|
//
|
||||||
|
// If error != nil, it will be either ErrFatal or ErrPartial.
|
||||||
|
func Get(idx int) (*Battery, error) {
|
||||||
|
return get(systemGet, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAll(sg func() ([]*Battery, error)) ([]*Battery, error) {
|
||||||
|
bs, err := sg()
|
||||||
|
if errors, ok := err.(Errors); ok {
|
||||||
|
nils := 0
|
||||||
|
partials := 0
|
||||||
|
for i, err := range errors {
|
||||||
|
err = wrapError(err)
|
||||||
|
if err == nil {
|
||||||
|
nils++
|
||||||
|
}
|
||||||
|
if _, ok := err.(ErrPartial); ok {
|
||||||
|
partials++
|
||||||
|
}
|
||||||
|
errors[i] = err
|
||||||
|
}
|
||||||
|
if nils == len(errors) {
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
if nils > 0 || partials > 0 {
|
||||||
|
return bs, errors
|
||||||
|
}
|
||||||
|
return nil, ErrFatal{ErrAllNotNil}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return bs, ErrFatal{err}
|
||||||
|
}
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns information about all batteries in the system.
|
||||||
|
//
|
||||||
|
// If error != nil, it will be either ErrFatal or Errors.
|
||||||
|
// If error is of type Errors, it is guaranteed that length of both returned slices is the same and that i-th error coresponds with i-th battery structure.
|
||||||
|
func GetAll() ([]*Battery, error) {
|
||||||
|
return getAll(systemGetAll)
|
||||||
|
}
|
||||||
117
vendor/github.com/distatus/battery/battery_darwin.go
generated
vendored
Normal file
117
vendor/github.com/distatus/battery/battery_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
plist "howett.net/plist"
|
||||||
|
)
|
||||||
|
|
||||||
|
type battery struct {
|
||||||
|
Voltage int
|
||||||
|
CurrentCapacity int `plist:"AppleRawCurrentCapacity"`
|
||||||
|
MaxCapacity int `plist:"AppleRawMaxCapacity"`
|
||||||
|
DesignCapacity int
|
||||||
|
Amperage int64
|
||||||
|
FullyCharged bool
|
||||||
|
IsCharging bool
|
||||||
|
ExternalConnected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBatteries() ([]*battery, error) {
|
||||||
|
out, err := exec.Command("ioreg", "-n", "AppleSmartBattery", "-r", "-a").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
// No batteries.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*battery
|
||||||
|
if _, err = plist.Unmarshal(out, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBattery(battery *battery) *Battery {
|
||||||
|
volts := float64(battery.Voltage) / 1000
|
||||||
|
b := &Battery{
|
||||||
|
Current: float64(battery.CurrentCapacity) * volts,
|
||||||
|
Full: float64(battery.MaxCapacity) * volts,
|
||||||
|
Design: float64(battery.DesignCapacity) * volts,
|
||||||
|
ChargeRate: math.Abs(float64(battery.Amperage)) * volts,
|
||||||
|
Voltage: volts,
|
||||||
|
DesignVoltage: volts,
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !battery.ExternalConnected:
|
||||||
|
b.State.Raw = Discharging
|
||||||
|
b.State.specific = "not(ExternalConnected)"
|
||||||
|
case battery.IsCharging:
|
||||||
|
b.State.Raw = Charging
|
||||||
|
b.State.specific = "IsCharging"
|
||||||
|
case battery.CurrentCapacity == 0:
|
||||||
|
b.State.Raw = Empty
|
||||||
|
b.State.specific = "CurrentCapacity is 0"
|
||||||
|
case !battery.FullyCharged:
|
||||||
|
b.State.Raw = Idle
|
||||||
|
b.State.specific = "not(FullyCharged)"
|
||||||
|
case battery.FullyCharged:
|
||||||
|
b.State.Raw = Full
|
||||||
|
b.State.specific = "FullyCharged"
|
||||||
|
default:
|
||||||
|
b.State.Raw = Undefined
|
||||||
|
b.State.specific = fmt.Sprintf("%+v", *battery)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
batteries, err := readBatteries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx >= len(batteries) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return convertBattery(batteries[idx]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
_batteries, err := readBatteries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
batteries := make([]*Battery, len(_batteries))
|
||||||
|
for i, battery := range _batteries {
|
||||||
|
batteries[i] = convertBattery(battery)
|
||||||
|
}
|
||||||
|
return batteries, nil
|
||||||
|
}
|
||||||
153
vendor/github.com/distatus/battery/battery_freebsd_dragonfly_.go
generated
vendored
Normal file
153
vendor/github.com/distatus/battery/battery_freebsd_dragonfly_.go
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
//go:build freebsd || dragonfly
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readUint32(bytes []byte) uint32 {
|
||||||
|
var ret uint32
|
||||||
|
for i, b := range bytes {
|
||||||
|
ret |= uint32(b) << uint(i*8)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint32ToFloat64(num uint32) (float64, error) {
|
||||||
|
if num == 0xffffffff {
|
||||||
|
return 0, errors.New("Unknown value received")
|
||||||
|
}
|
||||||
|
return float64(num), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctl_(fd int, nr int64, retptr *[164]byte) error {
|
||||||
|
return ioctl(fd, nr, 'B', unsafe.Sizeof(*retptr), unsafe.Pointer(retptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
fd, err := unix.Open("/dev/acpi", unix.O_RDONLY, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer unix.Close(fd)
|
||||||
|
|
||||||
|
b := &Battery{}
|
||||||
|
e := ErrPartial{}
|
||||||
|
|
||||||
|
// No unions in Go, so lets "emulate" union with byte array ;-].
|
||||||
|
var retptr [164]byte
|
||||||
|
// XXX What about byte encoding idx and storing it in retptr?
|
||||||
|
// Then we wouldn't need this unsafe pointer manipulations
|
||||||
|
unit := (*int)(unsafe.Pointer(&retptr[0]))
|
||||||
|
|
||||||
|
*unit = idx
|
||||||
|
err = ioctl_(fd, 0x10, &retptr) // ACPIIO_BATT_GET_BIF
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mw := readUint32(retptr[0:4]) == 0 // acpi_bif.units
|
||||||
|
|
||||||
|
b.Design, e.Design = uint32ToFloat64(readUint32(retptr[4:8])) // acpi_bif.dcap
|
||||||
|
b.Full, e.Full = uint32ToFloat64(readUint32(retptr[8:12])) // acpi_bif.lfcap
|
||||||
|
b.DesignVoltage, e.DesignVoltage = uint32ToFloat64(readUint32(retptr[16:20])) // acpi_bif.dvol
|
||||||
|
b.DesignVoltage /= 1000
|
||||||
|
|
||||||
|
*unit = idx
|
||||||
|
err = ioctl_(fd, 0x11, &retptr) // APCIIO_BATT_GET_BST
|
||||||
|
if err == nil {
|
||||||
|
state := readUint32(retptr[0:4]) // acpi_bst.state
|
||||||
|
b.State.specific = fmt.Sprintf("%x", state)
|
||||||
|
switch state {
|
||||||
|
case 0x0000:
|
||||||
|
b.State.Raw = Full
|
||||||
|
case 0x0001:
|
||||||
|
b.State.Raw = Discharging
|
||||||
|
case 0x0002:
|
||||||
|
b.State.Raw = Charging
|
||||||
|
case 0x0004:
|
||||||
|
b.State.Raw = Empty
|
||||||
|
default:
|
||||||
|
b.State.Raw = Undefined
|
||||||
|
}
|
||||||
|
b.ChargeRate, e.ChargeRate = uint32ToFloat64(readUint32(retptr[4:8])) // acpi_bst.rate
|
||||||
|
b.Current, e.Current = uint32ToFloat64(readUint32(retptr[8:12])) // acpi_bst.cap
|
||||||
|
b.Voltage, e.Voltage = uint32ToFloat64(readUint32(retptr[12:16])) // acpi_bst.volt
|
||||||
|
b.Voltage /= 1000
|
||||||
|
} else {
|
||||||
|
e.State = err
|
||||||
|
e.ChargeRate = err
|
||||||
|
e.Current = err
|
||||||
|
e.Voltage = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.DesignVoltage != nil && e.Voltage == nil {
|
||||||
|
b.DesignVoltage, e.DesignVoltage = b.Voltage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mw {
|
||||||
|
if e.DesignVoltage == nil {
|
||||||
|
b.Design *= b.DesignVoltage
|
||||||
|
} else {
|
||||||
|
e.Design = e.DesignVoltage
|
||||||
|
}
|
||||||
|
if e.Voltage == nil {
|
||||||
|
b.Full *= b.Voltage
|
||||||
|
b.ChargeRate *= b.Voltage
|
||||||
|
b.Current *= b.Voltage
|
||||||
|
} else {
|
||||||
|
e.Full = e.Voltage
|
||||||
|
e.ChargeRate = e.Voltage
|
||||||
|
e.Current = e.Voltage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, e
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is no way to iterate over available batteries.
|
||||||
|
// Therefore we assume here that if we were not able to retrieve
|
||||||
|
// anything, it means we're done.
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
var batteries []*Battery
|
||||||
|
var errors Errors
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
b, err := systemGet(i)
|
||||||
|
if perr, ok := err.(ErrPartial); ok && perr.noNil() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errno, ok := err.(syscall.Errno); ok && errno == 6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
batteries = append(batteries, b)
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
165
vendor/github.com/distatus/battery/battery_linux.go
generated
vendored
Normal file
165
vendor/github.com/distatus/battery/battery_linux.go
generated
vendored
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sysfs = "/sys/class/power_supply"
|
||||||
|
|
||||||
|
func readFloat(path, filename string) (float64, error) {
|
||||||
|
str, err := ioutil.ReadFile(filepath.Join(path, filename))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
num, err := strconv.ParseFloat(string(str[:len(str)-1]), 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return num / 1000, nil // Convert micro->milli
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAmp(path, filename string, volts float64) (float64, error) {
|
||||||
|
val, err := readFloat(path, filename)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return val * volts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBattery(path string) bool {
|
||||||
|
t, err := ioutil.ReadFile(filepath.Join(path, "type"))
|
||||||
|
return err == nil && string(t) == "Battery\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBatteryFiles() ([]string, error) {
|
||||||
|
files, err := ioutil.ReadDir(sysfs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bFiles []string
|
||||||
|
for _, file := range files {
|
||||||
|
path := filepath.Join(sysfs, file.Name())
|
||||||
|
if isBattery(path) {
|
||||||
|
bFiles = append(bFiles, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getByPath(path string) (*Battery, error) {
|
||||||
|
b := &Battery{}
|
||||||
|
e := ErrPartial{}
|
||||||
|
b.Current, e.Current = readFloat(path, "energy_now")
|
||||||
|
b.Voltage, e.Voltage = readFloat(path, "voltage_now")
|
||||||
|
b.Voltage /= 1000
|
||||||
|
|
||||||
|
b.DesignVoltage, e.DesignVoltage = readFloat(path, "voltage_max_design")
|
||||||
|
if e.DesignVoltage != nil {
|
||||||
|
b.DesignVoltage, e.DesignVoltage = readFloat(path, "voltage_min_design")
|
||||||
|
}
|
||||||
|
if e.DesignVoltage != nil && e.Voltage == nil {
|
||||||
|
b.DesignVoltage, e.DesignVoltage = b.Voltage, nil
|
||||||
|
}
|
||||||
|
b.DesignVoltage /= 1000
|
||||||
|
|
||||||
|
if os.IsNotExist(e.Current) {
|
||||||
|
if e.DesignVoltage == nil {
|
||||||
|
b.Design, e.Design = readAmp(path, "charge_full_design", b.DesignVoltage)
|
||||||
|
} else {
|
||||||
|
e.Design = e.DesignVoltage
|
||||||
|
}
|
||||||
|
if e.Voltage == nil {
|
||||||
|
b.Current, e.Current = readAmp(path, "charge_now", b.Voltage)
|
||||||
|
b.Full, e.Full = readAmp(path, "charge_full", b.Voltage)
|
||||||
|
b.ChargeRate, e.ChargeRate = readAmp(path, "current_now", b.Voltage)
|
||||||
|
} else {
|
||||||
|
e.Current = e.Voltage
|
||||||
|
e.Full = e.Voltage
|
||||||
|
e.ChargeRate = e.Voltage
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.Full, e.Full = readFloat(path, "energy_full")
|
||||||
|
b.Design, e.Design = readFloat(path, "energy_full_design")
|
||||||
|
b.ChargeRate, e.ChargeRate = readFloat(path, "power_now")
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := ioutil.ReadFile(filepath.Join(path, "status"))
|
||||||
|
if err == nil {
|
||||||
|
status := string(status[:len(status)-1])
|
||||||
|
b.State.specific = status
|
||||||
|
switch status {
|
||||||
|
case "Unknown":
|
||||||
|
b.State.Raw = Unknown
|
||||||
|
case "Empty":
|
||||||
|
b.State.Raw = Empty
|
||||||
|
case "Full":
|
||||||
|
b.State.Raw = Full
|
||||||
|
case "Charging":
|
||||||
|
b.State.Raw = Charging
|
||||||
|
case "Discharging":
|
||||||
|
b.State.Raw = Discharging
|
||||||
|
case "Not charging":
|
||||||
|
b.State.Raw = Idle
|
||||||
|
default:
|
||||||
|
b.State.Raw = Undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.State = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
bFiles, err := getBatteryFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx >= len(bFiles) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return getByPath(bFiles[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
bFiles, err := getBatteryFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
batteries := make([]*Battery, len(bFiles))
|
||||||
|
errors := make(Errors, len(bFiles))
|
||||||
|
for i, bFile := range bFiles {
|
||||||
|
battery, err := getByPath(bFile)
|
||||||
|
batteries[i] = battery
|
||||||
|
errors[i] = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
221
vendor/github.com/distatus/battery/battery_netbsd.go
generated
vendored
Normal file
221
vendor/github.com/distatus/battery/battery_netbsd.go
generated
vendored
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
plist "howett.net/plist"
|
||||||
|
)
|
||||||
|
|
||||||
|
type plistref struct {
|
||||||
|
pref_plist unsafe.Pointer
|
||||||
|
pref_len uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type values struct {
|
||||||
|
Description string `plist:"description"`
|
||||||
|
CurValue int `plist:"cur-value"`
|
||||||
|
MaxValue int `plist:"max-value"`
|
||||||
|
State string `plist:"state"`
|
||||||
|
Type string `plist:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type prop []values
|
||||||
|
|
||||||
|
type props map[string]prop
|
||||||
|
|
||||||
|
func readBytes(ptr unsafe.Pointer, length uint64) []byte {
|
||||||
|
buf := make([]byte, length-1)
|
||||||
|
var i uint64
|
||||||
|
for ; i < length-1; i++ {
|
||||||
|
buf[i] = *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(i)))
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func readProps() (props, error) {
|
||||||
|
fd, err := unix.Open("/dev/sysmon", unix.O_RDONLY, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer unix.Close(fd)
|
||||||
|
|
||||||
|
var retptr plistref
|
||||||
|
|
||||||
|
if err = ioctl(fd, 0, 'E', unsafe.Sizeof(retptr), unsafe.Pointer(&retptr)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bytes := readBytes(retptr.pref_plist, retptr.pref_len)
|
||||||
|
|
||||||
|
var props props
|
||||||
|
if _, err = plist.Unmarshal(bytes, &props); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return props, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleValue(val values, div float64, res *float64, amps *[]string) error {
|
||||||
|
if val.State == "invalid" || val.State == "unknown" {
|
||||||
|
return errors.New("Unknown value received")
|
||||||
|
}
|
||||||
|
|
||||||
|
*res = float64(val.CurValue) / div
|
||||||
|
|
||||||
|
if amps != nil && strings.HasPrefix(val.Type, "Amp") {
|
||||||
|
*amps = append(*amps, val.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveState(cr1, cr2 error, current float64, max int) (AgnosticState, error) {
|
||||||
|
if cr1 == nil && cr2 != nil {
|
||||||
|
return Charging, nil
|
||||||
|
}
|
||||||
|
if cr1 != nil && cr2 == nil {
|
||||||
|
return Discharging, nil
|
||||||
|
}
|
||||||
|
if cr1 != nil && cr2 != nil && current == float64(max)/1000 {
|
||||||
|
return Full, nil
|
||||||
|
}
|
||||||
|
return Unknown, errors.New("Contradicting values received")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleVoltage(amps []string, b *Battery, e *ErrPartial) {
|
||||||
|
if e.DesignVoltage != nil && e.Voltage == nil {
|
||||||
|
b.DesignVoltage, e.DesignVoltage = b.Voltage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, val := range amps {
|
||||||
|
switch val {
|
||||||
|
case "design cap":
|
||||||
|
if e.DesignVoltage == nil {
|
||||||
|
b.Design *= b.DesignVoltage
|
||||||
|
} else {
|
||||||
|
e.Design = e.DesignVoltage
|
||||||
|
}
|
||||||
|
case "last full cap":
|
||||||
|
if e.Voltage == nil {
|
||||||
|
b.Full *= b.Voltage
|
||||||
|
} else {
|
||||||
|
e.Full = e.Voltage
|
||||||
|
}
|
||||||
|
case "charge":
|
||||||
|
if e.Voltage == nil {
|
||||||
|
b.Current *= b.Voltage
|
||||||
|
} else {
|
||||||
|
e.Current = e.Voltage
|
||||||
|
}
|
||||||
|
case "charge rate", "discharge rate":
|
||||||
|
if e.Voltage == nil {
|
||||||
|
b.ChargeRate *= b.Voltage
|
||||||
|
} else {
|
||||||
|
e.ChargeRate = e.Voltage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortFilterProps(props props) []string {
|
||||||
|
var keys []string
|
||||||
|
for key := range props {
|
||||||
|
if key[:7] != "acpibat" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBattery(prop prop) (*Battery, error) {
|
||||||
|
b := &Battery{}
|
||||||
|
e := ErrPartial{}
|
||||||
|
|
||||||
|
amps := []string{}
|
||||||
|
var cr1, cr2 error
|
||||||
|
var maxCharge int
|
||||||
|
|
||||||
|
for _, val := range prop {
|
||||||
|
switch val.Description {
|
||||||
|
case "voltage":
|
||||||
|
e.Voltage = handleValue(val, 1000000, &b.Voltage, nil)
|
||||||
|
case "design voltage":
|
||||||
|
e.DesignVoltage = handleValue(val, 1000000, &b.DesignVoltage, nil)
|
||||||
|
case "design cap":
|
||||||
|
e.Design = handleValue(val, 1000, &b.Design, &s)
|
||||||
|
case "last full cap":
|
||||||
|
e.Full = handleValue(val, 1000, &b.Full, &s)
|
||||||
|
case "charge":
|
||||||
|
e.Current = handleValue(val, 1000, &b.Current, &s)
|
||||||
|
maxCharge = val.MaxValue
|
||||||
|
case "charge rate":
|
||||||
|
cr1 = handleValue(val, 1000, &b.ChargeRate, &s)
|
||||||
|
case "discharge rate":
|
||||||
|
cr2 = handleValue(val, 1000, &b.ChargeRate, &s)
|
||||||
|
b.ChargeRate = math.Abs(b.ChargeRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.State.Raw, e.State = deriveState(cr1, cr2, b.Current, maxCharge)
|
||||||
|
b.State.specific = fmt.Sprintf("cr1: %v, cr2: %v")
|
||||||
|
|
||||||
|
handleVoltage(amps, b, &e)
|
||||||
|
|
||||||
|
return b, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
props, err := readProps()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := sortFilterProps(props)
|
||||||
|
if idx >= len(keys) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return convertBattery(props[keys[idx]])
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
props, err := readProps()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := sortFilterProps(props)
|
||||||
|
batteries := make([]*Battery, len(keys))
|
||||||
|
errors := make(Errors, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
batteries[i], errors[i] = convertBattery(props[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
282
vendor/github.com/distatus/battery/battery_openbsd.go
generated
vendored
Normal file
282
vendor/github.com/distatus/battery/battery_openbsd.go
generated
vendored
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2019,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sysctl(mib []int32, out unsafe.Pointer, n uintptr) unix.Errno {
|
||||||
|
_, _, e := unix.Syscall6(
|
||||||
|
unix.SYS___SYSCTL,
|
||||||
|
uintptr(unsafe.Pointer(&mib[0])),
|
||||||
|
uintptr(len(mib)),
|
||||||
|
uintptr(out),
|
||||||
|
uintptr(unsafe.Pointer(&n)),
|
||||||
|
uintptr(unsafe.Pointer(nil)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
var errValueNotFound = fmt.Errorf("Value not found")
|
||||||
|
|
||||||
|
func setErr(err *error, newErr error) {
|
||||||
|
if *err == errValueNotFound {
|
||||||
|
*err = newErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sensorW = [4]int32{
|
||||||
|
2, // SENSOR_VOLTS_DC (uV)
|
||||||
|
5, // SENSOR_WATTS (uW)
|
||||||
|
7, // SENSOR_WATTHOUR (uWh)
|
||||||
|
10, // SENSOR_INTEGER
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sensorA = 6 // SENSOR_AMPS (uA)
|
||||||
|
sensorAH = 8 // SENSOR_AMPHOUR (uAh)
|
||||||
|
)
|
||||||
|
|
||||||
|
type sensorStatus int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
unspecified sensorStatus = iota
|
||||||
|
ok
|
||||||
|
warning
|
||||||
|
critical
|
||||||
|
unknown
|
||||||
|
)
|
||||||
|
|
||||||
|
type sensor struct {
|
||||||
|
desc [32]byte
|
||||||
|
tv [16]byte // struct timeval
|
||||||
|
value int64
|
||||||
|
typ [4]byte // enum sensor_type
|
||||||
|
status sensorStatus
|
||||||
|
numt int32
|
||||||
|
flags int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sensor) readValue(div float64) (float64, error) {
|
||||||
|
if s.status == unknown {
|
||||||
|
return 0, fmt.Errorf("Unknown value received")
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(s.value) / div, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sensor) handleA(factor float64, field *float64, fieldErr *error) {
|
||||||
|
*field, *fieldErr = s.readValue(1000)
|
||||||
|
if *fieldErr == nil {
|
||||||
|
*field *= factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sensor) handleAH(factor float64, factorErr error, field *float64, fieldErr *error) {
|
||||||
|
if factorErr != nil {
|
||||||
|
*fieldErr = factorErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.handleA(factor, field, fieldErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sensordev struct {
|
||||||
|
num int32
|
||||||
|
xname [16]byte
|
||||||
|
maxnumt [23]int32
|
||||||
|
sensors_count int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *sensordev) get() (*Battery, error) {
|
||||||
|
var battery Battery
|
||||||
|
err := ErrPartial{
|
||||||
|
Design: errValueNotFound,
|
||||||
|
Full: errValueNotFound,
|
||||||
|
Current: errValueNotFound,
|
||||||
|
ChargeRate: errValueNotFound,
|
||||||
|
State: errValueNotFound,
|
||||||
|
Voltage: errValueNotFound,
|
||||||
|
DesignVoltage: errValueNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
mib := []int32{unix.CTL_HW, 11, sd.num, 0, 0}
|
||||||
|
var s sensor
|
||||||
|
|
||||||
|
iter := func(maxnumtIdx int32, cb func(string)) {
|
||||||
|
mib[3] = maxnumtIdx
|
||||||
|
|
||||||
|
for i := int32(0); i < sd.maxnumt[maxnumtIdx]; i++ {
|
||||||
|
mib[4] = i
|
||||||
|
|
||||||
|
if errno := sysctl(mib, unsafe.Pointer(&s), unsafe.Sizeof(s)); errno != 0 {
|
||||||
|
setErr(&err.Design, errno)
|
||||||
|
setErr(&err.Full, errno)
|
||||||
|
setErr(&err.Current, errno)
|
||||||
|
setErr(&err.ChargeRate, errno)
|
||||||
|
setErr(&err.Voltage, errno)
|
||||||
|
setErr(&err.DesignVoltage, errno)
|
||||||
|
setErr(&err.State, errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert 0-terminated C-string to a Go string
|
||||||
|
cb(string(s.desc[:bytes.IndexByte(s.desc[:], 0)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range sensorW {
|
||||||
|
mib[3] = w
|
||||||
|
|
||||||
|
iter(w, func(desc string) {
|
||||||
|
if strings.HasPrefix(desc, "battery ") {
|
||||||
|
battery.State.specific, err.State = desc, nil
|
||||||
|
|
||||||
|
switch desc[8:] {
|
||||||
|
case "unknown":
|
||||||
|
battery.State.Raw = Unknown
|
||||||
|
case "full":
|
||||||
|
battery.State.Raw = Full
|
||||||
|
case "charging":
|
||||||
|
battery.State.Raw = Charging
|
||||||
|
case "discharging":
|
||||||
|
battery.State.Raw = Discharging
|
||||||
|
case "idle":
|
||||||
|
battery.State.Raw = Idle
|
||||||
|
case "critical":
|
||||||
|
battery.State.Raw = Empty
|
||||||
|
default:
|
||||||
|
battery.State.Raw = Undefined
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch desc {
|
||||||
|
case "rate":
|
||||||
|
battery.ChargeRate, err.ChargeRate = s.readValue(1000)
|
||||||
|
case "design capacity":
|
||||||
|
battery.Design, err.Design = s.readValue(1000)
|
||||||
|
case "last full capacity":
|
||||||
|
battery.Full, err.Full = s.readValue(1000)
|
||||||
|
case "remaining capacity":
|
||||||
|
battery.Current, err.Current = s.readValue(1000)
|
||||||
|
case "current voltage":
|
||||||
|
battery.Voltage, err.Voltage = s.readValue(1000_000)
|
||||||
|
case "voltage":
|
||||||
|
battery.DesignVoltage, err.DesignVoltage = s.readValue(1000_000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.DesignVoltage != nil && err.Voltage == nil {
|
||||||
|
battery.DesignVoltage, err.DesignVoltage = battery.Voltage, nil
|
||||||
|
}
|
||||||
|
if err.ChargeRate == errValueNotFound {
|
||||||
|
if err.Voltage == nil {
|
||||||
|
iter(sensorA, func(desc string) {
|
||||||
|
if desc == "rate" {
|
||||||
|
s.handleA(battery.Voltage, &battery.ChargeRate, &err.ChargeRate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
err.ChargeRate = err.Voltage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err.Design == errValueNotFound || err.Full == errValueNotFound || err.Current == errValueNotFound {
|
||||||
|
iter(sensorAH, func(desc string) {
|
||||||
|
switch desc {
|
||||||
|
case "design capacity":
|
||||||
|
s.handleAH(battery.DesignVoltage, err.DesignVoltage, &battery.Design, &err.Design)
|
||||||
|
case "last full capacity":
|
||||||
|
s.handleAH(battery.Voltage, err.Voltage, &battery.Full, &err.Full)
|
||||||
|
case "remaining capacity":
|
||||||
|
s.handleAH(battery.Voltage, err.Voltage, &battery.Current, &err.Current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &battery, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBatteryAtMIBIndex(i int32) (*Battery, int32, error) {
|
||||||
|
mib := []int32{
|
||||||
|
unix.CTL_HW,
|
||||||
|
11, // HW_SENSORS
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
var sd sensordev
|
||||||
|
for ; ; i++ {
|
||||||
|
mib[2] = i
|
||||||
|
|
||||||
|
errno := sysctl(mib, unsafe.Pointer(&sd), unsafe.Sizeof(sd))
|
||||||
|
if errno == unix.ENOENT {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errno != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(sd.xname[:], []byte("acpibat")) {
|
||||||
|
battery, err := sd.get()
|
||||||
|
return battery, i + 1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
var i int32
|
||||||
|
for idxCurr := 0; ; idxCurr++ {
|
||||||
|
battery, iNext, err := getBatteryAtMIBIndex(i)
|
||||||
|
// If this is the index we look for, grab it.
|
||||||
|
// Otherwise just move on, regardless of errors etc.
|
||||||
|
if idxCurr == idx {
|
||||||
|
return battery, err
|
||||||
|
}
|
||||||
|
|
||||||
|
i = iNext
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
var batteries []*Battery
|
||||||
|
var errors Errors
|
||||||
|
|
||||||
|
var i int32
|
||||||
|
for {
|
||||||
|
battery, iNext, err := getBatteryAtMIBIndex(i)
|
||||||
|
if battery == nil && err == nil { // no more batteries
|
||||||
|
break
|
||||||
|
}
|
||||||
|
batteries = append(batteries, battery)
|
||||||
|
errors = append(errors, err)
|
||||||
|
|
||||||
|
i = iNext
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
260
vendor/github.com/distatus/battery/battery_solaris.go
generated
vendored
Normal file
260
vendor/github.com/distatus/battery/battery_solaris.go
generated
vendored
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errValueNotFound = fmt.Errorf("Value not found")
|
||||||
|
|
||||||
|
func readFloat(val string) (float64, error) {
|
||||||
|
num, err := strconv.ParseFloat(val, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if num == math.MaxUint32 {
|
||||||
|
return 0, fmt.Errorf("Unknown value received")
|
||||||
|
}
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readVoltage(val string) (float64, error) {
|
||||||
|
voltage, err := readFloat(val)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return voltage / 1000, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readState(val string) (AgnosticState, error) {
|
||||||
|
state, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return Unknown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case state&1 != 0:
|
||||||
|
return Discharging, nil
|
||||||
|
case state&2 != 0:
|
||||||
|
return Charging, nil
|
||||||
|
case state&4 != 0:
|
||||||
|
return Empty, nil
|
||||||
|
default:
|
||||||
|
return Undefined, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errParse int
|
||||||
|
|
||||||
|
func (p errParse) Error() string {
|
||||||
|
return fmt.Sprintf("Parse error: `%d`", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryReader struct {
|
||||||
|
cmdout *bufio.Scanner
|
||||||
|
li int
|
||||||
|
lline []byte
|
||||||
|
e ErrPartial
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *batteryReader) setErrParse(n int) {
|
||||||
|
if r.e.Design == errValueNotFound {
|
||||||
|
r.e.Design = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.Full == errValueNotFound {
|
||||||
|
r.e.Full = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.Current == errValueNotFound {
|
||||||
|
r.e.Current = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.ChargeRate == errValueNotFound {
|
||||||
|
r.e.ChargeRate = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.State == errValueNotFound {
|
||||||
|
r.e.State = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.Voltage == errValueNotFound {
|
||||||
|
r.e.Voltage = errParse(n)
|
||||||
|
}
|
||||||
|
if r.e.DesignVoltage == errValueNotFound {
|
||||||
|
r.e.DesignVoltage = errParse(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *batteryReader) readValue() (string, string, int) {
|
||||||
|
var piece []byte
|
||||||
|
if r.lline != nil {
|
||||||
|
piece = r.lline
|
||||||
|
r.lline = nil
|
||||||
|
} else {
|
||||||
|
pieces := bytes.Split(r.cmdout.Bytes(), []byte{':'})
|
||||||
|
if len(pieces) < 4 {
|
||||||
|
return "", "", 4
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(string(pieces[1]))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != r.li {
|
||||||
|
r.li = i
|
||||||
|
r.lline = pieces[3]
|
||||||
|
return "", "", 666
|
||||||
|
}
|
||||||
|
|
||||||
|
piece = pieces[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
values := bytes.Split(piece, []byte{'\t'})
|
||||||
|
if len(values) < 2 {
|
||||||
|
return "", "", 2
|
||||||
|
}
|
||||||
|
return string(values[0]), string(values[1]), 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *batteryReader) readBattery() (*Battery, bool, bool) {
|
||||||
|
b := &Battery{}
|
||||||
|
var exists, amps bool
|
||||||
|
|
||||||
|
for r.cmdout.Scan() {
|
||||||
|
exists = true
|
||||||
|
|
||||||
|
name, value, errno := r.readValue()
|
||||||
|
if errno == 666 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errno != 0 {
|
||||||
|
r.setErrParse(errno)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "bif_design_cap":
|
||||||
|
b.Design, r.e.Design = readFloat(value)
|
||||||
|
case "bif_last_cap":
|
||||||
|
b.Full, r.e.Full = readFloat(value)
|
||||||
|
case "bif_unit":
|
||||||
|
amps = value != "0"
|
||||||
|
case "bif_voltage":
|
||||||
|
b.DesignVoltage, r.e.DesignVoltage = readVoltage(value)
|
||||||
|
case "bst_voltage":
|
||||||
|
b.Voltage, r.e.Voltage = readVoltage(value)
|
||||||
|
case "bst_rem_cap":
|
||||||
|
b.Current, r.e.Current = readFloat(value)
|
||||||
|
case "bst_rate":
|
||||||
|
b.ChargeRate, r.e.ChargeRate = readFloat(value)
|
||||||
|
case "bst_state":
|
||||||
|
b.State.Raw, r.e.State = readState(value)
|
||||||
|
b.State.specific = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, amps, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *batteryReader) next() (*Battery, error) {
|
||||||
|
r.e = ErrPartial{
|
||||||
|
Design: errValueNotFound,
|
||||||
|
Full: errValueNotFound,
|
||||||
|
Current: errValueNotFound,
|
||||||
|
ChargeRate: errValueNotFound,
|
||||||
|
State: errValueNotFound,
|
||||||
|
Voltage: errValueNotFound,
|
||||||
|
DesignVoltage: errValueNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, amps, exists := r.readBattery()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.e.DesignVoltage != nil && r.e.Voltage == nil {
|
||||||
|
b.DesignVoltage, r.e.DesignVoltage = b.Voltage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if amps {
|
||||||
|
if r.e.DesignVoltage == nil {
|
||||||
|
b.Design *= b.DesignVoltage
|
||||||
|
} else {
|
||||||
|
r.e.Design = r.e.DesignVoltage
|
||||||
|
}
|
||||||
|
if r.e.Voltage == nil {
|
||||||
|
b.Full *= b.Voltage
|
||||||
|
b.Current *= b.Voltage
|
||||||
|
b.ChargeRate *= b.Voltage
|
||||||
|
} else {
|
||||||
|
r.e.Full = r.e.Voltage
|
||||||
|
r.e.Current = r.e.Voltage
|
||||||
|
r.e.ChargeRate = r.e.Voltage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.State.Raw == Unknown && r.e.Current == nil && r.e.Full == nil && b.Current >= b.Full {
|
||||||
|
b.State.Raw = Full
|
||||||
|
b.State.specific = fmt.Sprintf("current >= full")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, r.e
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBatteryReader() (*batteryReader, error) {
|
||||||
|
out, err := exec.Command("kstat", "-p", "-m", "acpi_drv", "-n", "battery B*").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &batteryReader{cmdout: bufio.NewScanner(bytes.NewReader(out))}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
br, err := newBatteryReader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return br.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
br, err := newBatteryReader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var batteries []*Battery
|
||||||
|
var errors Errors
|
||||||
|
for b, e := br.next(); e != io.EOF; b, e = br.next() {
|
||||||
|
batteries = append(batteries, b)
|
||||||
|
errors = append(errors, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
320
vendor/github.com/distatus/battery/battery_windows.go
generated
vendored
Normal file
320
vendor/github.com/distatus/battery/battery_windows.go
generated
vendored
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type batteryQueryInformation struct {
|
||||||
|
BatteryTag uint32
|
||||||
|
InformationLevel int32
|
||||||
|
AtRate int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryInformation struct {
|
||||||
|
Capabilities uint32
|
||||||
|
Technology uint8
|
||||||
|
Reserved [3]uint8
|
||||||
|
Chemistry [4]uint8
|
||||||
|
DesignedCapacity uint32
|
||||||
|
FullChargedCapacity uint32
|
||||||
|
DefaultAlert1 uint32
|
||||||
|
DefaultAlert2 uint32
|
||||||
|
CriticalBias uint32
|
||||||
|
CycleCount uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryWaitStatus struct {
|
||||||
|
BatteryTag uint32
|
||||||
|
Timeout uint32
|
||||||
|
PowerState uint32
|
||||||
|
LowCapacity uint32
|
||||||
|
HighCapacity uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryStatus struct {
|
||||||
|
PowerState uint32
|
||||||
|
Capacity uint32
|
||||||
|
Voltage uint32
|
||||||
|
Rate int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type guid struct {
|
||||||
|
Data1 uint32
|
||||||
|
Data2 uint16
|
||||||
|
Data3 uint16
|
||||||
|
Data4 [8]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type spDeviceInterfaceData struct {
|
||||||
|
cbSize uint32
|
||||||
|
InterfaceClassGuid guid
|
||||||
|
Flags uint32
|
||||||
|
Reserved uint
|
||||||
|
}
|
||||||
|
|
||||||
|
var guidDeviceBattery = guid{
|
||||||
|
0x72631e54,
|
||||||
|
0x78A4,
|
||||||
|
0x11d0,
|
||||||
|
[8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a},
|
||||||
|
}
|
||||||
|
|
||||||
|
func int32ToFloat64(num int32) (float64, error) {
|
||||||
|
// There is something wrong with this constant, but
|
||||||
|
// it appears to work so far...
|
||||||
|
if num == -0x80000000 { // BATTERY_UNKNOWN_RATE
|
||||||
|
return 0, errors.New("Unknown value received")
|
||||||
|
}
|
||||||
|
return math.Abs(float64(num)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint32ToFloat64(num uint32) (float64, error) {
|
||||||
|
if num == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY
|
||||||
|
return 0, errors.New("Unknown value received")
|
||||||
|
}
|
||||||
|
return float64(num), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
|
||||||
|
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
|
||||||
|
if windows.Handle(r1) == windows.InvalidHandle {
|
||||||
|
if errno != 0 {
|
||||||
|
return 0, error(errno)
|
||||||
|
}
|
||||||
|
return 0, syscall.EINVAL
|
||||||
|
}
|
||||||
|
return r1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
|
||||||
|
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6)
|
||||||
|
if r1 == 0 {
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
|
||||||
|
var setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
|
||||||
|
var setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
|
||||||
|
var setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
|
||||||
|
var setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
|
||||||
|
|
||||||
|
func readState(powerState uint32) AgnosticState {
|
||||||
|
switch {
|
||||||
|
case powerState&0x00000004 != 0:
|
||||||
|
return Charging
|
||||||
|
case powerState&0x00000008 != 0:
|
||||||
|
return Empty
|
||||||
|
case powerState&0x00000002 != 0:
|
||||||
|
return Discharging
|
||||||
|
case powerState&0x00000001 != 0:
|
||||||
|
return Full
|
||||||
|
default:
|
||||||
|
return Undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGet(idx int) (*Battery, error) {
|
||||||
|
hdev, err := setupDiSetup(
|
||||||
|
setupDiGetClassDevsW,
|
||||||
|
4,
|
||||||
|
uintptr(unsafe.Pointer(&guidDeviceBattery)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer syscall.Syscall(setupDiDestroyDeviceInfoList.Addr(), 1, hdev, 0, 0)
|
||||||
|
|
||||||
|
var did spDeviceInterfaceData
|
||||||
|
did.cbSize = uint32(unsafe.Sizeof(did))
|
||||||
|
errno := setupDiCall(
|
||||||
|
setupDiEnumDeviceInterfaces,
|
||||||
|
5,
|
||||||
|
hdev,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&guidDeviceBattery)),
|
||||||
|
uintptr(idx),
|
||||||
|
uintptr(unsafe.Pointer(&did)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno == 259 { // ERROR_NO_MORE_ITEMS
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if errno != 0 {
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
var cbRequired uint32
|
||||||
|
errno = setupDiCall(
|
||||||
|
setupDiGetDeviceInterfaceDetailW,
|
||||||
|
6,
|
||||||
|
hdev,
|
||||||
|
uintptr(unsafe.Pointer(&did)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&cbRequired)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
// The god damn struct with ANYSIZE_ARRAY of utf16 in it is crazy.
|
||||||
|
// So... let's emulate it with array of uint16 ;-D.
|
||||||
|
// Keep in mind that the first two elements are actually cbSize.
|
||||||
|
didd := make([]uint16, cbRequired/2)
|
||||||
|
cbSize := (*uint32)(unsafe.Pointer(&didd[0]))
|
||||||
|
if unsafe.Sizeof(uint(0)) == 8 {
|
||||||
|
*cbSize = 8
|
||||||
|
} else {
|
||||||
|
*cbSize = 6
|
||||||
|
}
|
||||||
|
errno = setupDiCall(
|
||||||
|
setupDiGetDeviceInterfaceDetailW,
|
||||||
|
6,
|
||||||
|
hdev,
|
||||||
|
uintptr(unsafe.Pointer(&did)),
|
||||||
|
uintptr(unsafe.Pointer(&didd[0])),
|
||||||
|
uintptr(cbRequired),
|
||||||
|
uintptr(unsafe.Pointer(&cbRequired)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
devicePath := &didd[2:][0]
|
||||||
|
|
||||||
|
handle, err := windows.CreateFile(
|
||||||
|
devicePath,
|
||||||
|
windows.GENERIC_READ|windows.GENERIC_WRITE,
|
||||||
|
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
|
||||||
|
nil,
|
||||||
|
windows.OPEN_EXISTING,
|
||||||
|
windows.FILE_ATTRIBUTE_NORMAL,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(handle)
|
||||||
|
|
||||||
|
var dwOut uint32
|
||||||
|
|
||||||
|
var dwWait uint32
|
||||||
|
var bqi batteryQueryInformation
|
||||||
|
err = windows.DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
2703424, // IOCTL_BATTERY_QUERY_TAG
|
||||||
|
(*byte)(unsafe.Pointer(&dwWait)),
|
||||||
|
uint32(unsafe.Sizeof(dwWait)),
|
||||||
|
(*byte)(unsafe.Pointer(&bqi.BatteryTag)),
|
||||||
|
uint32(unsafe.Sizeof(bqi.BatteryTag)),
|
||||||
|
&dwOut,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bqi.BatteryTag == 0 {
|
||||||
|
return nil, errors.New("BatteryTag not returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Battery{}
|
||||||
|
e := ErrPartial{}
|
||||||
|
|
||||||
|
var bi batteryInformation
|
||||||
|
err = windows.DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
2703428, // IOCTL_BATTERY_QUERY_INFORMATION
|
||||||
|
(*byte)(unsafe.Pointer(&bqi)),
|
||||||
|
uint32(unsafe.Sizeof(bqi)),
|
||||||
|
(*byte)(unsafe.Pointer(&bi)),
|
||||||
|
uint32(unsafe.Sizeof(bi)),
|
||||||
|
&dwOut,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
b.Full = float64(bi.FullChargedCapacity)
|
||||||
|
b.Design = float64(bi.DesignedCapacity)
|
||||||
|
} else {
|
||||||
|
e.Full = err
|
||||||
|
e.Design = err
|
||||||
|
}
|
||||||
|
|
||||||
|
bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag}
|
||||||
|
var bs batteryStatus
|
||||||
|
err = windows.DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
2703436, // IOCTL_BATTERY_QUERY_STATUS
|
||||||
|
(*byte)(unsafe.Pointer(&bws)),
|
||||||
|
uint32(unsafe.Sizeof(bws)),
|
||||||
|
(*byte)(unsafe.Pointer(&bs)),
|
||||||
|
uint32(unsafe.Sizeof(bs)),
|
||||||
|
&dwOut,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
b.Current, e.Current = uint32ToFloat64(bs.Capacity)
|
||||||
|
b.ChargeRate, e.ChargeRate = int32ToFloat64(bs.Rate)
|
||||||
|
b.Voltage, e.Voltage = uint32ToFloat64(bs.Voltage)
|
||||||
|
b.Voltage /= 1000
|
||||||
|
b.State.Raw = readState(bs.PowerState)
|
||||||
|
b.State.specific = fmt.Sprintf("%x", bs.PowerState)
|
||||||
|
} else {
|
||||||
|
e.Current = err
|
||||||
|
e.ChargeRate = err
|
||||||
|
e.Voltage = err
|
||||||
|
e.State = err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.DesignVoltage, e.DesignVoltage = b.Voltage, e.Voltage
|
||||||
|
|
||||||
|
return b, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemGetAll() ([]*Battery, error) {
|
||||||
|
var batteries []*Battery
|
||||||
|
var errors Errors
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
b, err := systemGet(i)
|
||||||
|
if err == ErrNotFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
batteries = append(batteries, b)
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
return batteries, errors
|
||||||
|
}
|
||||||
147
vendor/github.com/distatus/battery/errors.go
generated
vendored
Normal file
147
vendor/github.com/distatus/battery/errors.go
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ErrNotFound variable represents battery not found error.
|
||||||
|
//
|
||||||
|
// Only ever returned wrapped in ErrFatal.
|
||||||
|
var ErrNotFound = fmt.Errorf("Not found")
|
||||||
|
|
||||||
|
// ErrAllNotNil variable says that backend returned ErrPartial with
|
||||||
|
// all fields having not nil values, hence it was converted to ErrFatal.
|
||||||
|
//
|
||||||
|
// Only ever returned wrapped in ErrFatal.
|
||||||
|
var ErrAllNotNil = fmt.Errorf("All fields had not nil errors")
|
||||||
|
|
||||||
|
// ErrFatal type represents a fatal error.
|
||||||
|
//
|
||||||
|
// It indicates that either the library was not able to perform some kind
|
||||||
|
// of operation critical to retrieving any data, or all partials have failed at
|
||||||
|
// once (which would be equivalent to returning a ErrPartial with no nils).
|
||||||
|
//
|
||||||
|
// As such, the caller should assume that no meaningful data was
|
||||||
|
// returned alongside the error and act accordingly.
|
||||||
|
type ErrFatal struct {
|
||||||
|
Err error // The actual error that happened.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f ErrFatal) Error() string {
|
||||||
|
return fmt.Sprintf("Could not retrieve battery info: `%s`", f.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPartial type represents a partial error.
|
||||||
|
//
|
||||||
|
// It indicates that there were problems retrieving some of the data,
|
||||||
|
// but some was also retrieved successfully.
|
||||||
|
// If there would be all nils, nil is returned instead.
|
||||||
|
// If there would be all not nils, ErrFatal is returned instead.
|
||||||
|
//
|
||||||
|
// The fields represent fields in the Battery type.
|
||||||
|
type ErrPartial struct {
|
||||||
|
State error
|
||||||
|
Current error
|
||||||
|
Full error
|
||||||
|
Design error
|
||||||
|
ChargeRate error
|
||||||
|
Voltage error
|
||||||
|
DesignVoltage error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ErrPartial) Error() string {
|
||||||
|
if p.isNil() {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
errors := map[string]error{
|
||||||
|
"State": p.State,
|
||||||
|
"Current": p.Current,
|
||||||
|
"Full": p.Full,
|
||||||
|
"Design": p.Design,
|
||||||
|
"ChargeRate": p.ChargeRate,
|
||||||
|
"Voltage": p.Voltage,
|
||||||
|
"DesignVoltage": p.DesignVoltage,
|
||||||
|
}
|
||||||
|
keys := []string{"State", "Current", "Full", "Design", "ChargeRate", "Voltage", "DesignVoltage"}
|
||||||
|
s := "{"
|
||||||
|
for _, name := range keys {
|
||||||
|
err := errors[name]
|
||||||
|
if err != nil {
|
||||||
|
s += fmt.Sprintf("%s:%s ", name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s[:len(s)-1] + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ErrPartial) isNil() bool {
|
||||||
|
return p.State == nil &&
|
||||||
|
p.Current == nil &&
|
||||||
|
p.Full == nil &&
|
||||||
|
p.Design == nil &&
|
||||||
|
p.ChargeRate == nil &&
|
||||||
|
p.Voltage == nil &&
|
||||||
|
p.DesignVoltage == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ErrPartial) noNil() bool {
|
||||||
|
return p.State != nil &&
|
||||||
|
p.Current != nil &&
|
||||||
|
p.Full != nil &&
|
||||||
|
p.Design != nil &&
|
||||||
|
p.ChargeRate != nil &&
|
||||||
|
p.Voltage != nil &&
|
||||||
|
p.DesignVoltage != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors type represents an array of ErrFatal, ErrPartial or nil values.
|
||||||
|
//
|
||||||
|
// Can only possibly be returned by GetAll() call.
|
||||||
|
type Errors []error
|
||||||
|
|
||||||
|
func (e Errors) Error() string {
|
||||||
|
s := "["
|
||||||
|
for _, err := range e {
|
||||||
|
if err != nil {
|
||||||
|
s += err.Error() + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s) > 1 {
|
||||||
|
s = s[:len(s)-1]
|
||||||
|
}
|
||||||
|
return s + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapError(err error) error {
|
||||||
|
if perr, ok := err.(ErrPartial); ok {
|
||||||
|
if perr.isNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if perr.noNil() {
|
||||||
|
return ErrFatal{ErrAllNotNil}
|
||||||
|
}
|
||||||
|
return perr
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return ErrFatal{err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
vendor/github.com/distatus/battery/ioctl.go
generated
vendored
Normal file
48
vendor/github.com/distatus/battery/ioctl.go
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// battery
|
||||||
|
// Copyright (C) 2016,2023 Karol 'Kenji Takahashi' Woźniak
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
// a copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
//go:build freebsd || dragonfly || netbsd
|
||||||
|
|
||||||
|
package battery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ioctl(fd int, nr int64, typ byte, size uintptr, retptr unsafe.Pointer) error {
|
||||||
|
_, _, errno := unix.Syscall(
|
||||||
|
unix.SYS_IOCTL,
|
||||||
|
uintptr(fd),
|
||||||
|
// Some magicks derived from sys/ioccom.h.
|
||||||
|
uintptr((0x40000000|0x80000000)|
|
||||||
|
((int64(size)&(1<<13-1))<<16)|
|
||||||
|
(int64(typ)<<8)|
|
||||||
|
nr,
|
||||||
|
),
|
||||||
|
uintptr(retptr),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
vendor/github.com/go-co-op/gocron/v2/.gitignore
generated
vendored
Normal file
20
vendor/github.com/go-co-op/gocron/v2/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
local_testing
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE project files
|
||||||
|
.idea
|
||||||
49
vendor/github.com/go-co-op/gocron/v2/.golangci.yaml
generated
vendored
Normal file
49
vendor/github.com/go-co-op/gocron/v2/.golangci.yaml
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: true
|
||||||
|
skip-dirs:
|
||||||
|
- local
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-same-issues: 100
|
||||||
|
include:
|
||||||
|
- EXC0012
|
||||||
|
- EXC0014
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- exportloopref
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
output:
|
||||||
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
|
format: colored-line-number
|
||||||
|
# print lines of code with issue, default is true
|
||||||
|
print-issued-lines: true
|
||||||
|
# print linter name in the end of issue text, default is true
|
||||||
|
print-linter-name: true
|
||||||
|
# make issues output unique by line, default is true
|
||||||
|
uniq-by-line: true
|
||||||
|
# add a prefix to the output file references; default is no prefix
|
||||||
|
path-prefix: ""
|
||||||
|
# sorts results by: filepath, line and column
|
||||||
|
sort-results: true
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
golint:
|
||||||
|
min-confidence: 0.8
|
||||||
|
|
||||||
|
fix: true
|
||||||
24
vendor/github.com/go-co-op/gocron/v2/.pre-commit-config.yaml
generated
vendored
Normal file
24
vendor/github.com/go-co-op/gocron/v2/.pre-commit-config.yaml
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-yaml
|
||||||
|
- id: detect-private-key
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v1.55.2
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
|
- repo: https://github.com/TekWizely/pre-commit-golang
|
||||||
|
rev: v1.0.0-rc.1
|
||||||
|
hooks:
|
||||||
|
- id: go-fumpt
|
||||||
|
args:
|
||||||
|
- -w
|
||||||
|
- id: go-mod-tidy
|
||||||
73
vendor/github.com/go-co-op/gocron/v2/CODE_OF_CONDUCT.md
generated
vendored
Normal file
73
vendor/github.com/go-co-op/gocron/v2/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone. And we mean everyone!
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and kind language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team initially on Slack to coordinate private communication. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
38
vendor/github.com/go-co-op/gocron/v2/CONTRIBUTING.md
generated
vendored
Normal file
38
vendor/github.com/go-co-op/gocron/v2/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Contributing to gocron
|
||||||
|
|
||||||
|
Thank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback.
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
If you find a bug then please let the project know by opening an issue after doing the following:
|
||||||
|
|
||||||
|
- Do a quick search of the existing issues to make sure the bug isn't already reported
|
||||||
|
- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing
|
||||||
|
- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc)
|
||||||
|
|
||||||
|
## Suggesting Enhancements
|
||||||
|
|
||||||
|
If you have a use case that you don't see a way to support yet, we would welcome the feedback in an issue. Before opening the issue, please consider:
|
||||||
|
|
||||||
|
- Is this a common use case?
|
||||||
|
- Is it simple to understand?
|
||||||
|
|
||||||
|
You can help us out by doing the following before raising a new issue:
|
||||||
|
|
||||||
|
- Check that the feature hasn't been requested already by searching existing issues
|
||||||
|
- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea
|
||||||
|
- Explain your own use cases as the basis of the request
|
||||||
|
|
||||||
|
## Adding Features
|
||||||
|
|
||||||
|
Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue.
|
||||||
|
This allows us to discuss the changes and make sure they are a good fit for the project.
|
||||||
|
|
||||||
|
Please always make sure a pull request has been:
|
||||||
|
|
||||||
|
- Unit tested with `make test`
|
||||||
|
- Linted with `make lint`
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
Tests should follow the [table driven test pattern](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go). See other tests in the code base for additional examples.
|
||||||
21
vendor/github.com/go-co-op/gocron/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/go-co-op/gocron/v2/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014, 辣椒面
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
19
vendor/github.com/go-co-op/gocron/v2/Makefile
generated
vendored
Normal file
19
vendor/github.com/go-co-op/gocron/v2/Makefile
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.PHONY: fmt lint test mocks test_coverage
|
||||||
|
|
||||||
|
GO_PKGS := $(shell go list -f {{.Dir}} ./...)
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {}
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@grep "^func " example_test.go | sort -c
|
||||||
|
@golangci-lint run
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)
|
||||||
|
|
||||||
|
test_coverage:
|
||||||
|
@go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS)
|
||||||
|
|
||||||
|
mocks:
|
||||||
|
@go generate ./...
|
||||||
156
vendor/github.com/go-co-op/gocron/v2/README.md
generated
vendored
Normal file
156
vendor/github.com/go-co-op/gocron/v2/README.md
generated
vendored
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# gocron: A Golang Job Scheduling Package
|
||||||
|
|
||||||
|
[](https://github.com/go-co-op/gocron/actions)
|
||||||
|
 [](https://pkg.go.dev/github.com/go-co-op/gocron/v2)
|
||||||
|
|
||||||
|
gocron is a job scheduling package which lets you run Go functions at pre-determined intervals.
|
||||||
|
|
||||||
|
If you want to chat, you can find us on Slack at
|
||||||
|
[<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-co-op/gocron/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// create a scheduler
|
||||||
|
s, err := gocron.NewScheduler()
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a job to the scheduler
|
||||||
|
j, err := s.NewJob(
|
||||||
|
gocron.DurationJob(
|
||||||
|
10*time.Second,
|
||||||
|
),
|
||||||
|
gocron.NewTask(
|
||||||
|
func(a string, b int) {
|
||||||
|
// do things
|
||||||
|
},
|
||||||
|
"hello",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
// each job has a unique id
|
||||||
|
fmt.Println(j.ID())
|
||||||
|
|
||||||
|
// start the scheduler
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
// when you're done, shut it down
|
||||||
|
err = s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- **Job**: The job encapsulates a "task", which is made up of a go function and any function parameters. The Job then
|
||||||
|
provides the scheduler with the time the job should next be scheduled to run.
|
||||||
|
- **Scheduler**: The scheduler keeps track of all the jobs and sends each job to the executor when
|
||||||
|
it is ready to be run.
|
||||||
|
- **Executor**: The executor calls the job's task and manages the complexities of different job
|
||||||
|
execution timing requirements (e.g. singletons that shouldn't overrun each other, limiting the max number of jobs running)
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Job types
|
||||||
|
Jobs can be run at various intervals.
|
||||||
|
- [**Duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):
|
||||||
|
Jobs can be run at a fixed `time.Duration`.
|
||||||
|
- [**Random duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationRandomJob):
|
||||||
|
Jobs can be run at a random `time.Duration` between a min and max.
|
||||||
|
- [**Cron**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#CronJob):
|
||||||
|
Jobs can be run using a crontab.
|
||||||
|
- [**Daily**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DailyJob):
|
||||||
|
Jobs can be run every x days at specific times.
|
||||||
|
- [**Weekly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WeeklyJob):
|
||||||
|
Jobs can be run every x weeks on specific days of the week and at specific times.
|
||||||
|
- [**Monthly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonthlyJob):
|
||||||
|
Jobs can be run every x months on specific days of the month and at specific times.
|
||||||
|
- [**One time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#OneTimeJob):
|
||||||
|
Jobs can be run once at a specific time. These are non-recurring jobs.
|
||||||
|
|
||||||
|
### Concurrency Limits
|
||||||
|
Jobs can be limited individually or across the entire scheduler.
|
||||||
|
- [**Per job limiting with singleton mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithSingletonMode):
|
||||||
|
Jobs can be limited to a single concurrent execution that either reschedules (skips overlapping executions)
|
||||||
|
or queues (waits for the previous execution to finish).
|
||||||
|
- [**Per scheduler limiting with limit mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithLimitConcurrentJobs):
|
||||||
|
Jobs can be limited to a certain number of concurrent executions across the entire scheduler
|
||||||
|
using either reschedule (skip when the limit is met) or queue (jobs are added to a queue to
|
||||||
|
wait for the limit to be available).
|
||||||
|
- **Note:** A scheduler limit and a job limit can both be enabled.
|
||||||
|
|
||||||
|
### Distributed instances of gocron
|
||||||
|
Multiple instances of gocron can be run.
|
||||||
|
- [**Elector**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedElector):
|
||||||
|
An elector can be used to elect a single instance of gocron to run as the primary with the
|
||||||
|
other instances checking to see if a new leader needs to be elected.
|
||||||
|
- Implementations: [go-co-op electors](https://github.com/go-co-op?q=-elector&type=all&language=&sort=)
|
||||||
|
- [**Locker**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedLocker):
|
||||||
|
A locker can be used to lock each run of a job to a single instance of gocron.
|
||||||
|
- Implementations: [go-co-op lockers](https://github.com/go-co-op?q=-lock&type=all&language=&sort=)
|
||||||
|
|
||||||
|
### Events
|
||||||
|
Job events can trigger actions.
|
||||||
|
- [**Listeners**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithEventListeners):
|
||||||
|
Can be added to a job, with [event listeners](https://pkg.go.dev/github.com/go-co-op/gocron/v2#EventListener),
|
||||||
|
or all jobs across the
|
||||||
|
[scheduler](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions)
|
||||||
|
to listen for job events and trigger actions.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
Many job and scheduler options are available.
|
||||||
|
- [**Job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#JobOption):
|
||||||
|
Job options can be set when creating a job using `NewJob`.
|
||||||
|
- [**Global job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions):
|
||||||
|
Global job options can be set when creating a scheduler using `NewScheduler`
|
||||||
|
and the `WithGlobalJobOptions` option.
|
||||||
|
- [**Scheduler options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerOption):
|
||||||
|
Scheduler options can be set when creating a scheduler using `NewScheduler`.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
Logs can be enabled.
|
||||||
|
- [Logger](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Logger):
|
||||||
|
The Logger interface can be implemented with your desired logging library.
|
||||||
|
The provided NewLogger uses the standard library's log package.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
The gocron library is set up to enable testing.
|
||||||
|
- Mocks are provided in [the mock package](mocks) using [gomock](https://github.com/uber-go/mock).
|
||||||
|
- Time can be mocked by passing in a [FakeClock](https://pkg.go.dev/github.com/jonboulle/clockwork#FakeClock)
|
||||||
|
to [WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithClock) -
|
||||||
|
see the [example on WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#example-WithClock).
|
||||||
|
|
||||||
|
## Supporters
|
||||||
|
|
||||||
|
We appreciate the support for free and open source software!
|
||||||
|
|
||||||
|
This project is supported by:
|
||||||
|
|
||||||
|
- [Jetbrains](https://www.jetbrains.com/?from=gocron)
|
||||||
|
- [Sentry](https://sentry.io/welcome/)
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#go-co-op/gocron&Date)
|
||||||
15
vendor/github.com/go-co-op/gocron/v2/SECURITY.md
generated
vendored
Normal file
15
vendor/github.com/go-co-op/gocron/v2/SECURITY.md
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
The current plan is to maintain version 1 as long as possible incorporating any necessary security patches.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 1.x.x | :white_check_mark: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Vulnerabilities can be reported by [opening an issue](https://github.com/go-co-op/gocron/issues/new/choose) or reaching out on Slack: [<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)
|
||||||
|
|
||||||
|
We will do our best to addrerss any vulnerabilities in an expeditious manner.
|
||||||
5
vendor/github.com/go-co-op/gocron/v2/codecov.yml
generated
vendored
Normal file
5
vendor/github.com/go-co-op/gocron/v2/codecov.yml
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
if_ci_failed: success
|
||||||
30
vendor/github.com/go-co-op/gocron/v2/distributed.go
generated
vendored
Normal file
30
vendor/github.com/go-co-op/gocron/v2/distributed.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//go:generate mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Elector determines the leader from instances asking to be the leader. Only
|
||||||
|
// the leader runs jobs. If the leader goes down, a new leader will be elected.
|
||||||
|
type Elector interface {
|
||||||
|
// IsLeader should return nil if the job should be scheduled by the instance
|
||||||
|
// making the request and an error if the job should not be scheduled.
|
||||||
|
IsLeader(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locker represents the required interface to lock jobs when running multiple schedulers.
|
||||||
|
// The lock is held for the duration of the job's run, and it is expected that the
|
||||||
|
// locker implementation handles time splay between schedulers.
|
||||||
|
// The lock key passed is the job's name - which, if not set, defaults to the
|
||||||
|
// go function's name, e.g. "pkg.myJob" for func myJob() {} in pkg
|
||||||
|
type Locker interface {
|
||||||
|
// Lock if an error is returned by lock, the job will not be scheduled.
|
||||||
|
Lock(ctx context.Context, key string) (Lock, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock represents an obtained lock. The lock is released after the execution of the job
|
||||||
|
// by the scheduler.
|
||||||
|
type Lock interface {
|
||||||
|
Unlock(ctx context.Context) error
|
||||||
|
}
|
||||||
52
vendor/github.com/go-co-op/gocron/v2/errors.go
generated
vendored
Normal file
52
vendor/github.com/go-co-op/gocron/v2/errors.go
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package gocron
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Public error definitions
|
||||||
|
var (
|
||||||
|
ErrCronJobParse = fmt.Errorf("gocron: CronJob: crontab parse failure")
|
||||||
|
ErrDailyJobAtTimeNil = fmt.Errorf("gocron: DailyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrDailyJobAtTimesNil = fmt.Errorf("gocron: DailyJob: atTimes must not be nil")
|
||||||
|
ErrDailyJobHours = fmt.Errorf("gocron: DailyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrDailyJobMinutesSeconds = fmt.Errorf("gocron: DailyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrDurationRandomJobMinMax = fmt.Errorf("gocron: DurationRandomJob: minimum duration must be less than maximum duration")
|
||||||
|
ErrEventListenerFuncNil = fmt.Errorf("gocron: eventListenerFunc must not be nil")
|
||||||
|
ErrJobNotFound = fmt.Errorf("gocron: job not found")
|
||||||
|
ErrJobRunNowFailed = fmt.Errorf("gocron: Job: RunNow: scheduler unreachable")
|
||||||
|
ErrMonthlyJobDays = fmt.Errorf("gocron: MonthlyJob: daysOfTheMonth must be between 31 and -31 inclusive, and not 0")
|
||||||
|
ErrMonthlyJobAtTimeNil = fmt.Errorf("gocron: MonthlyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrMonthlyJobAtTimesNil = fmt.Errorf("gocron: MonthlyJob: atTimes must not be nil")
|
||||||
|
ErrMonthlyJobDaysNil = fmt.Errorf("gocron: MonthlyJob: daysOfTheMonth must not be nil")
|
||||||
|
ErrMonthlyJobHours = fmt.Errorf("gocron: MonthlyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrMonthlyJobMinutesSeconds = fmt.Errorf("gocron: MonthlyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrNewJobTaskNil = fmt.Errorf("gocron: NewJob: Task must not be nil")
|
||||||
|
ErrNewJobTaskNotFunc = fmt.Errorf("gocron: NewJob: Task.Function must be of kind reflect.Func")
|
||||||
|
ErrNewJobWrongNumberOfParameters = fmt.Errorf("gocron: NewJob: Number of provided parameters does not match expected")
|
||||||
|
ErrNewJobWrongTypeOfParameters = fmt.Errorf("gocron: NewJob: Type of provided parameters does not match expected")
|
||||||
|
ErrOneTimeJobStartDateTimePast = fmt.Errorf("gocron: OneTimeJob: start must not be in the past")
|
||||||
|
ErrStopExecutorTimedOut = fmt.Errorf("gocron: timed out waiting for executor to stop")
|
||||||
|
ErrStopJobsTimedOut = fmt.Errorf("gocron: timed out waiting for jobs to finish")
|
||||||
|
ErrStopSchedulerTimedOut = fmt.Errorf("gocron: timed out waiting for scheduler to stop")
|
||||||
|
ErrWeeklyJobAtTimeNil = fmt.Errorf("gocron: WeeklyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrWeeklyJobAtTimesNil = fmt.Errorf("gocron: WeeklyJob: atTimes must not be nil")
|
||||||
|
ErrWeeklyJobDaysOfTheWeekNil = fmt.Errorf("gocron: WeeklyJob: daysOfTheWeek must not be nil")
|
||||||
|
ErrWeeklyJobHours = fmt.Errorf("gocron: WeeklyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrWeeklyJobMinutesSeconds = fmt.Errorf("gocron: WeeklyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrWithClockNil = fmt.Errorf("gocron: WithClock: clock must not be nil")
|
||||||
|
ErrWithDistributedElectorNil = fmt.Errorf("gocron: WithDistributedElector: elector must not be nil")
|
||||||
|
ErrWithDistributedLockerNil = fmt.Errorf("gocron: WithDistributedLocker: locker must not be nil")
|
||||||
|
ErrWithLimitConcurrentJobsZero = fmt.Errorf("gocron: WithLimitConcurrentJobs: limit must be greater than 0")
|
||||||
|
ErrWithLocationNil = fmt.Errorf("gocron: WithLocation: location must not be nil")
|
||||||
|
ErrWithLoggerNil = fmt.Errorf("gocron: WithLogger: logger must not be nil")
|
||||||
|
ErrWithNameEmpty = fmt.Errorf("gocron: WithName: name must not be empty")
|
||||||
|
ErrWithStartDateTimePast = fmt.Errorf("gocron: WithStartDateTime: start must not be in the past")
|
||||||
|
ErrWithStopTimeoutZeroOrNegative = fmt.Errorf("gocron: WithStopTimeout: timeout must be greater than 0")
|
||||||
|
)
|
||||||
|
|
||||||
|
// internal errors
|
||||||
|
var (
|
||||||
|
errAtTimeNil = fmt.Errorf("errAtTimeNil")
|
||||||
|
errAtTimesNil = fmt.Errorf("errAtTimesNil")
|
||||||
|
errAtTimeHours = fmt.Errorf("errAtTimeHours")
|
||||||
|
errAtTimeMinSec = fmt.Errorf("errAtTimeMinSec")
|
||||||
|
)
|
||||||
440
vendor/github.com/go-co-op/gocron/v2/executor.go
generated
vendored
Normal file
440
vendor/github.com/go-co-op/gocron/v2/executor.go
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type executor struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
logger Logger
|
||||||
|
stopCh chan struct{}
|
||||||
|
jobsIn chan jobIn
|
||||||
|
jobIDsOut chan uuid.UUID
|
||||||
|
jobOutRequest chan jobOutRequest
|
||||||
|
stopTimeout time.Duration
|
||||||
|
done chan error
|
||||||
|
singletonRunners map[uuid.UUID]singletonRunner
|
||||||
|
limitMode *limitModeConfig
|
||||||
|
elector Elector
|
||||||
|
locker Locker
|
||||||
|
}
|
||||||
|
|
||||||
|
type jobIn struct {
|
||||||
|
id uuid.UUID
|
||||||
|
shouldSendOut bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type singletonRunner struct {
|
||||||
|
in chan jobIn
|
||||||
|
rescheduleLimiter chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type limitModeConfig struct {
|
||||||
|
started bool
|
||||||
|
mode LimitMode
|
||||||
|
limit uint
|
||||||
|
rescheduleLimiter chan struct{}
|
||||||
|
in chan jobIn
|
||||||
|
// singletonJobs is used to track singleton jobs that are running
|
||||||
|
// in the limit mode runner. This is used to prevent the same job
|
||||||
|
// from running multiple times across limit mode runners when both
|
||||||
|
// a limit mode and singleton mode are enabled.
|
||||||
|
singletonJobs map[uuid.UUID]struct{}
|
||||||
|
singletonJobsMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) start() {
|
||||||
|
e.logger.Debug("gocron: executor started")
|
||||||
|
|
||||||
|
// creating the executor's context here as the executor
|
||||||
|
// is the only goroutine that should access this context
|
||||||
|
// any other uses within the executor should create a context
|
||||||
|
// using the executor context as parent.
|
||||||
|
e.ctx, e.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// the standardJobsWg tracks
|
||||||
|
standardJobsWg := &waitGroupWithMutex{}
|
||||||
|
|
||||||
|
singletonJobsWg := &waitGroupWithMutex{}
|
||||||
|
|
||||||
|
limitModeJobsWg := &waitGroupWithMutex{}
|
||||||
|
|
||||||
|
// create a fresh map for tracking singleton runners
|
||||||
|
e.singletonRunners = make(map[uuid.UUID]singletonRunner)
|
||||||
|
|
||||||
|
// start the for leap that is the executor
|
||||||
|
// selecting on channels for work to do
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// job ids in are sent from 1 of 2 places:
|
||||||
|
// 1. the scheduler sends directly when jobs
|
||||||
|
// are run immediately.
|
||||||
|
// 2. sent from time.AfterFuncs in which job schedules
|
||||||
|
// are spun up by the scheduler
|
||||||
|
case jIn := <-e.jobsIn:
|
||||||
|
select {
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// this context is used to handle cancellation of the executor
|
||||||
|
// on requests for a job to the scheduler via requestJobCtx
|
||||||
|
ctx, cancel := context.WithCancel(e.ctx)
|
||||||
|
|
||||||
|
if e.limitMode != nil && !e.limitMode.started {
|
||||||
|
// check if we are already running the limit mode runners
|
||||||
|
// if not, spin up the required number i.e. limit!
|
||||||
|
e.limitMode.started = true
|
||||||
|
for i := e.limitMode.limit; i > 0; i-- {
|
||||||
|
limitModeJobsWg.Add(1)
|
||||||
|
go e.limitModeRunner("limitMode-"+strconv.Itoa(int(i)), e.limitMode.in, limitModeJobsWg, e.limitMode.mode, e.limitMode.rescheduleLimiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spin off into a goroutine to unblock the executor and
|
||||||
|
// allow for processing for more work
|
||||||
|
go func() {
|
||||||
|
// make sure to cancel the above context per the docs
|
||||||
|
// // Canceling this context releases resources associated with it, so code should
|
||||||
|
// // call cancel as soon as the operations running in this Context complete.
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// check for limit mode - this spins up a separate runner which handles
|
||||||
|
// limiting the total number of concurrently running jobs
|
||||||
|
if e.limitMode != nil {
|
||||||
|
if e.limitMode.mode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
// rescheduleLimiter is a channel the size of the limit
|
||||||
|
// this blocks publishing to the channel and keeps
|
||||||
|
// the executor from building up a waiting queue
|
||||||
|
// and forces rescheduling
|
||||||
|
case e.limitMode.rescheduleLimiter <- struct{}{}:
|
||||||
|
e.limitMode.in <- jIn
|
||||||
|
default:
|
||||||
|
// all runners are busy, reschedule the work for later
|
||||||
|
// which means we just skip it here and do nothing
|
||||||
|
// TODO when metrics are added, this should increment a rescheduled metric
|
||||||
|
if jIn.shouldSendOut {
|
||||||
|
select {
|
||||||
|
case e.jobIDsOut <- jIn.id:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// since we're not using LimitModeReschedule, but instead using LimitModeWait
|
||||||
|
// we do want to queue up the work to the limit mode runners and allow them
|
||||||
|
// to work through the channel backlog. A hard limit of 1000 is in place
|
||||||
|
// at which point this call would block.
|
||||||
|
// TODO when metrics are added, this should increment a wait metric
|
||||||
|
e.limitMode.in <- jIn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no limit mode, so we're either running a regular job or
|
||||||
|
// a job with a singleton mode
|
||||||
|
//
|
||||||
|
// get the job, so we can figure out what kind it is and how
|
||||||
|
// to execute it
|
||||||
|
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
|
||||||
|
if j == nil {
|
||||||
|
// safety check as it'd be strange bug if this occurred
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if j.singletonMode {
|
||||||
|
// for singleton mode, get the existing runner for the job
|
||||||
|
// or spin up a new one
|
||||||
|
runner, ok := e.singletonRunners[jIn.id]
|
||||||
|
if !ok {
|
||||||
|
runner.in = make(chan jobIn, 1000)
|
||||||
|
if j.singletonLimitMode == LimitModeReschedule {
|
||||||
|
runner.rescheduleLimiter = make(chan struct{}, 1)
|
||||||
|
}
|
||||||
|
e.singletonRunners[jIn.id] = runner
|
||||||
|
singletonJobsWg.Add(1)
|
||||||
|
go e.singletonModeRunner("singleton-"+jIn.id.String(), runner.in, singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.singletonLimitMode == LimitModeReschedule {
|
||||||
|
// reschedule mode uses the limiter channel to check
|
||||||
|
// for a running job and reschedules if the channel is full.
|
||||||
|
select {
|
||||||
|
case runner.rescheduleLimiter <- struct{}{}:
|
||||||
|
runner.in <- jIn
|
||||||
|
default:
|
||||||
|
// runner is busy, reschedule the work for later
|
||||||
|
// which means we just skip it here and do nothing
|
||||||
|
// TODO when metrics are added, this should increment a rescheduled metric
|
||||||
|
if jIn.shouldSendOut {
|
||||||
|
select {
|
||||||
|
case e.jobIDsOut <- jIn.id:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// wait mode, fill up that queue (buffered channel, so it's ok)
|
||||||
|
runner.in <- jIn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// we've gotten to the basic / standard jobs --
|
||||||
|
// the ones without anything special that just want
|
||||||
|
// to be run. Add to the WaitGroup so that
|
||||||
|
// stopping or shutting down can wait for the jobs to
|
||||||
|
// complete.
|
||||||
|
standardJobsWg.Add(1)
|
||||||
|
go func(j internalJob) {
|
||||||
|
e.runJob(j, jIn.shouldSendOut)
|
||||||
|
standardJobsWg.Done()
|
||||||
|
}(*j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
|
||||||
|
e.logger.Debug("gocron: limitModeRunner starting", "name", name)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case jIn := <-in:
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("gocron: limitModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(e.ctx)
|
||||||
|
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
|
||||||
|
cancel()
|
||||||
|
if j != nil {
|
||||||
|
if j.singletonMode {
|
||||||
|
e.limitMode.singletonJobsMu.Lock()
|
||||||
|
_, ok := e.limitMode.singletonJobs[jIn.id]
|
||||||
|
if ok {
|
||||||
|
// this job is already running, so don't run it
|
||||||
|
// but instead reschedule it
|
||||||
|
e.limitMode.singletonJobsMu.Unlock()
|
||||||
|
if jIn.shouldSendOut {
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-j.ctx.Done():
|
||||||
|
return
|
||||||
|
case e.jobIDsOut <- j.id:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove the limiter block, as this particular job
|
||||||
|
// was a singleton already running, and we want to
|
||||||
|
// allow another job to be scheduled
|
||||||
|
if limitMode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
case <-rescheduleLimiter:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.limitMode.singletonJobs[jIn.id] = struct{}{}
|
||||||
|
e.limitMode.singletonJobsMu.Unlock()
|
||||||
|
}
|
||||||
|
e.runJob(*j, jIn.shouldSendOut)
|
||||||
|
|
||||||
|
if j.singletonMode {
|
||||||
|
e.limitMode.singletonJobsMu.Lock()
|
||||||
|
delete(e.limitMode.singletonJobs, jIn.id)
|
||||||
|
e.limitMode.singletonJobsMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the limiter block to allow another job to be scheduled
|
||||||
|
if limitMode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
case <-rescheduleLimiter:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("limitModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
|
||||||
|
e.logger.Debug("gocron: singletonModeRunner starting", "name", name)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case jIn := <-in:
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("gocron: singletonModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(e.ctx)
|
||||||
|
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
|
||||||
|
cancel()
|
||||||
|
if j != nil {
|
||||||
|
e.runJob(*j, jIn.shouldSendOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the limiter block to allow another job to be scheduled
|
||||||
|
if limitMode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
case <-rescheduleLimiter:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("singletonModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) runJob(j internalJob, shouldSendOut bool) {
|
||||||
|
if j.ctx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-j.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.elector != nil {
|
||||||
|
if err := e.elector.IsLeader(j.ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if e.locker != nil {
|
||||||
|
lock, err := e.locker.Lock(j.ctx, j.name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = lock.Unlock(j.ctx) }()
|
||||||
|
}
|
||||||
|
_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
|
||||||
|
|
||||||
|
if shouldSendOut {
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-j.ctx.Done():
|
||||||
|
return
|
||||||
|
case e.jobIDsOut <- j.id:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := callJobFuncWithParams(j.function, j.parameters...)
|
||||||
|
if err != nil {
|
||||||
|
_ = callJobFuncWithParams(j.afterJobRunsWithError, j.id, j.name, err)
|
||||||
|
} else {
|
||||||
|
_ = callJobFuncWithParams(j.afterJobRuns, j.id, j.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobsWg *waitGroupWithMutex) {
|
||||||
|
e.logger.Debug("gocron: stopping executor")
|
||||||
|
// we've been asked to stop. This is either because the scheduler has been told
|
||||||
|
// to stop all jobs or the scheduler has been asked to completely shutdown.
|
||||||
|
//
|
||||||
|
// cancel tells all the functions to stop their work and send in a done response
|
||||||
|
e.cancel()
|
||||||
|
|
||||||
|
// the wait for job channels are used to report back whether we successfully waited
|
||||||
|
// for all jobs to complete or if we hit the configured timeout.
|
||||||
|
waitForJobs := make(chan struct{}, 1)
|
||||||
|
waitForSingletons := make(chan struct{}, 1)
|
||||||
|
waitForLimitMode := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
// the waiter context is used to cancel the functions waiting on jobs.
|
||||||
|
// this is done to avoid goroutine leaks.
|
||||||
|
waiterCtx, waiterCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// wait for standard jobs to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("gocron: waiting for standard jobs to complete")
|
||||||
|
go func() {
|
||||||
|
// this is done in a separate goroutine, so we aren't
|
||||||
|
// blocked by the WaitGroup's Wait call in the event
|
||||||
|
// that the waiter context is cancelled.
|
||||||
|
// This particular goroutine could leak in the event that
|
||||||
|
// some long-running standard job doesn't complete.
|
||||||
|
standardJobsWg.Wait()
|
||||||
|
e.logger.Debug("gocron: standard jobs completed")
|
||||||
|
waitForJobs <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for per job singleton limit mode runner jobs to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("gocron: waiting for singleton jobs to complete")
|
||||||
|
go func() {
|
||||||
|
singletonJobsWg.Wait()
|
||||||
|
e.logger.Debug("gocron: singleton jobs completed")
|
||||||
|
waitForSingletons <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for limit mode runners to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("gocron: waiting for limit mode jobs to complete")
|
||||||
|
go func() {
|
||||||
|
limitModeJobsWg.Wait()
|
||||||
|
e.logger.Debug("gocron: limitMode jobs completed")
|
||||||
|
waitForLimitMode <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// now either wait for all the jobs to complete,
|
||||||
|
// or hit the timeout.
|
||||||
|
var count int
|
||||||
|
timeout := time.Now().Add(e.stopTimeout)
|
||||||
|
for time.Now().Before(timeout) && count < 3 {
|
||||||
|
select {
|
||||||
|
case <-waitForJobs:
|
||||||
|
count++
|
||||||
|
case <-waitForSingletons:
|
||||||
|
count++
|
||||||
|
case <-waitForLimitMode:
|
||||||
|
count++
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count < 3 {
|
||||||
|
e.done <- ErrStopJobsTimedOut
|
||||||
|
e.logger.Debug("gocron: executor stopped - timed out")
|
||||||
|
} else {
|
||||||
|
e.done <- nil
|
||||||
|
e.logger.Debug("gocron: executor stopped")
|
||||||
|
}
|
||||||
|
waiterCancel()
|
||||||
|
}
|
||||||
913
vendor/github.com/go-co-op/gocron/v2/job.go
generated
vendored
Normal file
913
vendor/github.com/go-co-op/gocron/v2/job.go
generated
vendored
Normal file
@@ -0,0 +1,913 @@
|
|||||||
|
//go:generate mockgen -destination=mocks/job.go -package=gocronmocks . Job
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// internalJob stores the information needed by the scheduler
|
||||||
|
// to manage scheduling, starting and stopping the job
|
||||||
|
type internalJob struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
tags []string
|
||||||
|
jobSchedule
|
||||||
|
lastRun, nextRun time.Time
|
||||||
|
function any
|
||||||
|
parameters []any
|
||||||
|
timer clockwork.Timer
|
||||||
|
singletonMode bool
|
||||||
|
singletonLimitMode LimitMode
|
||||||
|
limitRunsTo *limitRunsTo
|
||||||
|
startTime time.Time
|
||||||
|
startImmediately bool
|
||||||
|
// event listeners
|
||||||
|
afterJobRuns func(jobID uuid.UUID, jobName string)
|
||||||
|
beforeJobRuns func(jobID uuid.UUID, jobName string)
|
||||||
|
afterJobRunsWithError func(jobID uuid.UUID, jobName string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop is used to stop the job's timer and cancel the context
|
||||||
|
// stopping the timer is critical for cleaning up jobs that are
|
||||||
|
// sleeping in a time.AfterFunc timer when the job is being stopped.
|
||||||
|
// cancelling the context keeps the executor from continuing to try
|
||||||
|
// and run the job.
|
||||||
|
func (j *internalJob) stop() {
|
||||||
|
if j.timer != nil {
|
||||||
|
j.timer.Stop()
|
||||||
|
}
|
||||||
|
j.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// task stores the function and parameters
|
||||||
|
// that are actually run when the job is executed.
|
||||||
|
type task struct {
|
||||||
|
function any
|
||||||
|
parameters []any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task defines a function that returns the task
|
||||||
|
// function and parameters.
|
||||||
|
type Task func() task
|
||||||
|
|
||||||
|
// NewTask provides the job's task function and parameters.
|
||||||
|
func NewTask(function any, parameters ...any) Task {
|
||||||
|
return func() task {
|
||||||
|
return task{
|
||||||
|
function: function,
|
||||||
|
parameters: parameters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitRunsTo is used for managing the number of runs
|
||||||
|
// when the user only wants the job to run a certain
|
||||||
|
// number of times and then be removed from the scheduler.
|
||||||
|
type limitRunsTo struct {
|
||||||
|
limit uint
|
||||||
|
runCount uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// --------------- Job Variants ------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// JobDefinition defines the interface that must be
|
||||||
|
// implemented to create a job from the definition.
|
||||||
|
type JobDefinition interface {
|
||||||
|
setup(*internalJob, *time.Location) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*cronJobDefinition)(nil)
|
||||||
|
|
||||||
|
type cronJobDefinition struct {
|
||||||
|
crontab string
|
||||||
|
withSeconds bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cronJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var withLocation string
|
||||||
|
if strings.HasPrefix(c.crontab, "TZ=") || strings.HasPrefix(c.crontab, "CRON_TZ=") {
|
||||||
|
withLocation = c.crontab
|
||||||
|
} else {
|
||||||
|
// since the user didn't provide a timezone default to the location
|
||||||
|
// passed in by the scheduler. Default: time.Local
|
||||||
|
withLocation = fmt.Sprintf("CRON_TZ=%s %s", location.String(), c.crontab)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cronSchedule cron.Schedule
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.withSeconds {
|
||||||
|
p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
cronSchedule, err = p.Parse(withLocation)
|
||||||
|
} else {
|
||||||
|
cronSchedule, err = cron.ParseStandard(withLocation)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(ErrCronJobParse, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobSchedule = &cronJob{cronSchedule: cronSchedule}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CronJob defines a new job using the crontab syntax: `* * * * *`.
|
||||||
|
// An optional 6th field can be used at the beginning if withSeconds
|
||||||
|
// is set to true: `* * * * * *`.
|
||||||
|
// The timezone can be set on the Scheduler using WithLocation, or in the
|
||||||
|
// crontab in the form `TZ=America/Chicago * * * * *` or
|
||||||
|
// `CRON_TZ=America/Chicago * * * * *`
|
||||||
|
func CronJob(crontab string, withSeconds bool) JobDefinition {
|
||||||
|
return cronJobDefinition{
|
||||||
|
crontab: crontab,
|
||||||
|
withSeconds: withSeconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*durationJobDefinition)(nil)
|
||||||
|
|
||||||
|
type durationJobDefinition struct {
|
||||||
|
duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d durationJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
||||||
|
j.jobSchedule = &durationJob{duration: d.duration}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationJob defines a new job using time.Duration
|
||||||
|
// for the interval.
|
||||||
|
func DurationJob(duration time.Duration) JobDefinition {
|
||||||
|
return durationJobDefinition{
|
||||||
|
duration: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*durationRandomJobDefinition)(nil)
|
||||||
|
|
||||||
|
type durationRandomJobDefinition struct {
|
||||||
|
min, max time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
||||||
|
if d.min >= d.max {
|
||||||
|
return ErrDurationRandomJobMinMax
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobSchedule = &durationRandomJob{
|
||||||
|
min: d.min,
|
||||||
|
max: d.max,
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationRandomJob defines a new job that runs on a random interval
|
||||||
|
// between the min and max duration values provided.
|
||||||
|
//
|
||||||
|
// To achieve a similar behavior as tools that use a splay/jitter technique
|
||||||
|
// consider the median value as the baseline and the difference between the
|
||||||
|
// max-median or median-min as the splay/jitter.
|
||||||
|
//
|
||||||
|
// For example, if you want a job to run every 5 minutes, but want to add
|
||||||
|
// up to 1 min of jitter to the interval, you could use
|
||||||
|
// DurationRandomJob(4*time.Minute, 6*time.Minute)
|
||||||
|
func DurationRandomJob(minDuration, maxDuration time.Duration) JobDefinition {
|
||||||
|
return durationRandomJobDefinition{
|
||||||
|
min: minDuration,
|
||||||
|
max: maxDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DailyJob runs the job on the interval of days, and at the set times.
|
||||||
|
// By default, the job will start the next available day, considering the last run to be now,
|
||||||
|
// and the time and day based on the interval and times you input. This means, if you
|
||||||
|
// select an interval greater than 1, your job by default will run X (interval) days from now
|
||||||
|
// if there are no atTimes left in the current day. You can use WithStartAt to tell the
|
||||||
|
// scheduler to start the job sooner.
|
||||||
|
func DailyJob(interval uint, atTimes AtTimes) JobDefinition {
|
||||||
|
return dailyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*dailyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type dailyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(d.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrDailyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrDailyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrDailyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrDailyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
ds := dailyJob{
|
||||||
|
interval: d.interval,
|
||||||
|
atTimes: atTimesDate,
|
||||||
|
}
|
||||||
|
j.jobSchedule = ds
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*weeklyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type weeklyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
daysOfTheWeek Weekdays
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var ws weeklyJob
|
||||||
|
ws.interval = w.interval
|
||||||
|
|
||||||
|
if w.daysOfTheWeek == nil {
|
||||||
|
return ErrWeeklyJobDaysOfTheWeekNil
|
||||||
|
}
|
||||||
|
|
||||||
|
daysOfTheWeek := w.daysOfTheWeek()
|
||||||
|
|
||||||
|
slices.Sort(daysOfTheWeek)
|
||||||
|
ws.daysOfWeek = daysOfTheWeek
|
||||||
|
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(w.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrWeeklyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrWeeklyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrWeeklyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrWeeklyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
ws.atTimes = atTimesDate
|
||||||
|
|
||||||
|
j.jobSchedule = ws
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weekdays defines a function that returns a list of week days.
|
||||||
|
type Weekdays func() []time.Weekday
|
||||||
|
|
||||||
|
// NewWeekdays provide the days of the week the job should run.
|
||||||
|
func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
|
||||||
|
return func() []time.Weekday {
|
||||||
|
return append(weekdays, weekday)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeeklyJob runs the job on the interval of weeks, on the specific days of the week
|
||||||
|
// specified, and at the set times.
|
||||||
|
//
|
||||||
|
// By default, the job will start the next available day, considering the last run to be now,
|
||||||
|
// and the time and day based on the interval, days and times you input. This means, if you
|
||||||
|
// select an interval greater than 1, your job by default will run X (interval) weeks from now
|
||||||
|
// if there are no daysOfTheWeek left in the current week. You can use WithStartAt to tell the
|
||||||
|
// scheduler to start the job sooner.
|
||||||
|
func WeeklyJob(interval uint, daysOfTheWeek Weekdays, atTimes AtTimes) JobDefinition {
|
||||||
|
return weeklyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
daysOfTheWeek: daysOfTheWeek,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*monthlyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type monthlyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
daysOfTheMonth DaysOfTheMonth
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var ms monthlyJob
|
||||||
|
ms.interval = m.interval
|
||||||
|
|
||||||
|
if m.daysOfTheMonth == nil {
|
||||||
|
return ErrMonthlyJobDaysNil
|
||||||
|
}
|
||||||
|
|
||||||
|
var daysStart, daysEnd []int
|
||||||
|
for _, day := range m.daysOfTheMonth() {
|
||||||
|
if day > 31 || day == 0 || day < -31 {
|
||||||
|
return ErrMonthlyJobDays
|
||||||
|
}
|
||||||
|
if day > 0 {
|
||||||
|
daysStart = append(daysStart, day)
|
||||||
|
} else {
|
||||||
|
daysEnd = append(daysEnd, day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
daysStart = removeSliceDuplicatesInt(daysStart)
|
||||||
|
slices.Sort(daysStart)
|
||||||
|
ms.days = daysStart
|
||||||
|
|
||||||
|
daysEnd = removeSliceDuplicatesInt(daysEnd)
|
||||||
|
slices.Sort(daysEnd)
|
||||||
|
ms.daysFromEnd = daysEnd
|
||||||
|
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(m.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrMonthlyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrMonthlyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrMonthlyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrMonthlyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
ms.atTimes = atTimesDate
|
||||||
|
|
||||||
|
j.jobSchedule = ms
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type days []int
|
||||||
|
|
||||||
|
// DaysOfTheMonth defines a function that returns a list of days.
|
||||||
|
type DaysOfTheMonth func() days
|
||||||
|
|
||||||
|
// NewDaysOfTheMonth provide the days of the month the job should
|
||||||
|
// run. The days can be positive 1 to 31 and/or negative -31 to -1.
|
||||||
|
// Negative values count backwards from the end of the month.
|
||||||
|
// For example: -1 == the last day of the month.
|
||||||
|
//
|
||||||
|
// -5 == 5 days before the end of the month.
|
||||||
|
func NewDaysOfTheMonth(day int, moreDays ...int) DaysOfTheMonth {
|
||||||
|
return func() days {
|
||||||
|
moreDays = append(moreDays, day)
|
||||||
|
return moreDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type atTime struct {
|
||||||
|
hours, minutes, seconds uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a atTime) time(location *time.Location) time.Time {
|
||||||
|
return time.Date(0, 0, 0, int(a.hours), int(a.minutes), int(a.seconds), 0, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AtTime defines a function that returns the internal atTime
|
||||||
|
type AtTime func() atTime
|
||||||
|
|
||||||
|
// NewAtTime provide the hours, minutes and seconds at which
|
||||||
|
// the job should be run
|
||||||
|
func NewAtTime(hours, minutes, seconds uint) AtTime {
|
||||||
|
return func() atTime {
|
||||||
|
return atTime{hours: hours, minutes: minutes, seconds: seconds}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AtTimes define a list of AtTime
|
||||||
|
type AtTimes func() []AtTime
|
||||||
|
|
||||||
|
// NewAtTimes provide the hours, minutes and seconds at which
|
||||||
|
// the job should be run
|
||||||
|
func NewAtTimes(atTime AtTime, atTimes ...AtTime) AtTimes {
|
||||||
|
return func() []AtTime {
|
||||||
|
atTimes = append(atTimes, atTime)
|
||||||
|
return atTimes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonthlyJob runs the job on the interval of months, on the specific days of the month
|
||||||
|
// specified, and at the set times. Days of the month can be 1 to 31 or negative (-1 to -31), which
|
||||||
|
// count backwards from the end of the month. E.g. -1 is the last day of the month.
|
||||||
|
//
|
||||||
|
// If a day of the month is selected that does not exist in all months (e.g. 31st)
|
||||||
|
// any month that does not have that day will be skipped.
|
||||||
|
//
|
||||||
|
// By default, the job will start the next available day, considering the last run to be now,
|
||||||
|
// and the time and month based on the interval, days and times you input.
|
||||||
|
// This means, if you select an interval greater than 1, your job by default will run
|
||||||
|
// X (interval) months from now if there are no daysOfTheMonth left in the current month.
|
||||||
|
// You can use WithStartAt to tell the scheduler to start the job sooner.
|
||||||
|
//
|
||||||
|
// Carefully consider your configuration!
|
||||||
|
// - For example: an interval of 2 months on the 31st of each month, starting 12/31
|
||||||
|
// would skip Feb, April, June, and next run would be in August.
|
||||||
|
func MonthlyJob(interval uint, daysOfTheMonth DaysOfTheMonth, atTimes AtTimes) JobDefinition {
|
||||||
|
return monthlyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
daysOfTheMonth: daysOfTheMonth,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*oneTimeJobDefinition)(nil)
|
||||||
|
|
||||||
|
type oneTimeJobDefinition struct {
|
||||||
|
startAt OneTimeJobStartAtOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
||||||
|
j.jobSchedule = oneTimeJob{}
|
||||||
|
return o.startAt(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneTimeJobStartAtOption defines when the one time job is run
|
||||||
|
type OneTimeJobStartAtOption func(*internalJob) error
|
||||||
|
|
||||||
|
// OneTimeJobStartImmediately tells the scheduler to run the one time job immediately.
|
||||||
|
func OneTimeJobStartImmediately() OneTimeJobStartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.startImmediately = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneTimeJobStartDateTime sets the date & time at which the job should run.
|
||||||
|
// This datetime must be in the future.
|
||||||
|
func OneTimeJobStartDateTime(start time.Time) OneTimeJobStartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if start.IsZero() || start.Before(time.Now()) {
|
||||||
|
return ErrOneTimeJobStartDateTimePast
|
||||||
|
}
|
||||||
|
j.startTime = start
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneTimeJob is to run a job once at a specified time and not on
|
||||||
|
// any regular schedule.
|
||||||
|
func OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {
|
||||||
|
return oneTimeJobDefinition{
|
||||||
|
startAt: startAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ----------------- Job Options -----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// JobOption defines the constructor for job options.
|
||||||
|
type JobOption func(*internalJob) error
|
||||||
|
|
||||||
|
// WithEventListeners sets the event listeners that should be
|
||||||
|
// run for the job.
|
||||||
|
func WithEventListeners(eventListeners ...EventListener) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
for _, eventListener := range eventListeners {
|
||||||
|
if err := eventListener(j); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLimitedRuns limits the number of executions of this job to n.
|
||||||
|
// Upon reaching the limit, the job is removed from the scheduler.
|
||||||
|
func WithLimitedRuns(limit uint) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.limitRunsTo = &limitRunsTo{
|
||||||
|
limit: limit,
|
||||||
|
runCount: 0,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName sets the name of the job. Name provides
|
||||||
|
// a human-readable identifier for the job.
|
||||||
|
func WithName(name string) JobOption {
|
||||||
|
// TODO use the name for metrics and future logging option
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if name == "" {
|
||||||
|
return ErrWithNameEmpty
|
||||||
|
}
|
||||||
|
j.name = name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSingletonMode keeps the job from running again if it is already running.
|
||||||
|
// This is useful for jobs that should not overlap, and that occasionally
|
||||||
|
// (but not consistently) run longer than the interval between job runs.
|
||||||
|
func WithSingletonMode(mode LimitMode) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.singletonMode = true
|
||||||
|
j.singletonLimitMode = mode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStartAt sets the option for starting the job at
|
||||||
|
// a specific datetime.
|
||||||
|
func WithStartAt(option StartAtOption) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
return option(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAtOption defines options for starting the job
|
||||||
|
type StartAtOption func(*internalJob) error
|
||||||
|
|
||||||
|
// WithStartImmediately tells the scheduler to run the job immediately
|
||||||
|
// regardless of the type or schedule of job. After this immediate run
|
||||||
|
// the job is scheduled from this time based on the job definition.
|
||||||
|
func WithStartImmediately() StartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.startImmediately = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStartDateTime sets the first date & time at which the job should run.
|
||||||
|
// This datetime must be in the future.
|
||||||
|
func WithStartDateTime(start time.Time) StartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if start.IsZero() || start.Before(time.Now()) {
|
||||||
|
return ErrWithStartDateTimePast
|
||||||
|
}
|
||||||
|
j.startTime = start
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTags sets the tags for the job. Tags provide
|
||||||
|
// a way to identify jobs by a set of tags and remove
|
||||||
|
// multiple jobs by tag.
|
||||||
|
func WithTags(tags ...string) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.tags = tags
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Job Event Listeners -------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// EventListener defines the constructor for event
|
||||||
|
// listeners that can be used to listen for job events.
|
||||||
|
type EventListener func(*internalJob) error
|
||||||
|
|
||||||
|
// AfterJobRuns is used to listen for when a job has run regardless
|
||||||
|
// of any returned error value, and run the provided function.
|
||||||
|
func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.afterJobRuns = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterJobRunsWithError is used to listen for when a job has run and
|
||||||
|
// returned an error, and then run the provided function.
|
||||||
|
func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.afterJobRunsWithError = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeJobRuns is used to listen for when a job is about to run and
|
||||||
|
// then run the provided function.
|
||||||
|
func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.beforeJobRuns = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ---------------- Job Schedules ----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type jobSchedule interface {
|
||||||
|
next(lastRun time.Time) time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*cronJob)(nil)
|
||||||
|
|
||||||
|
type cronJob struct {
|
||||||
|
cronSchedule cron.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *cronJob) next(lastRun time.Time) time.Time {
|
||||||
|
return j.cronSchedule.Next(lastRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*durationJob)(nil)
|
||||||
|
|
||||||
|
type durationJob struct {
|
||||||
|
duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *durationJob) next(lastRun time.Time) time.Time {
|
||||||
|
return lastRun.Add(j.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*durationRandomJob)(nil)
|
||||||
|
|
||||||
|
type durationRandomJob struct {
|
||||||
|
min, max time.Duration
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *durationRandomJob) next(lastRun time.Time) time.Time {
|
||||||
|
r := j.rand.Int63n(int64(j.max - j.min))
|
||||||
|
return lastRun.Add(j.min + time.Duration(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*dailyJob)(nil)
|
||||||
|
|
||||||
|
type dailyJob struct {
|
||||||
|
interval uint
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJob) next(lastRun time.Time) time.Time {
|
||||||
|
firstPass := true
|
||||||
|
next := d.nextDay(lastRun, firstPass)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
firstPass = false
|
||||||
|
|
||||||
|
startNextDay := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(d.interval), 0, 0, 0, lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
return d.nextDay(startNextDay, firstPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {
|
||||||
|
for _, at := range d.atTimes {
|
||||||
|
// sub the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if firstPass && atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
} else if !firstPass && !atDate.Before(lastRun) {
|
||||||
|
// now that we're looking at the next day, it's ok to consider
|
||||||
|
// the same at time that was last run (as lastRun has been incremented)
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*weeklyJob)(nil)
|
||||||
|
|
||||||
|
type weeklyJob struct {
|
||||||
|
interval uint
|
||||||
|
daysOfWeek []time.Weekday
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJob) next(lastRun time.Time) time.Time {
|
||||||
|
firstPass := true
|
||||||
|
next := w.nextWeekDayAtTime(lastRun, firstPass)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
firstPass = false
|
||||||
|
|
||||||
|
startOfTheNextIntervalWeek := (lastRun.Day() - int(lastRun.Weekday())) + int(w.interval*7)
|
||||||
|
from := time.Date(lastRun.Year(), lastRun.Month(), startOfTheNextIntervalWeek, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
return w.nextWeekDayAtTime(from, firstPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool) time.Time {
|
||||||
|
for _, wd := range w.daysOfWeek {
|
||||||
|
// checking if we're on the same day or later in the same week
|
||||||
|
if wd >= lastRun.Weekday() {
|
||||||
|
// weekDayDiff is used to add the correct amount to the atDate day below
|
||||||
|
weekDayDiff := wd - lastRun.Weekday()
|
||||||
|
for _, at := range w.atTimes {
|
||||||
|
// sub the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if firstPass && atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
} else if !firstPass && !atDate.Before(lastRun) {
|
||||||
|
// now that we're looking at the next week, it's ok to consider
|
||||||
|
// the same at time that was last run (as lastRun has been incremented)
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*monthlyJob)(nil)
|
||||||
|
|
||||||
|
type monthlyJob struct {
|
||||||
|
interval uint
|
||||||
|
days []int
|
||||||
|
daysFromEnd []int
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJob) next(lastRun time.Time) time.Time {
|
||||||
|
daysList := make([]int, len(m.days))
|
||||||
|
copy(daysList, m.days)
|
||||||
|
firstDayNextMonth := time.Date(lastRun.Year(), lastRun.Month()+1, 1, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
for _, daySub := range m.daysFromEnd {
|
||||||
|
// getting a combined list of all the daysList and the negative daysList
|
||||||
|
// which count backwards from the first day of the next month
|
||||||
|
// -1 == the last day of the month
|
||||||
|
day := firstDayNextMonth.AddDate(0, 0, daySub).Day()
|
||||||
|
daysList = append(daysList, day)
|
||||||
|
}
|
||||||
|
slices.Sort(daysList)
|
||||||
|
|
||||||
|
firstPass := true
|
||||||
|
next := m.nextMonthDayAtTime(lastRun, daysList, firstPass)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
firstPass = false
|
||||||
|
|
||||||
|
from := time.Date(lastRun.Year(), lastRun.Month()+time.Month(m.interval), 1, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
for next.IsZero() {
|
||||||
|
next = m.nextMonthDayAtTime(from, daysList, firstPass)
|
||||||
|
from = from.AddDate(0, int(m.interval), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass bool) time.Time {
|
||||||
|
// find the next day in the month that should run and then check for an at time
|
||||||
|
for _, day := range days {
|
||||||
|
if day >= lastRun.Day() {
|
||||||
|
for _, at := range m.atTimes {
|
||||||
|
// sub the day, and the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if atDate.Month() != lastRun.Month() {
|
||||||
|
// this check handles if we're setting a day not in the current month
|
||||||
|
// e.g. setting day 31 in Feb results in March 2nd
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstPass && atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
} else if !firstPass && !atDate.Before(lastRun) {
|
||||||
|
// now that we're looking at the next month, it's ok to consider
|
||||||
|
// the same at time that was lastRun (as lastRun has been incremented)
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*oneTimeJob)(nil)
|
||||||
|
|
||||||
|
type oneTimeJob struct{}
|
||||||
|
|
||||||
|
func (o oneTimeJob) next(_ time.Time) time.Time {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ---------------- Job Interface ----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// Job provides the available methods on the job
|
||||||
|
// available to the caller.
|
||||||
|
type Job interface {
|
||||||
|
// ID returns the job's unique identifier.
|
||||||
|
ID() uuid.UUID
|
||||||
|
// LastRun returns the time of the job's last run
|
||||||
|
LastRun() (time.Time, error)
|
||||||
|
// Name returns the name defined on the job.
|
||||||
|
Name() string
|
||||||
|
// NextRun returns the time of the job's next scheduled run.
|
||||||
|
NextRun() (time.Time, error)
|
||||||
|
// RunNow runs the job once, now. This does not alter
|
||||||
|
// the existing run schedule, and will respect all job
|
||||||
|
// and scheduler limits.
|
||||||
|
RunNow() error
|
||||||
|
// Tags returns the job's string tags.
|
||||||
|
Tags() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Job = (*job)(nil)
|
||||||
|
|
||||||
|
// job is the internal struct that implements
|
||||||
|
// the public interface. This is used to avoid
|
||||||
|
// leaking information the caller never needs
|
||||||
|
// to have or tinker with.
|
||||||
|
type job struct {
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
tags []string
|
||||||
|
jobOutRequest chan jobOutRequest
|
||||||
|
runJobRequest chan runJobRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) ID() uuid.UUID {
|
||||||
|
return j.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) LastRun() (time.Time, error) {
|
||||||
|
ij := requestJob(j.id, j.jobOutRequest)
|
||||||
|
if ij == nil || ij.id == uuid.Nil {
|
||||||
|
return time.Time{}, ErrJobNotFound
|
||||||
|
}
|
||||||
|
return ij.lastRun, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) Name() string {
|
||||||
|
return j.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) NextRun() (time.Time, error) {
|
||||||
|
ij := requestJob(j.id, j.jobOutRequest)
|
||||||
|
if ij == nil || ij.id == uuid.Nil {
|
||||||
|
return time.Time{}, ErrJobNotFound
|
||||||
|
}
|
||||||
|
return ij.nextRun, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) Tags() []string {
|
||||||
|
return j.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j job) RunNow() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
resp := make(chan error, 1)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case j.runJobRequest <- runJobRequest{
|
||||||
|
id: j.id,
|
||||||
|
outChan: resp,
|
||||||
|
}:
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
return ErrJobRunNowFailed
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrJobRunNowFailed
|
||||||
|
case errReceived := <-resp:
|
||||||
|
err = errReceived
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
101
vendor/github.com/go-co-op/gocron/v2/logger.go
generated
vendored
Normal file
101
vendor/github.com/go-co-op/gocron/v2/logger.go
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//go:generate mockgen -destination=mocks/logger.go -package=gocronmocks . Logger
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the interface that wraps the basic logging methods
|
||||||
|
// used by gocron. The methods are modeled after the standard
|
||||||
|
// library slog package. The default logger is a no-op logger.
|
||||||
|
// To enable logging, use one of the provided New*Logger functions
|
||||||
|
// or implement your own Logger. The actual level of Log that is logged
|
||||||
|
// is handled by the implementation.
|
||||||
|
type Logger interface {
|
||||||
|
Debug(msg string, args ...any)
|
||||||
|
Error(msg string, args ...any)
|
||||||
|
Info(msg string, args ...any)
|
||||||
|
Warn(msg string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Logger = (*noOpLogger)(nil)
|
||||||
|
|
||||||
|
type noOpLogger struct{}
|
||||||
|
|
||||||
|
func (l noOpLogger) Debug(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Error(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Info(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Warn(_ string, _ ...any) {}
|
||||||
|
|
||||||
|
var _ Logger = (*logger)(nil)
|
||||||
|
|
||||||
|
// LogLevel is the level of logging that should be logged
|
||||||
|
// when using the basic NewLogger.
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
// The different log levels that can be used.
|
||||||
|
const (
|
||||||
|
LogLevelError LogLevel = iota
|
||||||
|
LogLevelWarn
|
||||||
|
LogLevelInfo
|
||||||
|
LogLevelDebug
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
log *log.Logger
|
||||||
|
level LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger returns a new Logger that logs at the given level.
|
||||||
|
func NewLogger(level LogLevel) Logger {
|
||||||
|
l := log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
return &logger{
|
||||||
|
log: l,
|
||||||
|
level: level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Debug(msg string, args ...any) {
|
||||||
|
if l.level < LogLevelDebug {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log.Printf("DEBUG: %s%s\n", msg, logFormatArgs(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Error(msg string, args ...any) {
|
||||||
|
if l.level < LogLevelError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log.Printf("ERROR: %s%s\n", msg, logFormatArgs(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Info(msg string, args ...any) {
|
||||||
|
if l.level < LogLevelInfo {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log.Printf("INFO: %s%s\n", msg, logFormatArgs(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Warn(msg string, args ...any) {
|
||||||
|
if l.level < LogLevelWarn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log.Printf("WARN: %s%s\n", msg, logFormatArgs(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFormatArgs(args ...any) string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(args)%2 != 0 {
|
||||||
|
return ", " + fmt.Sprint(args...)
|
||||||
|
}
|
||||||
|
var pairs []string
|
||||||
|
for i := 0; i < len(args); i += 2 {
|
||||||
|
pairs = append(pairs, fmt.Sprintf("%s=%v", args[i], args[i+1]))
|
||||||
|
}
|
||||||
|
return ", " + strings.Join(pairs, ", ")
|
||||||
|
}
|
||||||
776
vendor/github.com/go-co-op/gocron/v2/scheduler.go
generated
vendored
Normal file
776
vendor/github.com/go-co-op/gocron/v2/scheduler.go
generated
vendored
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
//go:generate mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Scheduler = (*scheduler)(nil)
|
||||||
|
|
||||||
|
// Scheduler defines the interface for the Scheduler.
|
||||||
|
type Scheduler interface {
|
||||||
|
// Jobs returns all the jobs currently in the scheduler.
|
||||||
|
Jobs() []Job
|
||||||
|
// NewJob creates a new job in the Scheduler. The job is scheduled per the provided
|
||||||
|
// definition when the Scheduler is started. If the Scheduler is already running
|
||||||
|
// the job will be scheduled when the Scheduler is started.
|
||||||
|
NewJob(JobDefinition, Task, ...JobOption) (Job, error)
|
||||||
|
// RemoveByTags removes all jobs that have at least one of the provided tags.
|
||||||
|
RemoveByTags(...string)
|
||||||
|
// RemoveJob removes the job with the provided id.
|
||||||
|
RemoveJob(uuid.UUID) error
|
||||||
|
// Shutdown should be called when you no longer need
|
||||||
|
// the Scheduler or Job's as the Scheduler cannot
|
||||||
|
// be restarted after calling Shutdown. This is similar
|
||||||
|
// to a Close or Cleanup method and is often deferred after
|
||||||
|
// starting the scheduler.
|
||||||
|
Shutdown() error
|
||||||
|
// Start begins scheduling jobs for execution based
|
||||||
|
// on each job's definition. Job's added to an already
|
||||||
|
// running scheduler will be scheduled immediately based
|
||||||
|
// on definition. Start is non-blocking.
|
||||||
|
Start()
|
||||||
|
// StopJobs stops the execution of all jobs in the scheduler.
|
||||||
|
// This can be useful in situations where jobs need to be
|
||||||
|
// paused globally and then restarted with Start().
|
||||||
|
StopJobs() error
|
||||||
|
// Update replaces the existing Job's JobDefinition with the provided
|
||||||
|
// JobDefinition. The Job's Job.ID() remains the same.
|
||||||
|
Update(uuid.UUID, JobDefinition, Task, ...JobOption) (Job, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ----------------- Scheduler -------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type scheduler struct {
|
||||||
|
shutdownCtx context.Context
|
||||||
|
shutdownCancel context.CancelFunc
|
||||||
|
exec executor
|
||||||
|
jobs map[uuid.UUID]internalJob
|
||||||
|
location *time.Location
|
||||||
|
clock clockwork.Clock
|
||||||
|
started bool
|
||||||
|
globalJobOptions []JobOption
|
||||||
|
logger Logger
|
||||||
|
|
||||||
|
startCh chan struct{}
|
||||||
|
startedCh chan struct{}
|
||||||
|
stopCh chan struct{}
|
||||||
|
stopErrCh chan error
|
||||||
|
allJobsOutRequest chan allJobsOutRequest
|
||||||
|
jobOutRequestCh chan jobOutRequest
|
||||||
|
runJobRequestCh chan runJobRequest
|
||||||
|
newJobCh chan internalJob
|
||||||
|
removeJobCh chan uuid.UUID
|
||||||
|
removeJobsByTagsCh chan []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type jobOutRequest struct {
|
||||||
|
id uuid.UUID
|
||||||
|
outChan chan internalJob
|
||||||
|
}
|
||||||
|
|
||||||
|
type runJobRequest struct {
|
||||||
|
id uuid.UUID
|
||||||
|
outChan chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
type allJobsOutRequest struct {
|
||||||
|
outChan chan []Job
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScheduler creates a new Scheduler instance.
|
||||||
|
// The Scheduler is not started until Start() is called.
|
||||||
|
//
|
||||||
|
// NewJob will add jobs to the Scheduler, but they will not
|
||||||
|
// be scheduled until Start() is called.
|
||||||
|
func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
|
||||||
|
schCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
exec := executor{
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stopTimeout: time.Second * 10,
|
||||||
|
singletonRunners: make(map[uuid.UUID]singletonRunner),
|
||||||
|
logger: &noOpLogger{},
|
||||||
|
|
||||||
|
jobsIn: make(chan jobIn),
|
||||||
|
jobIDsOut: make(chan uuid.UUID),
|
||||||
|
jobOutRequest: make(chan jobOutRequest, 1000),
|
||||||
|
done: make(chan error),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &scheduler{
|
||||||
|
shutdownCtx: schCtx,
|
||||||
|
shutdownCancel: cancel,
|
||||||
|
exec: exec,
|
||||||
|
jobs: make(map[uuid.UUID]internalJob),
|
||||||
|
location: time.Local,
|
||||||
|
clock: clockwork.NewRealClock(),
|
||||||
|
logger: &noOpLogger{},
|
||||||
|
|
||||||
|
newJobCh: make(chan internalJob),
|
||||||
|
removeJobCh: make(chan uuid.UUID),
|
||||||
|
removeJobsByTagsCh: make(chan []string),
|
||||||
|
startCh: make(chan struct{}),
|
||||||
|
startedCh: make(chan struct{}),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stopErrCh: make(chan error, 1),
|
||||||
|
jobOutRequestCh: make(chan jobOutRequest),
|
||||||
|
runJobRequestCh: make(chan runJobRequest),
|
||||||
|
allJobsOutRequest: make(chan allJobsOutRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
err := option(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.logger.Info("gocron: new scheduler created")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case id := <-s.exec.jobIDsOut:
|
||||||
|
s.selectExecJobIDsOut(id)
|
||||||
|
|
||||||
|
case j := <-s.newJobCh:
|
||||||
|
s.selectNewJob(j)
|
||||||
|
|
||||||
|
case id := <-s.removeJobCh:
|
||||||
|
s.selectRemoveJob(id)
|
||||||
|
|
||||||
|
case tags := <-s.removeJobsByTagsCh:
|
||||||
|
s.selectRemoveJobsByTags(tags)
|
||||||
|
|
||||||
|
case out := <-s.exec.jobOutRequest:
|
||||||
|
s.selectJobOutRequest(out)
|
||||||
|
|
||||||
|
case out := <-s.jobOutRequestCh:
|
||||||
|
s.selectJobOutRequest(out)
|
||||||
|
|
||||||
|
case out := <-s.allJobsOutRequest:
|
||||||
|
s.selectAllJobsOutRequest(out)
|
||||||
|
|
||||||
|
case run := <-s.runJobRequestCh:
|
||||||
|
s.selectRunJobRequest(run)
|
||||||
|
|
||||||
|
case <-s.startCh:
|
||||||
|
s.selectStart()
|
||||||
|
|
||||||
|
case <-s.stopCh:
|
||||||
|
s.stopScheduler()
|
||||||
|
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
s.stopScheduler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// --------- Scheduler Channel Methods -----------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// The scheduler's channel functions are broken out here
|
||||||
|
// to allow prioritizing within the select blocks. The idea
|
||||||
|
// being that we want to make sure that scheduling tasks
|
||||||
|
// are not blocked by requests from the caller for information
|
||||||
|
// about jobs.
|
||||||
|
|
||||||
|
func (s *scheduler) stopScheduler() {
|
||||||
|
s.logger.Debug("gocron: stopping scheduler")
|
||||||
|
if s.started {
|
||||||
|
s.exec.stopCh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
j.stop()
|
||||||
|
}
|
||||||
|
for id, j := range s.jobs {
|
||||||
|
<-j.ctx.Done()
|
||||||
|
|
||||||
|
j.ctx, j.cancel = context.WithCancel(s.shutdownCtx)
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if s.started {
|
||||||
|
select {
|
||||||
|
case err = <-s.exec.done:
|
||||||
|
case <-time.After(s.exec.stopTimeout + 1*time.Second):
|
||||||
|
err = ErrStopExecutorTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.stopErrCh <- err
|
||||||
|
s.started = false
|
||||||
|
s.logger.Debug("gocron: scheduler stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectAllJobsOutRequest(out allJobsOutRequest) {
|
||||||
|
outJobs := make([]Job, len(s.jobs))
|
||||||
|
var counter int
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
outJobs[counter] = s.jobFromInternalJob(j)
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
slices.SortFunc(outJobs, func(a, b Job) int {
|
||||||
|
aID, bID := a.ID().String(), b.ID().String()
|
||||||
|
switch {
|
||||||
|
case aID < bID:
|
||||||
|
return -1
|
||||||
|
case aID > bID:
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case out.outChan <- outJobs:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectRunJobRequest(run runJobRequest) {
|
||||||
|
j, ok := s.jobs[run.id]
|
||||||
|
if !ok {
|
||||||
|
select {
|
||||||
|
case run.outChan <- ErrJobNotFound:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
select {
|
||||||
|
case run.outChan <- ErrJobRunNowFailed:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: j.id,
|
||||||
|
shouldSendOut: false,
|
||||||
|
}:
|
||||||
|
select {
|
||||||
|
case run.outChan <- nil:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectRemoveJob(id uuid.UUID) {
|
||||||
|
j, ok := s.jobs[id]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.stop()
|
||||||
|
delete(s.jobs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jobs coming back from the executor to the scheduler that
|
||||||
|
// need to evaluated for rescheduling.
|
||||||
|
func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) {
|
||||||
|
j := s.jobs[id]
|
||||||
|
j.lastRun = j.nextRun
|
||||||
|
|
||||||
|
// if the job has a limited number of runs set, we need to
|
||||||
|
// check how many runs have occurred and stop running this
|
||||||
|
// job if it has reached the limit.
|
||||||
|
if j.limitRunsTo != nil {
|
||||||
|
j.limitRunsTo.runCount = j.limitRunsTo.runCount + 1
|
||||||
|
if j.limitRunsTo.runCount == j.limitRunsTo.limit {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next := j.next(j.lastRun)
|
||||||
|
if next.IsZero() {
|
||||||
|
// the job's next function will return zero for OneTime jobs.
|
||||||
|
// since they are one time only, they do not need rescheduling.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if next.Before(s.now()) {
|
||||||
|
// in some cases the next run time can be in the past, for example:
|
||||||
|
// - the time on the machine was incorrect and has been synced with ntp
|
||||||
|
// - the machine went to sleep, and woke up some time later
|
||||||
|
// in those cases, we want to increment to the next run in the future
|
||||||
|
// and schedule the job for that time.
|
||||||
|
for next.Before(s.now()) {
|
||||||
|
next = j.next(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j.nextRun = next
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
// set the actual timer on the job here and listen for
|
||||||
|
// shut down events so that the job doesn't attempt to
|
||||||
|
// run if the scheduler has been shutdown.
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: j.id,
|
||||||
|
shouldSendOut: true,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// update the job with its new next and last run times and timer.
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectJobOutRequest(out jobOutRequest) {
|
||||||
|
if j, ok := s.jobs[out.id]; ok {
|
||||||
|
select {
|
||||||
|
case out.outChan <- j:
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(out.outChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectNewJob(j internalJob) {
|
||||||
|
if s.started {
|
||||||
|
next := j.startTime
|
||||||
|
if j.startImmediately {
|
||||||
|
next = s.now()
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: j.id,
|
||||||
|
shouldSendOut: true,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if next.IsZero() {
|
||||||
|
next = j.next(s.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
id := j.id
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: id,
|
||||||
|
shouldSendOut: true,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
j.nextRun = next
|
||||||
|
}
|
||||||
|
|
||||||
|
s.jobs[j.id] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectRemoveJobsByTags(tags []string) {
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if slices.Contains(j.tags, tag) {
|
||||||
|
j.stop()
|
||||||
|
delete(s.jobs, j.id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectStart() {
|
||||||
|
s.logger.Debug("gocron: scheduler starting")
|
||||||
|
go s.exec.start()
|
||||||
|
|
||||||
|
s.started = true
|
||||||
|
for id, j := range s.jobs {
|
||||||
|
next := j.startTime
|
||||||
|
if j.startImmediately {
|
||||||
|
next = s.now()
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: id,
|
||||||
|
shouldSendOut: true,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if next.IsZero() {
|
||||||
|
next = j.next(s.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := id
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIn <- jobIn{
|
||||||
|
id: jobID,
|
||||||
|
shouldSendOut: true,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
j.nextRun = next
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.startedCh <- struct{}{}:
|
||||||
|
s.logger.Info("gocron: scheduler started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Scheduler Methods ---------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
func (s *scheduler) now() time.Time {
|
||||||
|
return s.clock.Now().In(s.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) jobFromInternalJob(in internalJob) job {
|
||||||
|
return job{
|
||||||
|
id: in.id,
|
||||||
|
name: in.name,
|
||||||
|
tags: slices.Clone(in.tags),
|
||||||
|
jobOutRequest: s.jobOutRequestCh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Jobs() []Job {
|
||||||
|
outChan := make(chan []Job)
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.allJobsOutRequest <- allJobsOutRequest{outChan: outChan}:
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs []Job
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case jobs = <-outChan:
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) NewJob(jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {
|
||||||
|
return s.addOrUpdateJob(uuid.Nil, jobDefinition, task, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskWrapper Task, options []JobOption) (Job, error) {
|
||||||
|
j := internalJob{}
|
||||||
|
if id == uuid.Nil {
|
||||||
|
j.id = uuid.New()
|
||||||
|
} else {
|
||||||
|
currentJob := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)
|
||||||
|
if currentJob != nil && currentJob.id != uuid.Nil {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return nil, nil
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
<-currentJob.ctx.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
j.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ctx, j.cancel = context.WithCancel(s.shutdownCtx)
|
||||||
|
|
||||||
|
if taskWrapper == nil {
|
||||||
|
return nil, ErrNewJobTaskNil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := taskWrapper()
|
||||||
|
taskFunc := reflect.ValueOf(tsk.function)
|
||||||
|
for taskFunc.Kind() == reflect.Ptr {
|
||||||
|
taskFunc = taskFunc.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskFunc.Kind() != reflect.Func {
|
||||||
|
return nil, ErrNewJobTaskNotFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedParameterLength := taskFunc.Type().NumIn()
|
||||||
|
if len(tsk.parameters) != expectedParameterLength {
|
||||||
|
return nil, ErrNewJobWrongNumberOfParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < expectedParameterLength; i++ {
|
||||||
|
t1 := reflect.TypeOf(tsk.parameters[i]).Kind()
|
||||||
|
if t1 == reflect.Interface || t1 == reflect.Pointer {
|
||||||
|
t1 = reflect.TypeOf(tsk.parameters[i]).Elem().Kind()
|
||||||
|
}
|
||||||
|
t2 := reflect.New(taskFunc.Type().In(i)).Elem().Kind()
|
||||||
|
if t2 == reflect.Interface || t2 == reflect.Pointer {
|
||||||
|
t2 = reflect.Indirect(reflect.ValueOf(taskFunc.Type().In(i))).Kind()
|
||||||
|
}
|
||||||
|
if t1 != t2 {
|
||||||
|
return nil, ErrNewJobWrongTypeOfParameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
j.name = runtime.FuncForPC(taskFunc.Pointer()).Name()
|
||||||
|
j.function = tsk.function
|
||||||
|
j.parameters = tsk.parameters
|
||||||
|
|
||||||
|
// apply global job options
|
||||||
|
for _, option := range s.globalJobOptions {
|
||||||
|
if err := option(&j); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply job specific options, which take precedence
|
||||||
|
for _, option := range options {
|
||||||
|
if err := option(&j); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := definition.setup(&j, s.location); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.newJobCh <- j:
|
||||||
|
}
|
||||||
|
|
||||||
|
return &job{
|
||||||
|
id: j.id,
|
||||||
|
name: j.name,
|
||||||
|
tags: slices.Clone(j.tags),
|
||||||
|
jobOutRequest: s.jobOutRequestCh,
|
||||||
|
runJobRequest: s.runJobRequestCh,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) RemoveByTags(tags ...string) {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.removeJobsByTagsCh <- tags:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) RemoveJob(id uuid.UUID) error {
|
||||||
|
j := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)
|
||||||
|
if j == nil || j.id == uuid.Nil {
|
||||||
|
return ErrJobNotFound
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Start() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.startCh <- struct{}{}:
|
||||||
|
<-s.startedCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) StopJobs() error {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return nil
|
||||||
|
case s.stopCh <- struct{}{}:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-s.stopErrCh:
|
||||||
|
return err
|
||||||
|
case <-time.After(s.exec.stopTimeout + 2*time.Second):
|
||||||
|
return ErrStopSchedulerTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Shutdown() error {
|
||||||
|
s.shutdownCancel()
|
||||||
|
select {
|
||||||
|
case err := <-s.stopErrCh:
|
||||||
|
return err
|
||||||
|
case <-time.After(s.exec.stopTimeout + 2*time.Second):
|
||||||
|
return ErrStopSchedulerTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Update(id uuid.UUID, jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {
|
||||||
|
return s.addOrUpdateJob(id, jobDefinition, task, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Scheduler Options ---------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// SchedulerOption defines the function for setting
|
||||||
|
// options on the Scheduler.
|
||||||
|
type SchedulerOption func(*scheduler) error
|
||||||
|
|
||||||
|
// WithClock sets the clock used by the Scheduler
|
||||||
|
// to the clock provided. See https://github.com/jonboulle/clockwork
|
||||||
|
func WithClock(clock clockwork.Clock) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if clock == nil {
|
||||||
|
return ErrWithClockNil
|
||||||
|
}
|
||||||
|
s.clock = clock
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDistributedElector sets the elector to be used by multiple
|
||||||
|
// Scheduler instances to determine who should be the leader.
|
||||||
|
// Only the leader runs jobs, while non-leaders wait and continue
|
||||||
|
// to check if a new leader has been elected.
|
||||||
|
func WithDistributedElector(elector Elector) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if elector == nil {
|
||||||
|
return ErrWithDistributedElectorNil
|
||||||
|
}
|
||||||
|
s.exec.elector = elector
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDistributedLocker sets the locker to be used by multiple
|
||||||
|
// Scheduler instances to ensure that only one instance of each
|
||||||
|
// job is run.
|
||||||
|
func WithDistributedLocker(locker Locker) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if locker == nil {
|
||||||
|
return ErrWithDistributedLockerNil
|
||||||
|
}
|
||||||
|
s.exec.locker = locker
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGlobalJobOptions sets JobOption's that will be applied to
|
||||||
|
// all jobs added to the scheduler. JobOption's set on the job
|
||||||
|
// itself will override if the same JobOption is set globally.
|
||||||
|
func WithGlobalJobOptions(jobOptions ...JobOption) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
s.globalJobOptions = jobOptions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitMode defines the modes used for handling jobs that reach
|
||||||
|
// the limit provided in WithLimitConcurrentJobs
|
||||||
|
type LimitMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LimitModeReschedule causes jobs reaching the limit set in
|
||||||
|
// WithLimitConcurrentJobs or WithSingletonMode to be skipped
|
||||||
|
// and rescheduled for the next run time rather than being
|
||||||
|
// queued up to wait.
|
||||||
|
LimitModeReschedule = 1
|
||||||
|
|
||||||
|
// LimitModeWait causes jobs reaching the limit set in
|
||||||
|
// WithLimitConcurrentJobs or WithSingletonMode to wait
|
||||||
|
// in a queue until a slot becomes available to run.
|
||||||
|
//
|
||||||
|
// Note: this mode can produce unpredictable results as
|
||||||
|
// job execution order isn't guaranteed. For example, a job that
|
||||||
|
// executes frequently may pile up in the wait queue and be executed
|
||||||
|
// many times back to back when the queue opens.
|
||||||
|
//
|
||||||
|
// Warning: do not use this mode if your jobs will continue to stack
|
||||||
|
// up beyond the ability of the limit workers to keep up. An example of
|
||||||
|
// what NOT to do:
|
||||||
|
//
|
||||||
|
// s, _ := gocron.NewScheduler(gocron.WithLimitConcurrentJobs)
|
||||||
|
// s.NewJob(
|
||||||
|
// gocron.DurationJob(
|
||||||
|
// time.Second,
|
||||||
|
// Task{
|
||||||
|
// Function: func() {
|
||||||
|
// time.Sleep(10 * time.Second)
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
LimitModeWait = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithLimitConcurrentJobs sets the limit and mode to be used by the
|
||||||
|
// Scheduler for limiting the number of jobs that may be running at
|
||||||
|
// a given time.
|
||||||
|
func WithLimitConcurrentJobs(limit uint, mode LimitMode) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if limit == 0 {
|
||||||
|
return ErrWithLimitConcurrentJobsZero
|
||||||
|
}
|
||||||
|
s.exec.limitMode = &limitModeConfig{
|
||||||
|
mode: mode,
|
||||||
|
limit: limit,
|
||||||
|
in: make(chan jobIn, 1000),
|
||||||
|
singletonJobs: make(map[uuid.UUID]struct{}),
|
||||||
|
}
|
||||||
|
if mode == LimitModeReschedule {
|
||||||
|
s.exec.limitMode.rescheduleLimiter = make(chan struct{}, limit)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLocation sets the location (i.e. timezone) that the scheduler
|
||||||
|
// should operate within. In many systems time.Local is UTC.
|
||||||
|
// Default: time.Local
|
||||||
|
func WithLocation(location *time.Location) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if location == nil {
|
||||||
|
return ErrWithLocationNil
|
||||||
|
}
|
||||||
|
s.location = location
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger sets the logger to be used by the Scheduler.
|
||||||
|
func WithLogger(logger Logger) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if logger == nil {
|
||||||
|
return ErrWithLoggerNil
|
||||||
|
}
|
||||||
|
s.logger = logger
|
||||||
|
s.exec.logger = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStopTimeout sets the amount of time the Scheduler should
|
||||||
|
// wait gracefully for jobs to complete before returning when
|
||||||
|
// StopJobs() or Shutdown() are called.
|
||||||
|
// Default: 10 * time.Second
|
||||||
|
func WithStopTimeout(timeout time.Duration) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if timeout <= 0 {
|
||||||
|
return ErrWithStopTimeoutZeroOrNegative
|
||||||
|
}
|
||||||
|
s.exec.stopTimeout = timeout
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
122
vendor/github.com/go-co-op/gocron/v2/util.go
generated
vendored
Normal file
122
vendor/github.com/go-co-op/gocron/v2/util.go
generated
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func callJobFuncWithParams(jobFunc any, params ...any) error {
|
||||||
|
if jobFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f := reflect.ValueOf(jobFunc)
|
||||||
|
if f.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(params) != f.Type().NumIn() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
in := make([]reflect.Value, len(params))
|
||||||
|
for k, param := range params {
|
||||||
|
in[k] = reflect.ValueOf(param)
|
||||||
|
}
|
||||||
|
returnValues := f.Call(in)
|
||||||
|
for _, val := range returnValues {
|
||||||
|
i := val.Interface()
|
||||||
|
if err, ok := i.(error); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJob(id uuid.UUID, ch chan jobOutRequest) *internalJob {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return requestJobCtx(ctx, id, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJobCtx(ctx context.Context, id uuid.UUID, ch chan jobOutRequest) *internalJob {
|
||||||
|
resp := make(chan internalJob, 1)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ch <- jobOutRequest{
|
||||||
|
id: id,
|
||||||
|
outChan: resp,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var j internalJob
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case jobReceived := <-resp:
|
||||||
|
j = jobReceived
|
||||||
|
}
|
||||||
|
return &j
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSliceDuplicatesInt(in []int) []int {
|
||||||
|
m := make(map[int]struct{})
|
||||||
|
|
||||||
|
for _, i := range in {
|
||||||
|
m[i] = struct{}{}
|
||||||
|
}
|
||||||
|
return maps.Keys(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAtTimesToDateTime(atTimes AtTimes, location *time.Location) ([]time.Time, error) {
|
||||||
|
if atTimes == nil {
|
||||||
|
return nil, errAtTimesNil
|
||||||
|
}
|
||||||
|
var atTimesDate []time.Time
|
||||||
|
for _, a := range atTimes() {
|
||||||
|
if a == nil {
|
||||||
|
return nil, errAtTimeNil
|
||||||
|
}
|
||||||
|
at := a()
|
||||||
|
if at.hours > 23 {
|
||||||
|
return nil, errAtTimeHours
|
||||||
|
} else if at.minutes > 59 || at.seconds > 59 {
|
||||||
|
return nil, errAtTimeMinSec
|
||||||
|
}
|
||||||
|
atTimesDate = append(atTimesDate, at.time(location))
|
||||||
|
}
|
||||||
|
slices.SortStableFunc(atTimesDate, func(a, b time.Time) int {
|
||||||
|
return a.Compare(b)
|
||||||
|
})
|
||||||
|
return atTimesDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type waitGroupWithMutex struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Add(delta int) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wg.Add(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Done() {
|
||||||
|
w.wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Wait() {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wg.Wait()
|
||||||
|
}
|
||||||
8
vendor/github.com/go-ole/go-ole/.travis.yml
generated
vendored
Normal file
8
vendor/github.com/go-ole/go-ole/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.9.x
|
||||||
|
- 1.10.x
|
||||||
|
- 1.11.x
|
||||||
|
- tip
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user