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