Compare commits
63 Commits
20200828-5
...
master
Author | SHA1 | Date |
---|---|---|
KemoNine | f2292f3099 | |
KemoNine | d335e1f85a | |
KemoNine | 39451ec3b3 | |
KemoNine | 35e0032826 | |
KemoNine | 0c9bfd6a7c | |
KemoNine | 0579b3e97d | |
KemoNine | df886ff66d | |
kemonine | 0c2c136413 | |
kemonine | ab925c4b2c | |
kemonine | 2ffda69ca8 | |
KemoNine | d5a06b79db | |
KemoNine | be99f9ae41 | |
KemoNine | 4b0d611715 | |
KemoNine | 97a3474bd2 | |
KemoNine | 9349b91819 | |
KemoNine | 13d019cf62 | |
KemoNine | c2a21eabd9 | |
KemoNine | 20fab2ade0 | |
KemoNine | 1ccbf4ee03 | |
KemoNine | e979318132 | |
KemoNine | 98d5fdce2f | |
KemoNine | 588145b163 | |
KemoNine | 6634e7d8ef | |
KemoNine | f8b4031aa3 | |
KemoNine | be7c3e63a2 | |
KemoNine | 3ca4b66304 | |
KemoNine | 5eab31f093 | |
KemoNine | 7afb3f17ee | |
KemoNine | 728c281ad3 | |
KemoNine | 15dfaa31f9 | |
KemoNine | 769029b9c2 | |
KemoNine | e326984ca6 | |
KemoNine | a33f8b7539 | |
KemoNine | b0764b684a | |
KemoNine | 20c73767de | |
KemoNine | bfbb296784 | |
KemoNine | 80828aa021 | |
KemoNine | 6c6bbe02fe | |
KemoNine | 638bd93675 | |
KemoNine | 592a152860 | |
KemoNine | 0e5d316731 | |
KemoNine | fffd67f8c0 | |
KemoNine | 1b5d77e07a | |
KemoNine | a1c2415536 | |
KemoNine | 30976f60fa | |
KemoNine | 8b3827220e | |
KemoNine | ce9f53cb45 | |
KemoNine | 712f7c7a45 | |
KemoNine | e2bce8d442 | |
KemoNine | 8737f2fe1a | |
KemoNine | 0834f3779f | |
KemoNine | 1841b19211 | |
KemoNine | a506546f1e | |
KemoNine | 37bbc4658d | |
KemoNine | e3008a6d43 | |
KemoNine | 48d757a6b6 | |
KemoNine | 83743a9f90 | |
KemoNine | 4a516cbed8 | |
KemoNine | 494ac4a1e3 | |
KemoNine | 55459e42f0 | |
KemoNine | d10c9e7a65 | |
KemoNine | 38817ae4d3 | |
KemoNine | 8b0c907c3d |
12
.drone.yml
12
.drone.yml
|
@ -7,10 +7,6 @@ trigger:
|
|||
event:
|
||||
- tag
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
volumes:
|
||||
- name: cache
|
||||
temp: {}
|
||||
|
@ -27,9 +23,11 @@ steps:
|
|||
path: /drone/src/out
|
||||
commands:
|
||||
- cd /drone/src
|
||||
- go build -o out/wifi cmd/wifi/wifi.go
|
||||
- go build -o out/inotify cmd/inotify/inotify.go
|
||||
- go build -o out/ui cmd/ui/ui.go
|
||||
- env GOOS=linux GOARCH=arm64 go build -o out/wifi cmd/wifi/wifi.go
|
||||
- env GOOS=linux GOARCH=arm64 go build -o out/inotify cmd/inotify/inotify.go
|
||||
- env GOOS=linux GOARCH=arm64 go build -o out/gui cmd/gui/gui.go
|
||||
- env GOOS=linux GOARCH=arm64 go build -o out/fan cmd/fan/fan.go
|
||||
- env GOOS=linux GOARCH=arm64 go build -o out/hdmi cmd/hdmi/hdmi.go
|
||||
- cp CHANGELOG.md out/
|
||||
- name: gitea-release
|
||||
image: plugins/gitea-release
|
||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,5 +1,30 @@
|
|||
# Change Log
|
||||
|
||||
## 20200906-1
|
||||
|
||||
- Build bug fixes
|
||||
- Add HDMI on/off command for help with ensuring screen is on OR off at boot since systemd timers won't re-run on boot
|
||||
- Update tools to watch for config changes and restart if config has changed
|
||||
|
||||
## 20200905-4
|
||||
|
||||
- Update fan daemon to be better about loading config
|
||||
- Update slideshow to keep it running UNTIL a user presses a key to enter the config ui
|
||||
- Added support for slideshow restart interval (this is for re-randomizing the slideshow)
|
||||
- Adjust randomization flag for fim to be a little more reliable (put back the seeded version)
|
||||
- Adjusted config defaults and handling to be more reliable
|
||||
|
||||
## 20200905-3
|
||||
|
||||
- Update inotify to use configured path for albums instead of being hard coded
|
||||
|
||||
## 20200827-1 through 20200905-2
|
||||
|
||||
- Full bring up of main config UI
|
||||
- Additional 'advanced' configuration available via config file
|
||||
- Fan management daemon
|
||||
- inotify dameon for watching the main photos storage for changes and restarting slideshow as appropriate
|
||||
|
||||
## 20200826-1
|
||||
|
||||
- Initial bring up of the wifi configuration utility
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Fan
|
||||
|
||||
A simple utility to control the [Argon Fan](https://www.argon40.com/argon-fan-hat-for-raspberry-pi-4-raspberry-pi-3b-and-raspberry-pi-3-b.html) speed based on CPU/GPU temperatures.
|
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
argonFan "git.sungo.io/sungo/argon/fan"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
"git.kemonine.info/PiFrame/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
BUS = 1
|
||||
ADDRESS = 0x1a
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load the config file
|
||||
pfConfig, configFileProvider := config.LoadConfig(false)
|
||||
|
||||
// Watch for config changes and re-load config if needed
|
||||
configFileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
log.Printf("Error setting up watch of config : %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Bail on slideshow if there is a config change so it restarts with updated config
|
||||
log.Fatalf("Config file changed! Exiting!")
|
||||
})
|
||||
|
||||
// Get the various fan related config options as local variables
|
||||
POLL_INTERVAL := pfConfig.String(config.CONFIG_KEY_FAN_POLL_INTERVAL)
|
||||
SPEED_FULL_TEMP := pfConfig.Float64(config.CONFIG_KEY_FAN_SPEEDS + "." + config.CONFIG_MAP_KEY_FAN_SPEED_100)
|
||||
SPEED_SEVENTY_FIVE_PERCENT_TEMP := pfConfig.Float64(config.CONFIG_KEY_FAN_SPEEDS + "." + config.CONFIG_MAP_KEY_FAN_SPEED_75)
|
||||
SPEED_FIFTY_PERCENT_TEMP := pfConfig.Float64(config.CONFIG_KEY_FAN_SPEEDS + "." + config.CONFIG_MAP_KEY_FAN_SPEED_50)
|
||||
SPEED_TWENTY_FIVE_PERCENT_TEMP := pfConfig.Float64(config.CONFIG_KEY_FAN_SPEEDS + "." + config.CONFIG_MAP_KEY_FAN_SPEED_25)
|
||||
SPEED_MINIMUM := pfConfig.Int(config.CONFIG_KEY_FAN_MIN_SPEED)
|
||||
|
||||
// Setup fan and bail if we can't see it
|
||||
fan, err := argonFan.New(ADDRESS, BUS)
|
||||
if err != nil {
|
||||
log.Fatalf("Error working with fan : %s", err)
|
||||
}
|
||||
|
||||
// Safe exit
|
||||
defer fan.SafeClose()
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(
|
||||
sigc,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT,
|
||||
)
|
||||
go func() {
|
||||
<-sigc
|
||||
fan.SafeClose()
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
// Control fan speed based on temps via a ticker / timeout
|
||||
pollInterval, err := time.ParseDuration(POLL_INTERVAL)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing interval duration : %s", err)
|
||||
}
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
for range ticker.C {
|
||||
cpuTemp := utils.GetCPUTemp()
|
||||
gpuTemp := utils.GetGPUTemp()
|
||||
|
||||
if cpuTemp >= SPEED_FULL_TEMP || gpuTemp >= SPEED_FULL_TEMP {
|
||||
if SPEED_MINIMUM > 100 {
|
||||
fan.SetSpeed(SPEED_MINIMUM)
|
||||
} else {
|
||||
fan.SetSpeed(100)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cpuTemp >= SPEED_SEVENTY_FIVE_PERCENT_TEMP || gpuTemp >= SPEED_SEVENTY_FIVE_PERCENT_TEMP {
|
||||
if SPEED_MINIMUM > 75 {
|
||||
fan.SetSpeed(SPEED_MINIMUM)
|
||||
} else {
|
||||
fan.SetSpeed(75)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cpuTemp >= SPEED_FIFTY_PERCENT_TEMP || gpuTemp >= SPEED_FIFTY_PERCENT_TEMP {
|
||||
if SPEED_MINIMUM > 50 {
|
||||
fan.SetSpeed(SPEED_MINIMUM)
|
||||
} else {
|
||||
fan.SetSpeed(50)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cpuTemp >= SPEED_TWENTY_FIVE_PERCENT_TEMP || gpuTemp >= SPEED_TWENTY_FIVE_PERCENT_TEMP {
|
||||
if SPEED_MINIMUM > 25 {
|
||||
fan.SetSpeed(SPEED_MINIMUM)
|
||||
} else {
|
||||
fan.SetSpeed(25)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cpuTemp < SPEED_TWENTY_FIVE_PERCENT_TEMP || gpuTemp < SPEED_TWENTY_FIVE_PERCENT_TEMP {
|
||||
fan.SetSpeed(SPEED_MINIMUM)
|
||||
continue
|
||||
}
|
||||
// We should never get here but...
|
||||
// Maxing fan to be on the safe side
|
||||
log.Print("pf-fan : This should never happen")
|
||||
fan.SetSpeed(100)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# gui
|
||||
|
||||
This is the main source code for the custom UI used by PiFrame. This is responsible for things like restart, WiFi config, ensuring the slide show is running and more.
|
||||
|
||||
## Config
|
||||
|
||||
The GUI uses a config similar to the following. To generate a default config, launch the gui and immediatly select the ```Save & Exit``` button. The generated configuration will be at ```/etc/default/pf.toml```.
|
||||
|
||||
**Please Note**: The below is a sample and may or may not match the coded defaults. We also do *not* recommend changing values that are not present in the UI unless you are sure of the consequences.
|
||||
|
||||
```
|
||||
|
||||
config-ui-only = false
|
||||
|
||||
[albums]
|
||||
root = "/tank/pictures"
|
||||
selected = ["/", "/KemoNine"]
|
||||
|
||||
[fan]
|
||||
minspeed = 10.0
|
||||
pollinginterval = "30s"
|
||||
|
||||
[fan.speeds]
|
||||
100 = 55.0
|
||||
25 = 45.0
|
||||
50 = 50.0
|
||||
75 = 52.0
|
||||
|
||||
[hdmi]
|
||||
off = "*-*-* 00:00:00"
|
||||
on = "*-*-* 06:00:00"
|
||||
|
||||
[slideshow]
|
||||
restartinterval = "168h"
|
||||
slideinterval = "300s"
|
||||
|
||||
|
||||
```
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
"git.kemonine.info/PiFrame/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Command line flag handler
|
||||
f := flag.NewFlagSet("piframe", flag.ContinueOnError)
|
||||
f.Usage = func() {
|
||||
fmt.Println(f.FlagUsages())
|
||||
os.Exit(0)
|
||||
}
|
||||
// Command line flags
|
||||
f.Bool(config.CLI_FLAG_CONFIG_ONLY, false, "Only show the config UI, NOT the slideshow")
|
||||
cliFlag := f.Lookup(config.CLI_FLAG_CONFIG_ONLY)
|
||||
if cliFlag != nil {
|
||||
cliFlag.NoOptDefVal = "true"
|
||||
}
|
||||
// Process command line flags into handler
|
||||
f.Parse(os.Args[1:])
|
||||
|
||||
// Load the config file
|
||||
pfConfig, configFileProvider := config.LoadConfig(false)
|
||||
|
||||
// Watch for config changes and re-load config if needed
|
||||
configFileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
log.Printf("Error setting up watch of config : %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Give the config UI a chance to save and exit clean
|
||||
time.Sleep(time.Minute)
|
||||
|
||||
// Bail on slideshow if there is a config change so it restarts with updated config
|
||||
log.Fatalf("Config file changed! Exiting!")
|
||||
})
|
||||
|
||||
// Process command line flags
|
||||
if err := pfConfig.Load(posflag.Provider(f, ".", pfConfig), nil); err != nil {
|
||||
log.Fatalf("Error loading command line flags : %s", err)
|
||||
}
|
||||
|
||||
hideSlideshow := pfConfig.Bool(config.CLI_FLAG_CONFIG_ONLY)
|
||||
|
||||
// Reset the CLI flag so it's never writted to the config as 'true'
|
||||
pfConfig.Load(confmap.Provider(map[string]interface{}{
|
||||
config.CLI_FLAG_CONFIG_ONLY: false,
|
||||
}, "."), nil)
|
||||
|
||||
if !hideSlideshow {
|
||||
ui.Slideshow(pfConfig)
|
||||
}
|
||||
|
||||
ui.ConfigGui(pfConfig)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# hdmi
|
||||
|
||||
This is the source for the screen on/off tool. systemd timers are great but if the system auto-reboots overnight for update purposes the screen will remain on post-boot. This tool will ensure the screen turns back off if a PiFrame is rebooted during off hours.
|
|
@ -0,0 +1,81 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_VCGENCMD = "/opt/vc/bin//vcgencmd"
|
||||
CMD_VCGENCMD_DISPLAY_POWER = "display_power"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load the config file
|
||||
pfConfig, _ := config.LoadConfig(false)
|
||||
|
||||
// Read config values
|
||||
hdmiOn := pfConfig.String(config.CONFIG_KEY_HDMI_ON)
|
||||
hdmiOff := pfConfig.String(config.CONFIG_KEY_HDMI_OFF)
|
||||
|
||||
// Strip off systemd stuff we don't care about for this purpose
|
||||
hdmiOn = strings.TrimLeft(hdmiOn, "*-*-* ")
|
||||
hdmiOnSplit := strings.Split(hdmiOn, ":")
|
||||
|
||||
// Split the config into hours/minutes/seconds
|
||||
hdmiOff = strings.TrimLeft(hdmiOff, "*-*-* ")
|
||||
hdmiOffSplit := strings.Split(hdmiOff, ":")
|
||||
|
||||
// Parse hdmi on hours/minutes/seconds into ints so they can be used to create real date/time
|
||||
hdmiOnHour, err := strconv.Atoi(hdmiOnSplit[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse hdmi on hour : %s", err)
|
||||
}
|
||||
hdmiOnMinute, err := strconv.Atoi(hdmiOnSplit[1])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse hdmi on minute : %s", err)
|
||||
}
|
||||
hdmiOnSecond, err := strconv.Atoi(hdmiOnSplit[2])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse htmi on second : %s", err)
|
||||
}
|
||||
|
||||
// Parse hdmi off hours/minutes/seconds into ints so they can be used to create real date/time
|
||||
hdmiOffHour, err := strconv.Atoi(hdmiOffSplit[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse hdmi off hour : %s", err)
|
||||
}
|
||||
hdmiOffMinute, err := strconv.Atoi(hdmiOffSplit[1])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse hdmi off minute : %s", err)
|
||||
}
|
||||
hdmiOffSecond, err := strconv.Atoi(hdmiOffSplit[2])
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse hdmi off second : %s", err)
|
||||
}
|
||||
|
||||
// Setup date/time vars for comparison
|
||||
currentTime := time.Now()
|
||||
hdmiOnTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), hdmiOnHour, hdmiOnMinute, hdmiOnSecond, 0, currentTime.Location())
|
||||
hdmiOffTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), hdmiOffHour, hdmiOffMinute, hdmiOffSecond, 0, currentTime.Location())
|
||||
|
||||
// Turn on/off screen depending on current time
|
||||
vcgencmdOnOffFlag := "-1"
|
||||
if currentTime.After(hdmiOnTime) && currentTime.Before(hdmiOffTime) {
|
||||
vcgencmdOnOffFlag = "1"
|
||||
}
|
||||
if currentTime.After(hdmiOffTime) && currentTime.Before(hdmiOnTime) {
|
||||
vcgencmdOnOffFlag = "0"
|
||||
}
|
||||
|
||||
hdmiControl := exec.Command(CMD_VCGENCMD, CMD_VCGENCMD_DISPLAY_POWER, vcgencmdOnOffFlag)
|
||||
err = hdmiControl.Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error turning display on/off (%s) : %s", vcgencmdOnOffFlag, err)
|
||||
}
|
||||
}
|
|
@ -7,16 +7,32 @@ import (
|
|||
|
||||
"github.com/dietsche/rfsnotify"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
"git.kemonine.info/PiFrame/watchdog"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_SYSTEMCTL = "/usr/bin/systemctl"
|
||||
PATH_PICTURES = "/tank/pictures"
|
||||
TIMEOUT = 5 * time.Second
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load the config file
|
||||
pfConfig, configFileProvider := config.LoadConfig(false)
|
||||
|
||||
// Watch for config changes and re-load config if needed
|
||||
configFileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
log.Printf("Error setting up watch of config : %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Bail on slideshow if there is a config change so it restarts with updated config
|
||||
log.Fatalf("Config file changed! Exiting!")
|
||||
})
|
||||
|
||||
PATH_PICTURES := pfConfig.String(config.CONFIG_KEY_ALBUMS_ROOT)
|
||||
|
||||
// Create watchdog timer that restarts pf-ui.service on timeout
|
||||
watchdog := watchdog.New(TIMEOUT, func() {
|
||||
err := exec.Command(CMD_SYSTEMCTL, "restart", "pf-ui.service").Run()
|
||||
|
@ -40,12 +56,10 @@ func main() {
|
|||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
case _, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Print out event (this is where logic will go eventually))
|
||||
log.Printf("event: %#v\n", event)
|
||||
// [Re]Start timer to restart slideshow after the fs events die down
|
||||
watchdog.Kick()
|
||||
case err, ok := <-watcher.Errors:
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# ui
|
||||
|
||||
This is the main source code for the custom UI used by PiFrame. This is responsible for things like restart, WiFi config, ensuring the slide show is running and more.
|
90
cmd/ui/ui.go
90
cmd/ui/ui.go
|
@ -1,90 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_FIM = "/usr/local/bin/pf-fim.sh"
|
||||
SLIDESHOW_INTERVAL = 300 * time.Second
|
||||
)
|
||||
|
||||
func main() {
|
||||
// fim placeholder so we can operate on it when a signal is received
|
||||
var fim *exec.Cmd = nil
|
||||
|
||||
// Setup signal listening
|
||||
sigs := make(chan os.Signal)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer func() { close(sigs) }()
|
||||
|
||||
// Goroutine to handle os signals (nuke fim so we can get to config ui)
|
||||
go func() {
|
||||
for range sigs {
|
||||
if fim != nil { // Just in case someone lays on ctrl-c or similar during startup
|
||||
if err := fim.Process.Kill(); err != nil {
|
||||
log.Fatalf("failed to kill fim : %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run slideshow
|
||||
fim = exec.Command(CMD_FIM)
|
||||
|
||||
// Setup stdin for fim to control slideshow
|
||||
stdin, err := fim.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting fim stdin : %s", err)
|
||||
}
|
||||
|
||||
// Advance slideshow every interval as defined in const()
|
||||
ticker := time.NewTicker(SLIDESHOW_INTERVAL)
|
||||
stop_ticker := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err = io.WriteString(stdin, "n")
|
||||
if err != nil {
|
||||
log.Fatalf("Error advancing slides : %s", err)
|
||||
}
|
||||
case <-stop_ticker:
|
||||
ticker.Stop()
|
||||
stdin.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run fim
|
||||
if err := fim.Run(); err != nil {
|
||||
// Unwrap the error a bit so we can find out if a signal killed fim or something else
|
||||
// An exit code of -1 means the program didn't exit in time or was terminated by a signal (per the docs)
|
||||
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != -1 {
|
||||
log.Fatalf("Error running fim : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop fim slideshow advancing go routine
|
||||
close(stop_ticker)
|
||||
|
||||
// Run config UI when slideshow stops
|
||||
app := tview.NewApplication()
|
||||
frame := tview.NewFrame(tview.NewBox().SetBackgroundColor(tcell.ColorBlack)).
|
||||
SetBorders(2, 2, 2, 2, 4, 4).
|
||||
AddText("PiFrame", true, tview.AlignCenter, tcell.ColorWhite).
|
||||
AddText("Configuration Utility", true, tview.AlignCenter, tcell.ColorRed)
|
||||
if err := app.SetRoot(frame, true).EnableMouse(true).Run(); err != nil {
|
||||
log.Fatalf("Error running UI : %s", err)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"git.kemonine.info/PiFrame/wifi"
|
||||
)
|
||||
|
||||
// Various commands that we need for this process
|
||||
|
@ -13,7 +15,6 @@ const (
|
|||
CMD_BLKID = "/usr/sbin/blkid"
|
||||
CMD_FINDMNT = "/usr/bin/findmnt"
|
||||
CMD_MOUNT = "/usr/bin/mount"
|
||||
CMD_NMCLI = "/usr/bin/nmcli"
|
||||
CMD_UMOUNT = "/usr/bin/umount"
|
||||
)
|
||||
|
||||
|
@ -89,30 +90,11 @@ func main() {
|
|||
// Doing stuff with the file contents goes here
|
||||
essid := config[0]
|
||||
password := config[1]
|
||||
log.Printf("Using config: %s / %s for WiFi\n", essid, password)
|
||||
// Cleanup existing WiFi connections
|
||||
nmcliOut, err := exec.Command(CMD_NMCLI, "-t", "connection", "show").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running %s : %s", CMD_NMCLI, err)
|
||||
}
|
||||
connections := strings.Split(strings.Trim(string(nmcliOut), "\n"), "\n")
|
||||
for _, connection := range connections {
|
||||
details := strings.Split(connection, ":")
|
||||
if details[2] != "802-11-wireless" {
|
||||
continue
|
||||
}
|
||||
log.Printf("Cleaning up WiFi connection %s", details[0])
|
||||
err := exec.Command(CMD_NMCLI, "connection", "del", details[1]).Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running %s : %s", CMD_NMCLI, err)
|
||||
}
|
||||
}
|
||||
// Create new WiFi connection with network manager
|
||||
log.Printf("Connecting to %s with password %s\n", essid, password)
|
||||
err = exec.Command(CMD_NMCLI, "d", "wifi", "connect", essid, "password", password).Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running %s : %s", CMD_NMCLI, err)
|
||||
}
|
||||
|
||||
// Cleanup old wifi configs and apply new one
|
||||
nmWifi := wifi.New(essid, password)
|
||||
nmWifi.ApplyConfig()
|
||||
|
||||
// Unmount the filesystem and continue
|
||||
err = exec.Command(CMD_UMOUNT, MOUNTPOINT).Run()
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# config
|
||||
|
||||
Mini module for working with PiFrame configuration file.
|
|
@ -0,0 +1,40 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
kfile "github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
func LoadConfig(errFatalOnMissing bool) (*koanf.Koanf, *kfile.File) {
|
||||
// Main config variable
|
||||
var pfConfig = koanf.New(".")
|
||||
|
||||
// Setup defaults
|
||||
pfConfig.Load(confmap.Provider(map[string]interface{}{
|
||||
CONFIG_KEY_SLIDESHOW_INTERVAL: DEFAULT_SLIDESHOW_INTERVAL,
|
||||
CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL: DEFAULT_SLIDESHOW_RESTART_INTERVAL,
|
||||
CONFIG_KEY_HDMI_OFF: DEFAULT_HDMI_OFF,
|
||||
CONFIG_KEY_HDMI_ON: DEFAULT_HDMI_ON,
|
||||
CONFIG_KEY_ALBUMS_ROOT: DEFAULT_ALBUMS_ROOT,
|
||||
CONFIG_KEY_ALBUMS_SELECTED: DEFAULT_ALBUM_SELECTED,
|
||||
CONFIG_KEY_FAN_POLL_INTERVAL: DEFAULT_FAN_POLL_INTERVAL,
|
||||
CONFIG_KEY_FAN_SPEEDS: DEFAULT_FAN_SPEEDS,
|
||||
CONFIG_KEY_FAN_MIN_SPEED: DEFAULT_FAN_MIN_SPEED,
|
||||
}, "."), nil)
|
||||
|
||||
// Bring in /etc/defaults/pf.toml if it exists
|
||||
configFileProvider := kfile.Provider(CONFIG_FILE_PATH)
|
||||
if err := pfConfig.Load(configFileProvider, toml.Parser()); err != nil {
|
||||
if errFatalOnMissing {
|
||||
log.Fatalf("Error loading config : %s", err)
|
||||
} else {
|
||||
log.Printf("Error loading config : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return pfConfig, configFileProvider
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
CLI_FLAG_CONFIG_ONLY = "config-ui-only"
|
||||
)
|
||||
|
||||
const (
|
||||
CONFIG_FILE_PATH = "/etc/default/pf.toml"
|
||||
CONFIG_FILE_PATH_TMP = "/etc/default/pf.toml.new"
|
||||
CONFIG_KEY_SLIDESHOW_INTERVAL = "slideshow.slideinterval"
|
||||
CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL = "slideshow.restartinterval"
|
||||
CONFIG_KEY_HDMI_OFF = "hdmi.off"
|
||||
CONFIG_KEY_HDMI_ON = "hdmi.on"
|
||||
CONFIG_KEY_ALBUMS_ROOT = "albums.root"
|
||||
CONFIG_KEY_ALBUMS_SELECTED = "albums.selected"
|
||||
CONFIG_KEY_FAN_POLL_INTERVAL = "fan.pollinginterval"
|
||||
CONFIG_KEY_FAN_SPEEDS = "fan.speeds"
|
||||
CONFIG_KEY_FAN_MIN_SPEED = "fan.minspeed"
|
||||
CONFIG_MAP_KEY_FAN_SPEED_25 = "25"
|
||||
CONFIG_MAP_KEY_FAN_SPEED_50 = "50"
|
||||
CONFIG_MAP_KEY_FAN_SPEED_75 = "75"
|
||||
CONFIG_MAP_KEY_FAN_SPEED_100 = "100"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_SLIDESHOW_INTERVAL = "300s"
|
||||
DEFAULT_SLIDESHOW_RESTART_INTERVAL = "168h"
|
||||
DEFAULT_HDMI_OFF = "*-*-* 00:00:00"
|
||||
DEFAULT_HDMI_ON = "*-*-* 06:00:00"
|
||||
DEFAULT_ALBUMS_ROOT = "/tank/pictures"
|
||||
DEFAULT_FAN_POLL_INTERVAL = "30s"
|
||||
DEFAULT_FAN_MIN_SPEED = 10
|
||||
)
|
||||
|
||||
var DEFAULT_ALBUM_SELECTED = []string{"/"}
|
||||
|
||||
// Speed : Temp to activate speed
|
||||
var DEFAULT_FAN_SPEEDS = map[string]float64{
|
||||
CONFIG_MAP_KEY_FAN_SPEED_25: 45.00,
|
||||
CONFIG_MAP_KEY_FAN_SPEED_50: 50.00,
|
||||
CONFIG_MAP_KEY_FAN_SPEED_75: 52.00,
|
||||
CONFIG_MAP_KEY_FAN_SPEED_100: 55.00,
|
||||
}
|
16
go.mod
16
go.mod
|
@ -3,13 +3,17 @@ module git.kemonine.info/PiFrame
|
|||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/d2r2/go-bh1750 v0.0.0-20181222061755-1195122364ab
|
||||
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc
|
||||
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
git.sungo.io/sungo/argon v0.0.0-20200904233623-18a98e1a4706
|
||||
github.com/dietsche/rfsnotify v0.0.0-20200716145600-b37be6e4177f
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/rivo/tview v0.0.0-20200818120338-53d50e499bf9
|
||||
github.com/guillermo/go.procmeminfo v0.0.0-20131127224636-be4355a9fb0e
|
||||
github.com/knadh/koanf v0.13.0
|
||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200915114512-42866ecf6ca6
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
)
|
||||
|
|
212
go.sum
212
go.sum
|
@ -1,14 +1,39 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
git.sungo.io/sungo/argon v0.0.0-20200904233623-18a98e1a4706 h1:gD/4qfTLeuaG9/MqcQYzoVutiADl6JtYL05PcPfETwc=
|
||||
git.sungo.io/sungo/argon v0.0.0-20200904233623-18a98e1a4706/go.mod h1:3XJJhLC7vO4Yk96G/LEiiwBDXAEdj2lVB/oueweUShY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/d2r2/go-bh1750 v0.0.0-20181222061755-1195122364ab h1:8zeQTn3owfeyIA5KIhtvcj9wlMStdFaxbYwPkWhGVgo=
|
||||
github.com/d2r2/go-bh1750 v0.0.0-20181222061755-1195122364ab/go.mod h1:3atw7ac57A1fKlJIGeLBaeXdh80Revqk6uVl7bZyH10=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc h1:HLRSIWzUGMLCq4ldt0W1GLs3nnAxa5EGoP+9qHgh6j0=
|
||||
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc/go.mod h1:AwxDPnsgIpy47jbGXZHA9Rv7pDkOJvQbezPuK1Y+nNk=
|
||||
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e h1:ZG3JBA6rPRl0xxQ+nNSfO7tor8w+CNCTs05DNJQYbLM=
|
||||
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e/go.mod h1:oA+9PUt8F1aKJ6o4YU1T120i7sgo1T6/1LWEEBy0BSs=
|
||||
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dietsche/rfsnotify v0.0.0-20200716145600-b37be6e4177f h1:b3QvpXLSx1U13VM79rSkA+6Xv4lmT/urEMzA36Yma0U=
|
||||
github.com/dietsche/rfsnotify v0.0.0-20200716145600-b37be6e4177f/go.mod h1:ztitxkMUaBsHRey1tS5xFCd4gm/zAQwA9yfCP5y4cAA=
|
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ=
|
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
|
@ -16,25 +41,208 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
|
|||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.11.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/guillermo/go.procmeminfo v0.0.0-20131127224636-be4355a9fb0e h1:/6/OurM62Ddm8CR8PveE0a+ql2mL+ycAhOwd563kpdg=
|
||||
github.com/guillermo/go.procmeminfo v0.0.0-20131127224636-be4355a9fb0e/go.mod h1:TQrLAmkOSnZ4g1eFORtCfTEbFuVZD0Zm55vdnrilBaw=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knadh/koanf v0.12.0 h1:xQo0Y43CbzOix0tTeE+plIcfs1pTuaUI1/SsvDl2ROI=
|
||||
github.com/knadh/koanf v0.12.0/go.mod h1:31bzRSM7vS5Vm9LNLo7B2Re1zhLOZT6EQKeodixBikE=
|
||||
github.com/knadh/koanf v0.13.0 h1:OEjNdmrP/5oAhJkNwTtarioqOC4xe6WxRK8Q5ffW8WU=
|
||||
github.com/knadh/koanf v0.13.0/go.mod h1:7XDF7OJIqSQLUZnaXkjb1HB3CgMEYHyrzmgT8A6xAaE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rivo/tview v0.0.0-20200818120338-53d50e499bf9 h1:csnip7QsoiE2Ee0RkELN1YggwejK2EFfcjU6tXOT0Q8=
|
||||
github.com/rivo/tview v0.0.0-20200818120338-53d50e499bf9/go.mod h1:xV4Aw4WIX8cmhg71U7MUHBdpIQ7zSEXdRruGHLaEAOc=
|
||||
github.com/rivo/tview v0.0.0-20200915114512-42866ecf6ca6 h1:LhmHZTzElCYlOXEWXWOQXy/vgjPsdiDb7LzHV8mTKvI=
|
||||
github.com/rivo/tview v0.0.0-20200915114512-42866ecf6ca6/go.mod h1:xV4Aw4WIX8cmhg71U7MUHBdpIQ7zSEXdRruGHLaEAOc=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/warthog618/config v0.4.1/go.mod h1:IzcIkVay6dCubN3WBAJzPuqHyE1fTPxICvKTQ/2JA9g=
|
||||
github.com/warthog618/gpio v1.0.0/go.mod h1:3yuGbOkcAcs8/pRFEnCnN7Qt2S+TkISbFXM+5gliAZM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw=
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# ui
|
||||
|
||||
This is the main implementation for various aspects of the main PiFrame [G]UI
|
|
@ -0,0 +1,595 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/guillermo/go.procmeminfo"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
"git.kemonine.info/PiFrame/utils"
|
||||
"git.kemonine.info/PiFrame/wifi"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_SYSTEMCTL = "/usr/bin/systemctl"
|
||||
CMD_FINDMNT = "/usr/bin/findmnt"
|
||||
SYNCTHING_FOLDER_SKIP = ".stfolder"
|
||||
)
|
||||
|
||||
const (
|
||||
PAGE_MAIN_UI = "PAGE_MAIN_UI"
|
||||
PAGE_SAVE_EXIT = "PAGE_SAVE_EXIT"
|
||||
PAGE_EXIT = "PAGE_EXIT"
|
||||
PAGE_REBOOT = "PAGE_REBOOT"
|
||||
PAGE_POWEROFF = "PAGE_POWEROFF"
|
||||
)
|
||||
|
||||
// Bullshit to remove an element from a slice
|
||||
func remove(s []string, index int) []string {
|
||||
return append(s[:index], s[index+1:]...)
|
||||
}
|
||||
|
||||
// Housekeeping
|
||||
var app *tview.Application
|
||||
var main *tview.Flex
|
||||
var menu *tview.List
|
||||
|
||||
// Function to reset the main input area that's used all over the place
|
||||
func resetMain() {
|
||||
main.Clear()
|
||||
app.SetFocus(menu)
|
||||
}
|
||||
|
||||
func ConfigGui(pfconfig *koanf.Koanf) {
|
||||
// Memory info for status panel
|
||||
meminfo := &procmeminfo.MemInfo{}
|
||||
err := meminfo.Update()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting memory info : %s", err)
|
||||
}
|
||||
|
||||
// Network interfaces for status panel
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting netork interfaces : %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Disk use
|
||||
findmntOut, err := exec.Command(CMD_FINDMNT, "-n", "-l",
|
||||
"-o", "TARGET,USE%",
|
||||
"-t", "ext4,exfat,vfat,btrfs,zfs,xfs").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting disk use : %s", err)
|
||||
}
|
||||
filesystems := strings.Split(strings.Trim(string(findmntOut), "\n"), "\n")
|
||||
|
||||
// GPU Temp
|
||||
gpuTemp := fmt.Sprintf("%.2f'C", utils.GetGPUTemp())
|
||||
|
||||
// CPU Temp
|
||||
cpuTemp := fmt.Sprintf("%.2f'C", utils.GetCPUTemp())
|
||||
|
||||
// Get list of all folders that can be used as albums
|
||||
var albums []string
|
||||
err = filepath.Walk(pfconfig.String(config.CONFIG_KEY_ALBUMS_ROOT), func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if strings.Contains(path, SYNCTHING_FOLDER_SKIP) {
|
||||
return nil
|
||||
}
|
||||
albumName := strings.TrimPrefix(path, pfconfig.String(config.CONFIG_KEY_ALBUMS_ROOT))
|
||||
if albumName == "" {
|
||||
albumName = "Main Folder"
|
||||
}
|
||||
albums = append(albums, albumName)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting list of albums : %s", err)
|
||||
}
|
||||
|
||||
// Run config UI when slideshow stops
|
||||
app = tview.NewApplication()
|
||||
|
||||
// Header
|
||||
headerTitle := tview.NewTextView().
|
||||
SetText("PiFrame").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetTextColor(tcell.ColorAqua)
|
||||
|
||||
headerSubTitle := tview.NewTextView().
|
||||
SetText("Management Utility").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetTextColor(tcell.ColorSilver)
|
||||
header := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow)
|
||||
header.AddItem(headerTitle, 0, 1, false).
|
||||
AddItem(headerSubTitle, 0, 1, false)
|
||||
|
||||
// Footer fields (Left Column)
|
||||
saveExitButton := tview.NewButton("Save & Exit").
|
||||
SetBackgroundColorActivated(tcell.ColorGray)
|
||||
saveExitButton.SetLabelColor(tcell.ColorBlack).
|
||||
SetBorder(true).
|
||||
SetBorderColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorGreen).
|
||||
SetRect(0, 0, 22, 3)
|
||||
|
||||
exitButton := tview.NewButton("Exit").
|
||||
SetBackgroundColorActivated(tcell.ColorGray)
|
||||
exitButton.SetLabelColor(tcell.ColorBlack).
|
||||
SetBorder(true).
|
||||
SetBorderColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorYellow).
|
||||
SetRect(0, 0, 22, 3)
|
||||
|
||||
rebootButton := tview.NewButton("Reboot").
|
||||
SetBackgroundColorActivated(tcell.ColorGray)
|
||||
rebootButton.SetLabelColor(tcell.ColorBlack).
|
||||
SetBorder(true).
|
||||
SetBorderColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorFuchsia).
|
||||
SetRect(0, 0, 22, 3)
|
||||
|
||||
powerOffButton := tview.NewButton("Power Off").
|
||||
SetBackgroundColorActivated(tcell.ColorGray)
|
||||
powerOffButton.SetLabelColor(tcell.ColorBlack).
|
||||
SetBorder(true).
|
||||
SetBorderColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorRed).
|
||||
SetRect(0, 0, 22, 3)
|
||||
|
||||
// Footer
|
||||
footer := tview.NewFlex()
|
||||
footer.AddItem(saveExitButton, 0, 1, false).
|
||||
AddItem(exitButton, 0, 1, false).
|
||||
AddItem(rebootButton, 0, 1, false).
|
||||
AddItem(powerOffButton, 0, 1, false)
|
||||
// Setup menu
|
||||
menu = tview.NewList()
|
||||
menu.SetBorder(true).
|
||||
SetTitle("Menu").
|
||||
SetTitleColor(tcell.ColorAqua)
|
||||
menu.AddItem("Select Albums", "", '1', nil)
|
||||
menu.AddItem("Intervals", "", '2', nil)
|
||||
menu.AddItem("WiFi", "", '3', nil)
|
||||
menu.AddItem("HDMI On/Off", "", '5', nil)
|
||||
menu.AddItem("Advanced", "", '6', nil)
|
||||
|
||||
// Setup base var for main column so the menu setup is easier to manage
|
||||
main = tview.NewFlex().
|
||||
SetDirection(tview.FlexRow)
|
||||
|
||||
// Setup main panel (Center column)
|
||||
main.SetTitle("").
|
||||
SetBorder(true).
|
||||
SetTitleColor(tcell.ColorAqua)
|
||||
|
||||
// Select Albums Form
|
||||
selectAlbumsForm := tview.NewForm()
|
||||
configSelectedAlbums := pfconfig.Strings(config.CONFIG_KEY_ALBUMS_SELECTED)
|
||||
albumCheckboxes := []*tview.Checkbox{}
|
||||
for _, album := range albums {
|
||||
albumSelected := false
|
||||
for _, configSelectedAlbum := range configSelectedAlbums {
|
||||
if configSelectedAlbum == "/" {
|
||||
configSelectedAlbum = "Main Folder"
|
||||
}
|
||||
if album == configSelectedAlbum {
|
||||
albumSelected = true
|
||||
}
|
||||
}
|
||||
albumCheckbox := tview.NewCheckbox()
|
||||
albumCheckbox.SetLabel(album)
|
||||
albumCheckbox.SetChecked(albumSelected)
|
||||
albumCheckboxes = append(albumCheckboxes, albumCheckbox)
|
||||
selectAlbumsForm.AddFormItem(albumCheckbox)
|
||||
}
|
||||
selectAlbumsForm.AddButton("Apply", resetMain)
|
||||
selectAlbumsForm.AddButton("Cancel", resetMain)
|
||||
|
||||
// Slide Interval Form
|
||||
intervalsForm := tview.NewForm()
|
||||
configSlideInterval := pfconfig.String(config.CONFIG_KEY_SLIDESHOW_INTERVAL)
|
||||
configRestartInterval := pfconfig.String(config.CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL)
|
||||
intervalsForm.AddInputField("Slide", configSlideInterval, 0, nil, func(value string) {
|
||||
configSlideInterval = value
|
||||
})
|
||||
intervalsForm.AddInputField("Restart/Reshuffle", configRestartInterval, 0, nil, func(value string) {
|
||||
configRestartInterval = value
|
||||
})
|
||||
intervalsForm.AddButton("Apply", resetMain)
|
||||
intervalsForm.AddButton("Cancel", resetMain)
|
||||
|
||||
// WiFi Config Form
|
||||
wifiConfigForm := tview.NewForm()
|
||||
wifiConfigAccessPoint := ""
|
||||
wifiConfigPassword := ""
|
||||
wifiConfigForm.AddInputField("Access Point", "", 0, nil, func(value string) {
|
||||
wifiConfigAccessPoint = value
|
||||
})
|
||||
wifiConfigForm.AddPasswordField("Password", "", 0, '*', func(value string) {
|
||||
wifiConfigPassword = value
|
||||
})
|
||||
wifiConfigForm.AddButton("Apply", func() {
|
||||
// Cleanup old wifi configs and apply new one
|
||||
nmWifi := wifi.New(wifiConfigAccessPoint, wifiConfigPassword)
|
||||
nmWifi.ApplyConfig()
|
||||
resetMain()
|
||||
})
|
||||
wifiConfigForm.AddButton("Cancel", resetMain)
|
||||
|
||||
// HDMI On/Off Form
|
||||
hdmiForm := tview.NewForm()
|
||||
configHDMIOff := pfconfig.String(config.CONFIG_KEY_HDMI_OFF)
|
||||
configHDMIOn := pfconfig.String(config.CONFIG_KEY_HDMI_ON)
|
||||
hdmiForm.AddInputField("HDMI Off Schedule", configHDMIOff, 0, nil, func(value string) {
|
||||
configHDMIOff = value
|
||||
})
|
||||
hdmiForm.AddInputField("HDMI On Schedule", configHDMIOn, 0, nil, func(value string) {
|
||||
configHDMIOn = value
|
||||
})
|
||||
hdmiForm.AddButton("Apply", resetMain)
|
||||
hdmiForm.AddButton("Cancel", resetMain)
|
||||
|
||||
// Advanced config form
|
||||
advancedForm := tview.NewForm()
|
||||
advancedForm.AddButton("Go Back", resetMain)
|
||||
|
||||
// Setup menu selection handler
|
||||
menu.SetSelectedFunc(func(index int, title string, desc string, shortcut rune) {
|
||||
if title == "Select Albums" {
|
||||
main.SetTitle("Select Albums")
|
||||
main.Clear()
|
||||
main.AddItem(selectAlbumsForm, 0, 1, true)
|
||||
app.SetFocus(selectAlbumsForm)
|
||||
}
|
||||
if title == "WiFi" {
|
||||
main.SetTitle("Configure WiFi")
|
||||
main.Clear()
|
||||
main.AddItem(wifiConfigForm, 0, 1, true)
|
||||
app.SetFocus(wifiConfigForm)
|
||||
}
|
||||
if title == "Intervals" {
|
||||
main.SetTitle("Configure Intervals")
|
||||
main.Clear()
|
||||
main.AddItem(intervalsForm, 0, 1, true)
|
||||
main.AddItem(tview.NewTextView().SetText("Intervals are a number + letter\n\nUse\ns for seconds\nm for minutes\nh for hours"), 0, 1, false)
|
||||
app.SetFocus(intervalsForm)
|
||||
}
|
||||
if title == "HDMI On/Off" {
|
||||
main.SetTitle("Configure HDMI On/Off")
|
||||
main.Clear()
|
||||
main.AddItem(hdmiForm, 0, 1, true)
|
||||
main.AddItem(tview.NewTextView().SetText("These values are date+time combos\n\nPlease see https://www.freedesktop.org/software/systemd/man/systemd.time.html for details\n\nONLY adjust the times (24h format) if unsure of what to change"), 0, 1, false)
|
||||
app.SetFocus(hdmiForm)
|
||||
}
|
||||
if title == "Advanced" {
|
||||
main.SetTitle("Advanced Configuration")
|
||||
main.Clear()
|
||||
main.AddItem(tview.NewTextView().SetText("For advanced configuration edit /etc/default/pf.toml via the command line. There are a few config values that are DANGEROUS to adjust and not exposed through this ui."), 0, 1, false)
|
||||
main.AddItem(advancedForm, 0, 1, false)
|
||||
app.SetFocus(advancedForm)
|
||||
}
|
||||
})
|
||||
|
||||
// Side bar fields
|
||||
sideBarCPUTempTitle := tview.NewTextView().
|
||||
SetText("CPU Temperature").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
sideBarCPUTemp := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %s", cpuTemp))
|
||||
sideBarGPUTempTitle := tview.NewTextView().
|
||||
SetText("GPU Temperature").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
sideBarGPUTemp := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %s", gpuTemp))
|
||||
sideBarMemoryTitle := tview.NewTextView().
|
||||
SetText("Memory Use (Mb)").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
divisor := math.Pow(1024.0, 2.0)
|
||||
sideBarMemoryStats := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %.1f / %.1f",
|
||||
float64(meminfo.Used())/divisor,
|
||||
float64(meminfo.Total())/divisor))
|
||||
sideBarSwapTitle := tview.NewTextView().
|
||||
SetText("Swap Use").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
sideBarSwapStats := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %d%%", meminfo.Swap()))
|
||||
sideBarFilesystemTitle := tview.NewTextView().
|
||||
SetText("Disk Use").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
var sideBarFilesystems []*tview.TextView
|
||||
for _, i := range filesystems {
|
||||
filesystemAsTextView := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %s", i))
|
||||
sideBarFilesystems = append(sideBarFilesystems, filesystemAsTextView)
|
||||
}
|
||||
sideBarIPAddressesTitle := tview.NewTextView().
|
||||
SetText("IP Addresses").
|
||||
SetTextColor(tcell.ColorYellow)
|
||||
var sideBarIPAddresses []*tview.TextView
|
||||
for _, i := range ifaces {
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
ipAsTextView := tview.NewTextView().
|
||||
SetText(fmt.Sprintf(" %v : %s", i.Name, a.String()))
|
||||
sideBarIPAddresses = append(sideBarIPAddresses, ipAsTextView)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup side bar (Right column)
|
||||
sideBar := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow)
|
||||
sideBar.SetTitle("System Info").
|
||||
SetBorder(true).
|
||||
SetTitleColor(tcell.ColorAqua)
|
||||
sideBar.AddItem(sideBarCPUTempTitle, 1, 1, false)
|
||||
sideBar.AddItem(sideBarCPUTemp, 1, 1, false)
|
||||
sideBar.AddItem(sideBarGPUTempTitle, 1, 1, false)
|
||||
sideBar.AddItem(sideBarGPUTemp, 1, 1, false)
|
||||
sideBar.AddItem(sideBarMemoryTitle, 1, 1, false)
|
||||
sideBar.AddItem(sideBarMemoryStats, 1, 1, false)
|
||||
sideBar.AddItem(sideBarSwapTitle, 1, 1, false)
|
||||
sideBar.AddItem(sideBarSwapStats, 1, 1, false)
|
||||
sideBar.AddItem(sideBarFilesystemTitle, 1, 1, false)
|
||||
for _, filesystemAsTextView := range sideBarFilesystems {
|
||||
sideBar.AddItem(filesystemAsTextView, 1, 1, false)
|
||||
}
|
||||
sideBar.AddItem(sideBarIPAddressesTitle, 1, 1, false)
|
||||
for _, ipAsTextView := range sideBarIPAddresses {
|
||||
sideBar.AddItem(ipAsTextView, 1, 1, false)
|
||||
}
|
||||
|
||||
// Pages
|
||||
pages := tview.NewPages()
|
||||
|
||||
// Main UI
|
||||
mainUI := tview.NewGrid().
|
||||
SetRows(2, 0, 4).
|
||||
SetColumns(25, 50, 50).
|
||||
SetBorders(true).
|
||||
AddItem(header, 0, 0, 1, 3, 0, 0, false).
|
||||
AddItem(footer, 2, 0, 1, 3, 0, 0, false)
|
||||
|
||||
mainUI.AddItem(menu, 1, 0, 1, 1, 0, 100, true).
|
||||
AddItem(main, 1, 1, 1, 1, 0, 100, false).
|
||||
AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)
|
||||
|
||||
pages.AddPage(PAGE_MAIN_UI, mainUI, true, true)
|
||||
|
||||
// Button modals
|
||||
saveExitModal := tview.NewModal().
|
||||
SetText("Are you sure you want to [red]SAVE [red]& [red]EXIT?").
|
||||
AddButtons([]string{"Yes", "Cancel"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Yes" {
|
||||
// Deal with selected albums
|
||||
for _, checkbox := range albumCheckboxes {
|
||||
checkboxLabel := checkbox.GetLabel()
|
||||
// Undo beautification of checkbox label
|
||||
if checkboxLabel == "Main Folder" {
|
||||
checkboxLabel = "/"
|
||||
}
|
||||
// Track whether or not we need to add OR remove from config
|
||||
selected := checkbox.IsChecked()
|
||||
inConfig := false
|
||||
configIndex := 0
|
||||
|
||||
// Walk the existing config values and bail if/when we find the album
|
||||
for configIndex = range configSelectedAlbums {
|
||||
if checkboxLabel == configSelectedAlbums[configIndex] {
|
||||
inConfig = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// We need to add to the config
|
||||
if selected && !inConfig {
|
||||
configSelectedAlbums = append(configSelectedAlbums, checkboxLabel)
|
||||
}
|
||||
// We need to remove from the config
|
||||
if !selected && inConfig {
|
||||
configSelectedAlbums = remove(configSelectedAlbums, configIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply HDMI configuration (on/off)
|
||||
var hdmiForDisk bytes.Buffer
|
||||
valueForTemplate := utils.SystemdTimer{OnCalendar: configHDMIOn}
|
||||
screenOn, err := template.New("screenon.systemd.timer").Parse(utils.SCREEN_ON_DOT_TIMER)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up screen on systemd timer : %s", err)
|
||||
}
|
||||
err = screenOn.Execute(&hdmiForDisk, valueForTemplate)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up screen on systemd timer : %s", err)
|
||||
}
|
||||
utils.WriteFile(utils.SCREEN_ON_TIMER_PATH, utils.SCREEN_ON_TIMER_PATH+".tmp", hdmiForDisk.Bytes())
|
||||
|
||||
valueForTemplate.OnCalendar = configHDMIOff
|
||||
hdmiForDisk.Truncate(0)
|
||||
screenOff, err := template.New("screenoff.systemd.timer").Parse(utils.SCREEN_OFF_DOT_TIMER)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up screen off systemd timer : %s", err)
|
||||
}
|
||||
err = screenOff.Execute(&hdmiForDisk, valueForTemplate)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up screen off systemd timer : %s", err)
|
||||
}
|
||||
utils.WriteFile(utils.SCREEN_OFF_TIMER_PATH, utils.SCREEN_OFF_TIMER_PATH+".tmp", hdmiForDisk.Bytes())
|
||||
|
||||
// Reload systemd units after applying HDMI config
|
||||
utils.SystemdDaemonReload()
|
||||
|
||||
// Apply configuration updates to main config manager prior to saving
|
||||
pfconfig.Load(confmap.Provider(map[string]interface{}{
|
||||
config.CONFIG_KEY_SLIDESHOW_INTERVAL: configSlideInterval,
|
||||
config.CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL: configRestartInterval,
|
||||
config.CONFIG_KEY_HDMI_ON: configHDMIOn,
|
||||
config.CONFIG_KEY_HDMI_OFF: configHDMIOff,
|
||||
config.CONFIG_KEY_ALBUMS_SELECTED: configSelectedAlbums,
|
||||
}, "."), nil)
|
||||
|
||||
// Save configuration
|
||||
parser := toml.Parser()
|
||||
configForDisk, err := pfconfig.Marshal(parser)
|
||||
if err != nil {
|
||||
log.Fatalf("Error preparing config for disk write : %s", err)
|
||||
}
|
||||
utils.WriteFile(config.CONFIG_FILE_PATH, config.CONFIG_FILE_PATH_TMP, configForDisk)
|
||||
app.Stop()
|
||||
os.Exit(0)
|
||||
}
|
||||
pages.SwitchToPage(PAGE_MAIN_UI)
|
||||
})
|
||||
pages.AddPage(PAGE_SAVE_EXIT, saveExitModal, true, false)
|
||||
saveExitButton.SetSelectedFunc(func() {
|
||||
pages.ShowPage(PAGE_SAVE_EXIT)
|
||||
})
|
||||
|
||||
exitModal := tview.NewModal().
|
||||
SetText("Are you sure you want to [red]EXIT?").
|
||||
AddButtons([]string{"Yes", "Cancel"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Yes" {
|
||||
app.Stop()
|
||||
}
|
||||
pages.SwitchToPage(PAGE_MAIN_UI)
|
||||
})
|
||||
pages.AddPage(PAGE_EXIT, exitModal, true, false)
|
||||
exitButton.SetSelectedFunc(func() {
|
||||
pages.ShowPage(PAGE_EXIT)
|
||||
})
|
||||
|
||||
rebootModal := tview.NewModal().
|
||||
SetText("Are you sure you want to [red]REBOOT?").
|
||||
AddButtons([]string{"Yes", "Cancel"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Yes" {
|
||||
err := exec.Command(CMD_SYSTEMCTL, "reboot").Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not reboot : %s ", err)
|
||||
}
|
||||
}
|
||||
pages.SwitchToPage(PAGE_MAIN_UI)
|
||||
})
|
||||
pages.AddPage(PAGE_REBOOT, rebootModal, true, false)
|
||||
rebootButton.SetSelectedFunc(func() {
|
||||
pages.ShowPage(PAGE_REBOOT)
|
||||
})
|
||||
|
||||
powerOffModal := tview.NewModal().
|
||||
SetText("Are you sure you want to [red]POWER [red]OFF?").
|
||||
AddButtons([]string{"Yes", "Cancel"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Yes" {
|
||||
err := exec.Command(CMD_SYSTEMCTL, "poweroff").Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not power off : %s ", err)
|
||||
}
|
||||
}
|
||||
pages.SwitchToPage(PAGE_MAIN_UI)
|
||||
})
|
||||
pages.AddPage(PAGE_POWEROFF, powerOffModal, true, false)
|
||||
powerOffButton.SetSelectedFunc(func() {
|
||||
pages.ShowPage(PAGE_POWEROFF)
|
||||
})
|
||||
|
||||
// Setup tracking of which are of the UI can/has focus
|
||||
primitivesThatCanFocus := []tview.Primitive{menu, saveExitButton, exitButton, rebootButton, powerOffButton}
|
||||
currentFocus := 0
|
||||
|
||||
// Setup basic switching between main menu and buttons for the UI
|
||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
// Don't process input if we aren't on an existing element that can focus
|
||||
canContinue := false
|
||||
focusedPrimitive := app.GetFocus()
|
||||
|
||||
// Override some of the default behavior so up/dn move between fields in forms
|
||||
// Per API GetFocusedItemIndex on a form will be -1 if a form item isn't currently focused
|
||||
// We use this as a bit of a cheat to figure out if we're inside of a form that needs better nav options for users (ie. tab doesn't exist on a remote)
|
||||
albumField, albumButton := selectAlbumsForm.GetFocusedItemIndex()
|
||||
intervalField, intervalButton := intervalsForm.GetFocusedItemIndex()
|
||||
wifiField, wifiButton := wifiConfigForm.GetFocusedItemIndex()
|
||||
hdmiField, hdmiButton := hdmiForm.GetFocusedItemIndex()
|
||||
advancedField, advancedButton := advancedForm.GetFocusedItemIndex()
|
||||
if wifiField != -1 || wifiButton != -1 ||
|
||||
albumField != -1 || albumButton != -1 ||
|
||||
intervalField != -1 || intervalButton != -1 ||
|
||||
hdmiField != -1 || hdmiButton != -1 ||
|
||||
advancedField != -1 || advancedButton != -1 {
|
||||
switch event.Key() {
|
||||
case tcell.KeyUp:
|
||||
return tcell.NewEventKey(tcell.KeyBacktab, 0, event.Modifiers())
|
||||
case tcell.KeyDown:
|
||||
return tcell.NewEventKey(tcell.KeyTab, 0, event.Modifiers())
|
||||
}
|
||||
}
|
||||
|
||||
// Standard override for the screen primitives we are using (ie. don't take over control in modals)
|
||||
for _, primitive := range primitivesThatCanFocus {
|
||||
if primitive == focusedPrimitive {
|
||||
canContinue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Bail if we shouldn't be affecting user input
|
||||
if !canContinue {
|
||||
return event
|
||||
}
|
||||
|
||||
// Add various forms of nav that make sense for a TUI (common stuff folks are used to)
|
||||
currentFocusChanged := false
|
||||
switch event.Key() {
|
||||
case tcell.KeyRight, tcell.KeyTab:
|
||||
currentFocus += 1
|
||||
if currentFocus >= len(primitivesThatCanFocus) {
|
||||
currentFocus = 0
|
||||
}
|
||||
currentFocusChanged = true
|
||||
case tcell.KeyLeft, tcell.KeyBacktab:
|
||||
currentFocus -= 1
|
||||
if currentFocus < 0 {
|
||||
currentFocus = len(primitivesThatCanFocus) - 1
|
||||
}
|
||||
currentFocusChanged = true
|
||||
}
|
||||
|
||||
// Update the focus based on navigation
|
||||
if currentFocusChanged {
|
||||
app.SetFocus(primitivesThatCanFocus[currentFocus])
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pass through event so the main UI can process stuff properly
|
||||
return event
|
||||
})
|
||||
|
||||
// Show UI and panic if there are any errors
|
||||
if err := app.SetRoot(pages, true).SetFocus(primitivesThatCanFocus[currentFocus]).EnableMouse(false).Run(); err != nil {
|
||||
log.Fatalf("Failed to run UI : %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/knadh/koanf"
|
||||
|
||||
"git.kemonine.info/PiFrame/config"
|
||||
)
|
||||
|
||||
const (
|
||||
PATH_TEMP_FOR_SLIDESHOW = "/run/piframe/fim"
|
||||
)
|
||||
|
||||
// fim placeholders so we can reset them as needed
|
||||
var fim *exec.Cmd = nil
|
||||
var stdin io.WriteCloser = nil
|
||||
|
||||
func setupFim(PATH_TEMP_FOR_SLIDESHOW string) {
|
||||
// Prep slideshow command and arguments
|
||||
// NOTE: The random flag is seeded with time() ; this is bad as we will be restarting the slideshow at about the same time per the configurd schedule
|
||||
// We use the non-seeded form to ensure that it's a little more random (or at least hope it's a little more random)
|
||||
CMD_FIM := "/usr/bin/fim"
|
||||
ARGS_FIM := []string{"--no-commandline", "--no-history", "--etc-fimrc", "/usr/local/etc/fimrc",
|
||||
"--device", "/dev/fb0", "--vt", "1",
|
||||
"--execute-commands-early", "\"clear\"", "--final-commands", "\"clear\"",
|
||||
"--autozoom", "--random", "--recursive", "--cd-and-readdir",
|
||||
PATH_TEMP_FOR_SLIDESHOW}
|
||||
|
||||
// fim command that'll be executed
|
||||
fim = exec.Command(CMD_FIM, ARGS_FIM...)
|
||||
// Put fim into a process group so ALL processes that may be executed are exited when main process exits
|
||||
fim.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
// Setup stdin for fim to control slideshow
|
||||
stdinLocal, err := fim.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting fim stdin : %s", err)
|
||||
}
|
||||
stdin = stdinLocal
|
||||
}
|
||||
|
||||
func Slideshow(pfconfig *koanf.Koanf) {
|
||||
// Prep folder setup needed for fim
|
||||
// fim does NOT allow multiple folders passed on the CLI (as far as KemoNine can tell)
|
||||
// We build a temp folder stup in /run/piframe/fim that has symlinks to selected albums
|
||||
// After we build up this directory setup we'll kick off fim against the temp dir with recursive
|
||||
// /run is a tmpfs so this won't wear on the sd card storage
|
||||
|
||||
// Create temp folder
|
||||
_, err := os.Stat(PATH_TEMP_FOR_SLIDESHOW)
|
||||
if os.IsNotExist(err) {
|
||||
errDir := os.MkdirAll(PATH_TEMP_FOR_SLIDESHOW, 0755)
|
||||
if errDir != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup temp folder if it already existed
|
||||
dirRead, err := os.Open(PATH_TEMP_FOR_SLIDESHOW)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
dirFiles, err := dirRead.Readdir(0)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
// Loop over the directory's files.
|
||||
for index := range dirFiles {
|
||||
fileHere := dirFiles[index]
|
||||
|
||||
// Get name of file and its full path.
|
||||
nameHere := fileHere.Name()
|
||||
fullPath := PATH_TEMP_FOR_SLIDESHOW + "/" + nameHere
|
||||
|
||||
// Remove the file.
|
||||
err = os.Remove(fullPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup symlinks to selected albums to be used with slideshow
|
||||
// Add albums full paths to command line args for fim
|
||||
albumRootPath := pfconfig.String(config.CONFIG_KEY_ALBUMS_ROOT)
|
||||
for _, album := range pfconfig.Strings(config.CONFIG_KEY_ALBUMS_SELECTED) {
|
||||
source := albumRootPath + album
|
||||
destination := PATH_TEMP_FOR_SLIDESHOW + album
|
||||
if album == "/" {
|
||||
files, err := ioutil.ReadDir(albumRootPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filePath := albumRootPath + "/" + file.Name()
|
||||
fileStat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
mode := fileStat.Mode()
|
||||
if mode.IsRegular() {
|
||||
err = os.Symlink(filePath, PATH_TEMP_FOR_SLIDESHOW+"/"+file.Name())
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = os.Symlink(source, destination)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up slideshow : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup fim for run
|
||||
// Commands can't be re-used so we move this to a function to support the restart interval cleanly
|
||||
setupFim(PATH_TEMP_FOR_SLIDESHOW)
|
||||
|
||||
// Advance slideshow every interval as defined in const()
|
||||
slideshowAdvanceDurationString := pfconfig.String(config.CONFIG_KEY_SLIDESHOW_INTERVAL)
|
||||
slideshowAdvanceDuration, err := time.ParseDuration(slideshowAdvanceDurationString)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing slide duration : %s", err)
|
||||
}
|
||||
ticker := time.NewTicker(slideshowAdvanceDuration)
|
||||
stop_ticker := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err = io.WriteString(stdin, "n")
|
||||
if err != nil {
|
||||
log.Fatalf("Error advancing slides : %s", err)
|
||||
}
|
||||
case <-stop_ticker:
|
||||
ticker.Stop()
|
||||
stdin.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start watching for key strokes and echo them back to stdout
|
||||
keysEvents, err := keyboard.GetKeys(10)
|
||||
if err != nil {
|
||||
log.Fatalf("Error setting up keyboard listener : %s", err)
|
||||
}
|
||||
// NOT deferring keyboard.close as we do that by hand further down when fim exits ahead of showing the config UI
|
||||
|
||||
// Keep running fim until we're signaled by the user to do otherwise
|
||||
// Common reasons that we MUST do this kind of goofy (bugs / behavior)
|
||||
// - fim doesn't quite see the /run changes where the albums are linked and bails out
|
||||
// - OOM kicks in and kills fim (it's kinda RAM heavy sometimes)
|
||||
// - Corrupt or invalid image causes fim to crash
|
||||
// - The timeout for re-randomizing kicked free and fim needs to restart
|
||||
// - fim thinks the slideshow has 'ended' for whatever reason and we really want to always show no matter what
|
||||
STOP_SLIDESHOW := false
|
||||
|
||||
// Goroutine for tracking which keys are pressed and controlling fim if appropriate
|
||||
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||
go func(keyboardCtx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-keyboardCtx.Done():
|
||||
return
|
||||
case event := <-keysEvents:
|
||||
if event.Err != nil {
|
||||
log.Fatalf("Error listening to key events : %s", err)
|
||||
}
|
||||
|
||||
// Keys for fim event management (previous/next in particular)
|
||||
fimKey := ""
|
||||
if event.Key == keyboard.KeyArrowLeft || event.Key == keyboard.KeyArrowDown {
|
||||
fimKey = "p"
|
||||
}
|
||||
if event.Key == keyboard.KeyArrowRight || event.Key == keyboard.KeyArrowUp {
|
||||
fimKey = "n"
|
||||
}
|
||||
|
||||
// Exit fim and move to the config UI
|
||||
if event.Key == keyboard.KeyEsc || event.Key == keyboard.KeyEnter || event.Key == keyboard.KeySpace {
|
||||
// We are being told to stop the slideshow, oblidge the user
|
||||
STOP_SLIDESHOW = true
|
||||
if fim != nil { // Just in case someone lays on exit key or similar during startup
|
||||
pgid, err := syscall.Getpgid(fim.Process.Pid)
|
||||
if err == nil {
|
||||
if err := syscall.Kill(-pgid, 9); err != nil {
|
||||
log.Fatalf("failed to kill fim : %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Control fim if we received a valid key for next/previous slide
|
||||
if fimKey != "" {
|
||||
_, err = io.WriteString(stdin, fimKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Error controlling fim : %s", err)
|
||||
}
|
||||
ticker.Reset(pfconfig.Duration(config.CONFIG_KEY_SLIDESHOW_INTERVAL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}(keyboardCtx)
|
||||
|
||||
// Restart fim after configured timeout ; This is setup as a ticker due to KemoNine not getting CommandWithContext stuff to work properly (lots of pointer related crashes and the like)
|
||||
fimRestartDurationString := pfconfig.String(config.CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL)
|
||||
fimRestartDuration, err := time.ParseDuration(fimRestartDurationString)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing restart duration : %S", err)
|
||||
}
|
||||
fimTicker := time.NewTicker(fimRestartDuration)
|
||||
stop_fim_ticker := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-fimTicker.C:
|
||||
if fim != nil { // Just in case someone lays on exit key or similar during startup
|
||||
pgid, err := syscall.Getpgid(fim.Process.Pid)
|
||||
if err == nil {
|
||||
if err := syscall.Kill(-pgid, 9); err != nil {
|
||||
log.Fatalf("failed to kill fim : %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-stop_fim_ticker:
|
||||
fimTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run fim
|
||||
for !STOP_SLIDESHOW {
|
||||
if err := fim.Run(); err != nil {
|
||||
// Unwrap the error a bit so we can find out if a signal killed fim or something else
|
||||
// An exit code of -1 means the program didn't exit in time or was terminated by a signal (per the docs)
|
||||
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != -1 {
|
||||
// We are NOT going to fatal here as there are many 'valid' reasons fim would exit for no good reason ;)
|
||||
log.Printf("Error running fim : %s", err)
|
||||
}
|
||||
}
|
||||
setupFim(PATH_TEMP_FOR_SLIDESHOW)
|
||||
}
|
||||
|
||||
// Stop fim slideshow advancing go routine
|
||||
close(stop_ticker)
|
||||
// Stop restart go routine
|
||||
close(stop_fim_ticker)
|
||||
|
||||
// Stop listening to keyboard events
|
||||
keyboard.Close()
|
||||
keyboardCancel()
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Utils
|
||||
|
||||
Misc utilities used elsewhere by PiFrame go code.
|
|
@ -0,0 +1,16 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func WriteFile(destinationPath string, tempPath string, dataForDisk []byte) {
|
||||
if err := ioutil.WriteFile(tempPath, dataForDisk, 0644); err != nil {
|
||||
log.Fatalf("Error writing temp file : %s", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, destinationPath); err != nil {
|
||||
log.Fatalf("Error moving new in place : %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package utils
|
||||
|
||||
type SystemdTimer struct {
|
||||
OnCalendar string
|
||||
}
|
||||
|
||||
const SCREEN_ON_DOT_TIMER = `
|
||||
[Unit]
|
||||
Description=Turn on display
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{.OnCalendar}}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
||||
const SCREEN_OFF_DOT_TIMER = `
|
||||
[Unit]
|
||||
Description=Turn off display
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{.OnCalendar}}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
|
@ -0,0 +1,23 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_SYSTEMCTL = "/usr/bin/systemctl"
|
||||
)
|
||||
|
||||
const (
|
||||
SCREEN_ON_TIMER_PATH = "/etc/systemd/system/screen-on.timer"
|
||||
SCREEN_OFF_TIMER_PATH = "/etc/systemd/system/screen-off.timer"
|
||||
)
|
||||
|
||||
func SystemdDaemonReload() {
|
||||
// Reload systemd units
|
||||
err := exec.Command(CMD_SYSTEMCTL, "daemon-reload").Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Error running %s : %s", CMD_SYSTEMCTL, err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
CMD_VCGENCMD = "/opt/vc/bin/vcgencmd"
|
||||
FILE_CPU_TEMP = "/sys/class/thermal/thermal_zone0/temp"
|
||||
)
|
||||
|
||||
func GetCPUTemp() float64 {
|
||||
cpuTempFileContents, err := ioutil.ReadFile(FILE_CPU_TEMP)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting CPU temp : %s", err)
|
||||
}
|
||||
cpuTempStr := strings.Trim(string(cpuTempFileContents), "\n")
|
||||
cpuTempInt, err := strconv.Atoi(cpuTempStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Error processing CPU temp : %S", err)
|
||||
}
|
||||
return float64(cpuTempInt) / 1000.0
|
||||
}
|
||||
|
||||
func GetGPUTemp() float64 {
|
||||
vcgencmdOut, err := exec.Command(CMD_VCGENCMD, "measure_temp").Output()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting GPU temp : %s", err)
|
||||
}
|
||||
gpuTempString := strings.Split(strings.Trim(string(vcgencmdOut), "\n"), "=")[1]
|
||||
gpuTempString = strings.Trim(gpuTempString, "'C")
|
||||
gpuTemp, err := strconv.ParseFloat(gpuTempString, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing GPU Temp : %s", err)
|
||||
}
|
||||
return gpuTemp
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
Software originally developed by sungo (https://sungo.io)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,58 @@
|
|||
package fan
|
||||
|
||||
// Code originally developed by sungo (https://sungo.io)
|
||||
// Distributed under the terms of the 0BSD license https://opensource.org/licenses/0BSD
|
||||
|
||||
import (
|
||||
"github.com/d2r2/go-i2c"
|
||||
"github.com/d2r2/go-logger"
|
||||
)
|
||||
|
||||
type Fan struct {
|
||||
conn *i2c.I2C
|
||||
}
|
||||
|
||||
func init() {
|
||||
logger.ChangePackageLogLevel("i2c", logger.PanicLevel)
|
||||
}
|
||||
|
||||
// From the argon python script:
|
||||
// On an rpi4, the bus should be 1
|
||||
// On any other rpi, the bus should 0
|
||||
// On all platforms, the address should be 0x1a
|
||||
func New(address uint8, bus int) (*Fan, error) {
|
||||
f := &Fan{}
|
||||
|
||||
i, err := i2c.NewI2C(address, bus)
|
||||
f.conn = i
|
||||
|
||||
return f, err
|
||||
}
|
||||
|
||||
func (f *Fan) Bus() int {
|
||||
return f.conn.GetBus()
|
||||
}
|
||||
|
||||
func (f *Fan) Addr() uint8 {
|
||||
return f.conn.GetAddr()
|
||||
}
|
||||
|
||||
func (f *Fan) SafeClose() error {
|
||||
f.SetSpeed(50)
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (f *Fan) Close() error {
|
||||
return f.conn.Close()
|
||||
}
|
||||
|
||||
func (f *Fan) SetSpeed(percent int) error {
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
} else if percent < 0 {
|
||||
percent = 0
|
||||
}
|
||||
|
||||
_, err := f.conn.WriteBytes([]byte(string(percent)))
|
||||
return err
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
# - "1.10"
|
||||
- "tip"
|
||||
|
||||
# first part of the GOARCH workaround
|
||||
# setting the GOARCH directly doesn't work, since the value will be overwritten later
|
||||
# so set it to a temporary environment variable first
|
||||
env:
|
||||
global:
|
||||
TRAVIS_CGO_ENABLED=1
|
||||
TRAVIS_GOOS=linux
|
||||
matrix:
|
||||
- TRAVIS_GOARCH=amd64
|
||||
- TRAVIS_GOARCH=arm TRAVIS_CC=arm-linux-gnueabi-gcc TRAVIS_GOARM=6
|
||||
|
||||
# second part of the GOARCH workaround
|
||||
# now actually set the GOARCH env variable to the value of the temporary variable set earlier
|
||||
before_install:
|
||||
- sudo apt-get install gcc-arm-linux-gnueabi # for CGO cross compile to ARM
|
||||
- export CGO_ENABLED=$TRAVIS_CGO_ENABLED GOARCH=$TRAVIS_GOARCH GOARM=$TRAVIS_GOARM GOOS=$TRAVIS_GOOS CC=$TRAVIS_CC
|
||||
- go env # for debugging
|
||||
- go tool dist env # for debugging
|
||||
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
|
||||
BH1750 ambient light sensor
|
||||
=====================
|
||||
|
||||
[![Build Status](https://travis-ci.org/d2r2/go-bh1750.svg?branch=master)](https://travis-ci.org/d2r2/go-bh1750)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/d2r2/go-bh1750)](https://goreportcard.com/report/github.com/d2r2/go-bh1750)
|
||||
[![GoDoc](https://godoc.org/github.com/d2r2/go-bh1750?status.svg)](https://godoc.org/github.com/d2r2/go-bh1750)
|
||||
[![MIT License](http://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
|
||||
|
||||
BH1750 ([general specification](https://raw.github.com/d2r2/go-bh1750/master/docs/bh1750fvi-e-186247.pdf)) is a power effective ambient light sensor with spectral response close to human eye. Sensor returns measured ambient light value in lux units. Easily integrated with Arduino and Raspberry PI via i2c communication interface:
|
||||
![image](https://raw.github.com/d2r2/go-bh1750/master/docs/bh1750.jpg)
|
||||
|
||||
Here is a library written in [Go programming language](https://golang.org/) for Raspberry PI and counterparts, which gives you in the output ambient light value (making all necessary i2c-bus interacting and values computing).
|
||||
|
||||
Golang usage
|
||||
------------
|
||||
|
||||
|
||||
```go
|
||||
func main() {
|
||||
// Create new connection to i2c-bus on 0 line with address 0x23.
|
||||
// Use i2cdetect utility to find device address over the i2c-bus
|
||||
i2c, err := i2c.NewI2C(0x23, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer i2c.Close()
|
||||
|
||||
sensor := bh1750.NewBH1750()
|
||||
|
||||
resolution := bh1750.HighResolution
|
||||
amb, err := sensor.MeasureAmbientLight(i2c, resolution)
|
||||
if err != nil {
|
||||
lg.Fatal(err)
|
||||
}
|
||||
log.Printf("Ambient light (%s) = %v lx", resolution, amb)
|
||||
```
|
||||
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
GoDoc [documentation](http://godoc.org/github.com/d2r2/go-bh1750)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/d2r2/go-bh1750
|
||||
```
|
||||
|
||||
Troubleshooting
|
||||
--------------
|
||||
|
||||
- *How to obtain fresh Golang installation to RPi device (either any RPi clone):*
|
||||
If your RaspberryPI golang installation taken by default from repository is outdated, you may consider
|
||||
to install actual golang manually from official Golang [site](https://golang.org/dl/). Download
|
||||
tar.gz file containing armv6l in the name. Follow installation instructions.
|
||||
|
||||
- *How to enable I2C bus on RPi device:*
|
||||
If you employ RaspberryPI, use raspi-config utility to activate i2c-bus on the OS level.
|
||||
Go to "Interfacing Options" menu, to active I2C bus.
|
||||
Probably you will need to reboot to load i2c kernel module.
|
||||
Finally you should have device like /dev/i2c-1 present in the system.
|
||||
|
||||
- *How to find I2C bus allocation and device address:*
|
||||
Use i2cdetect utility in format "i2cdetect -y X", where X may vary from 0 to 5 or more,
|
||||
to discover address occupied by peripheral device. To install utility you should run
|
||||
`apt install i2c-tools` on debian-kind system. `i2cdetect -y 1` sample output:
|
||||
```
|
||||
0 1 2 3 4 5 6 7 8 9 a b c d e f
|
||||
00: -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
70: -- -- -- -- -- -- 76 --
|
||||
```
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
Please use [Github issue tracker](https://github.com/d2r2/go-bh1750/issues) for filing bugs or feature requests.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Go-bh1750 is licensed under MIT License.
|
|
@ -1,323 +0,0 @@
|
|||
//--------------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright (c) 2018 Denis Dyakov
|
||||
//
|
||||
// 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 bh1750
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
i2c "github.com/d2r2/go-i2c"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Command bytes
|
||||
const (
|
||||
// No active state.
|
||||
CMD_POWER_DOWN = 0x00
|
||||
|
||||
// Waiting for measurement command.
|
||||
CMD_POWER_ON = 0x01
|
||||
|
||||
// Reset Data register value.
|
||||
// Reset command is not acceptable in Power Down mode.
|
||||
CMD_RESET = 0x07
|
||||
|
||||
// Start measurement at 1lx resolution.
|
||||
// Measurement Time is typically 120ms.
|
||||
CMD_CONTINUOUSLY_H_RES_MODE = 0x10
|
||||
|
||||
// Start measurement at 0.5lx resolution.
|
||||
// Measurement Time is typically 120ms.
|
||||
CMD_CONTINUOUSLY_H_RES_MODE2 = 0x11
|
||||
|
||||
// Start measurement at 4lx resolution.
|
||||
// Measurement Time is typically 16ms.
|
||||
CMD_CONTINUOUSLY_L_RES_MODE = 0x13
|
||||
|
||||
// Start measurement at 1lx resolution.
|
||||
// Measurement Time is typically 120ms.
|
||||
// It is automatically set to Power Down mode after measurement
|
||||
CMD_ONE_TIME_H_RES_MODE = 0x20
|
||||
|
||||
// Start measurement at 0.5lx resolution.
|
||||
// Measurement Time is typically 120ms.
|
||||
// It is automatically set to Power Down mode after measurement.
|
||||
CMD_ONE_TIME_H_RES_MODE2 = 0x21
|
||||
|
||||
// Start measurement at 4lx resolution.
|
||||
// Measurement Time is typically 16ms.
|
||||
// It is automatically set to Power Down mode after measurement.
|
||||
CMD_ONE_TIME_L_RES_MODE = 0x23
|
||||
|
||||
// Change measurement time. 01000_MT[7,6,5]
|
||||
CMD_CHANGE_MEAS_TIME_HIGH = 0x40
|
||||
|
||||
// Change measurement time. 011_MT[4,3,2,1,0]
|
||||
CMD_CHANGE_MEAS_TIME_LOW = 0x60
|
||||
)
|
||||
|
||||
// ResolutionMode define sensor sensitivity
|
||||
// and measure time. Be aware, that improving
|
||||
// sensitivity lead to increasing of measurement time.
|
||||
type ResolutionMode int
|
||||
|
||||
const (
|
||||
// LowResolution precision 4 lx, 16 ms measurement time
|
||||
LowResolution ResolutionMode = iota + 1
|
||||
// HighResolution precision 1 lx, 120 ms measurement time
|
||||
HighResolution
|
||||
// HighestResolution precision 0.5 lx, 120 ms measurement time
|
||||
HighestResolution
|
||||
)
|
||||
|
||||
// String define stringer interface.
|
||||
func (v ResolutionMode) String() string {
|
||||
switch v {
|
||||
case LowResolution:
|
||||
return "Low Resolution"
|
||||
case HighResolution:
|
||||
return "High Resolution"
|
||||
case HighestResolution:
|
||||
return "Highest Resolution"
|
||||
default:
|
||||
return "<unknown>"
|
||||
}
|
||||
}
|
||||
|
||||
// BH1750 it's a sensor itself.
|
||||
type BH1750 struct {
|
||||
// Since sensor have no register
|
||||
// to report current state, we save
|
||||
// last issued command to fill this gap.
|
||||
lastCmd byte
|
||||
lastResolution ResolutionMode
|
||||
factor byte
|
||||
}
|
||||
|
||||
// NewBH1750 return new sensor instance.
|
||||
func NewBH1750() *BH1750 {
|
||||
v := &BH1750{}
|
||||
v.factor = v.GetDefaultSensivityFactor()
|
||||
return v
|
||||
}
|
||||
|
||||
// Reset clear ambient light register value.
|
||||
func (v *BH1750) Reset(i2c *i2c.I2C) error {
|
||||
lg.Debug("Reset sensor...")
|
||||
_, err := i2c.WriteBytes([]byte{CMD_RESET})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.lastCmd = CMD_RESET
|
||||
time.Sleep(time.Microsecond * 3)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PowerDown return register to idle state.
|
||||
func (v *BH1750) PowerDown(i2c *i2c.I2C) error {
|
||||
lg.Debug("Power down sensor...")
|
||||
_, err := i2c.WriteBytes([]byte{CMD_POWER_DOWN})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.lastCmd = CMD_POWER_DOWN
|
||||
return nil
|
||||
}
|
||||
|
||||
// PowerOn activate sensor.
|
||||
func (v *BH1750) PowerOn(i2c *i2c.I2C) error {
|
||||
lg.Debug("Power on sensor...")
|
||||
_, err := i2c.WriteBytes([]byte{CMD_POWER_ON})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.lastCmd = CMD_POWER_ON
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get internal parameters used to program sensor.
|
||||
func (v *BH1750) getResolutionData(resolution ResolutionMode) (cmd byte,
|
||||
wait time.Duration, divider uint32) {
|
||||
|
||||
switch resolution {
|
||||
case LowResolution:
|
||||
cmd = CMD_ONE_TIME_L_RES_MODE
|
||||
divider = 1
|
||||
// typical measure time is 16 ms,
|
||||
// but as it was found 24 ms max time
|
||||
// gives better results
|
||||
wait = time.Millisecond * 24
|
||||
case HighResolution:
|
||||
cmd = CMD_ONE_TIME_H_RES_MODE
|
||||
divider = 1
|
||||
// typical measure time
|
||||
wait = time.Millisecond * 120
|
||||
case HighestResolution:
|
||||
cmd = CMD_ONE_TIME_H_RES_MODE2
|
||||
divider = 2
|
||||
// typical measure time
|
||||
wait = time.Millisecond * 120
|
||||
}
|
||||
wait = wait * time.Duration(v.factor) /
|
||||
time.Duration(v.GetDefaultSensivityFactor())
|
||||
|
||||
return cmd, wait, divider
|
||||
}
|
||||
|
||||
// MeasureAmbientLight measure and return ambient light once in lux.
|
||||
func (v *BH1750) MeasureAmbientLight(i2c *i2c.I2C,
|
||||
resolution ResolutionMode) (uint16, error) {
|
||||
|
||||
lg.Debug("Run one time measure...")
|
||||
|
||||
cmd, wait, divider := v.getResolutionData(resolution)
|
||||
|
||||
v.lastCmd = cmd
|
||||
v.lastResolution = resolution
|
||||
|
||||
_, err := i2c.WriteBytes([]byte{cmd})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
time.Sleep(wait)
|
||||
|
||||
var data struct {
|
||||
Data [2]byte
|
||||
}
|
||||
err = readDataToStruct(i2c, 2, binary.BigEndian, &data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
amb := uint16(uint32(uint16(data.Data[0])<<8|uint16(data.Data[1])) *
|
||||
5 / 6 / divider)
|
||||
|
||||
return amb, nil
|
||||
}
|
||||
|
||||
// StartMeasureAmbientLightContinuously start continuous
|
||||
// measurement process. Use FetchMeasuredAmbientLight to get
|
||||
// average ambient light amount collected and calculated over a time.
|
||||
// Use PowerDown to stop measurements and return sensor to idle state.
|
||||
func (v *BH1750) StartMeasureAmbientLightContinuously(i2c *i2c.I2C,
|
||||
resolution ResolutionMode) (wait time.Duration, err error) {
|
||||
|
||||
lg.Debug("Start measures continuously...")
|
||||
|
||||
cmd, wait, _ := v.getResolutionData(resolution)
|
||||
|
||||
v.lastCmd = cmd
|
||||
v.lastResolution = resolution
|
||||
|
||||
_, err = i2c.WriteBytes([]byte{cmd})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Wait first time to collect necessary
|
||||
// amount of light for correct results.
|
||||
// It's not necessary to wait next time
|
||||
// same amount of time, because
|
||||
// sensor accumulate average lux amount
|
||||
// without any overwrite old value.
|
||||
time.Sleep(wait)
|
||||
|
||||
// In any case we are returning same
|
||||
// recommended amount of time to wait
|
||||
// between measures.
|
||||
return wait, nil
|
||||
}
|
||||
|
||||
// FetchMeasuredAmbientLight return current average ambient light in lux.
|
||||
// Previous command should be any continuous measurement initiation,
|
||||
// otherwise error will be reported.
|
||||
func (v *BH1750) FetchMeasuredAmbientLight(i2c *i2c.I2C) (uint16, error) {
|
||||
|
||||
lg.Debug("Fetch measured data...")
|
||||
|
||||
cmd, _, divider := v.getResolutionData(v.lastResolution)
|
||||
|
||||
if v.lastCmd != cmd {
|
||||
return 0, errors.New(
|
||||
"can't fetch measured ambient light, since last command doesn't match")
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Data [2]byte
|
||||
}
|
||||
err := readDataToStruct(i2c, 2, binary.BigEndian, &data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
amb := uint16(uint32(uint16(data.Data[0])<<8|uint16(data.Data[1])) *
|
||||
5 / 6 / divider)
|
||||
|
||||
return amb, nil
|
||||
}
|
||||
|
||||
// GetDefaultSensivityFactor return factor value
|
||||
// used when your sensor have no any protection cover.
|
||||
// This is default setting according to specification.
|
||||
func (v *BH1750) GetDefaultSensivityFactor() byte {
|
||||
return 69
|
||||
}
|
||||
|
||||
// ChangeSensivityFactor used when you close sensor
|
||||
// with protection cover, which change (ordinary decrease)
|
||||
// expected amount of light falling on the sensor.
|
||||
// In this case you should calibrate you sensor and find
|
||||
// appropriate factor to get in output correct ambient light value.
|
||||
// Be aware, that improving sensitivity will increase
|
||||
// measurement time.
|
||||
func (v *BH1750) ChangeSensivityFactor(i2c *i2c.I2C, factor byte) error {
|
||||
|
||||
lg.Debug("Change sensitivity factor...")
|
||||
|
||||
// minimum limit
|
||||
const minValue = 31
|
||||
// maximum limit
|
||||
const maxValue = 254
|
||||
|
||||
if factor < minValue || factor > maxValue {
|
||||
return errors.New(spew.Sprintf("sensitivity factor value exceed range [%d..%d]",
|
||||
minValue, maxValue))
|
||||
}
|
||||
|
||||
high := (factor & 0xE0) >> 5
|
||||
_, err := i2c.WriteBytes([]byte{CMD_CHANGE_MEAS_TIME_HIGH | high})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
low := (factor & 0x1F)
|
||||
_, err = i2c.WriteBytes([]byte{CMD_CHANGE_MEAS_TIME_LOW | low})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.factor = factor
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package bh1750
|
||||
|
||||
import logger "github.com/d2r2/go-logger"
|
||||
|
||||
// You can manage verbosity of log output
|
||||
// in the package by changing last parameter value.
|
||||
var lg = logger.NewPackageLogger("bh1750",
|
||||
logger.DebugLevel,
|
||||
// logger.InfoLevel,
|
||||
)
|
|
@ -1,106 +0,0 @@
|
|||
//--------------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright (c) 2018 Denis Dyakov
|
||||
//
|
||||
// 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 bh1750
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
i2c "github.com/d2r2/go-i2c"
|
||||
)
|
||||
|
||||
// Utility functions
|
||||
|
||||
// getS16BE extract 2-byte integer as signed big-endian.
|
||||
func getS16BE(buf []byte) int16 {
|
||||
v := int16(buf[0])<<8 + int16(buf[1])
|
||||
return v
|
||||
}
|
||||
|
||||
// getS16LE extract 2-byte integer as signed little-endian.
|
||||
func getS16LE(buf []byte) int16 {
|
||||
w := getS16BE(buf)
|
||||
// exchange bytes
|
||||
v := (w&0xFF)<<8 + w>>8
|
||||
return v
|
||||
}
|
||||
|
||||
// getU16BE extract 2-byte integer as unsigned big-endian.
|
||||
func getU16BE(buf []byte) uint16 {
|
||||
v := uint16(buf[0])<<8 + uint16(buf[1])
|
||||
return v
|
||||
}
|
||||
|
||||
// getU16LE extract 2-byte integer as unsigned little-endian.
|
||||
func getU16LE(buf []byte) uint16 {
|
||||
w := getU16BE(buf)
|
||||
// exchange bytes
|
||||
v := (w&0xFF)<<8 + w>>8
|
||||
return v
|
||||
}
|
||||
|
||||
func calcCRC1(seed byte, buf []byte) byte {
|
||||
for i := 0; i < len(buf); i++ {
|
||||
b := buf[ /*len(buf)-1-*/ i]
|
||||
for j := 0; j < 8; j++ {
|
||||
if (seed^b)&0x01 != 0 {
|
||||
seed ^= 0x18
|
||||
seed >>= 1
|
||||
seed |= 0x80
|
||||
// crc = crc ^ 0x8c
|
||||
} else {
|
||||
seed >>= 1
|
||||
}
|
||||
b >>= 1
|
||||
}
|
||||
}
|
||||
return seed
|
||||
}
|
||||
|
||||
// Round float amount to certain precision.
|
||||
func round64(value float64, precision int) float64 {
|
||||
value2 := math.Round(value*math.Pow10(precision)) /
|
||||
math.Pow10(precision)
|
||||
return value2
|
||||
}
|
||||
|
||||
// Round float amount to certain precision.
|
||||
func round32(value float32, precision int) float32 {
|
||||
return float32(round64(float64(value), precision))
|
||||
}
|
||||
|
||||
// Read byte block from i2c device to struct object.
|
||||
func readDataToStruct(i2c *i2c.I2C, byteCount int,
|
||||
byteOrder binary.ByteOrder, obj interface{}) error {
|
||||
buf1 := make([]byte, byteCount)
|
||||
_, err := i2c.ReadBytes(buf1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBuffer(buf1)
|
||||
err = binary.Read(buf, byteOrder, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# JetBrains IntelliJIdea
|
||||
*.iml
|
||||
.idea/
|
|
@ -0,0 +1,5 @@
|
|||
# Please keep this file sorted.
|
||||
|
||||
Emanuele Iannone <emanuele@fondani.it>
|
||||
Georg Reinke <guelfey@googlemail.com>
|
||||
nsf <no.smile.face@gmail.com>
|
|
@ -0,0 +1,23 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2012 termbox-go authors
|
||||
Copyright (c) 2015 Emanuele Iannone
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# Keyboard
|
||||
Simple library to listen for keystrokes from the keyboard
|
||||
|
||||
The code is inspired by [termbox-go](https://github.com/nsf/termbox-go) library.
|
||||
|
||||
### Installation
|
||||
Install and update this go package with `go get -u github.com/eiannone/keyboard`
|
||||
|
||||
### Usage
|
||||
Example of getting a single keystroke:
|
||||
|
||||
```go
|
||||
char, _, err := keyboard.GetSingleKey()
|
||||
if (err != nil) {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("You pressed: %q\r\n", char)
|
||||
```
|
||||
|
||||
Example of getting a series of keystrokes with a blocking `GetKey()` function:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/eiannone/keyboard"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := keyboard.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = keyboard.Close()
|
||||
}()
|
||||
|
||||
fmt.Println("Press ESC to quit")
|
||||
for {
|
||||
char, key, err := keyboard.GetKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("You pressed: rune %q, key %X\r\n", char, key)
|
||||
if key == keyboard.KeyEsc {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example of getting a series of keystrokes using a channel:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/eiannone/keyboard"
|
||||
)
|
||||
|
||||
func main() {
|
||||
keysEvents, err := keyboard.GetKeys(10)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = keyboard.Close()
|
||||
}()
|
||||
|
||||
fmt.Println("Press ESC to quit")
|
||||
for {
|
||||
event := <-keysEvents
|
||||
if event.Err != nil {
|
||||
panic(event.Err)
|
||||
}
|
||||
fmt.Printf("You pressed: rune %q, key %X\r\n", event.Rune, event.Key)
|
||||
if event.Key == keyboard.KeyEsc {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,207 @@
|
|||
// +build !windows
|
||||
|
||||
package keyboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type (
|
||||
input_event struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
out *os.File
|
||||
in int
|
||||
|
||||
// term specific keys
|
||||
keys []string
|
||||
|
||||
// termbox inner state
|
||||
orig_tios unix.Termios
|
||||
|
||||
sigio = make(chan os.Signal, 1)
|
||||
quitEvProd = make(chan bool)
|
||||
quitConsole = make(chan bool)
|
||||
inbuf = make([]byte, 0, 128)
|
||||
input_buf = make(chan input_event)
|
||||
)
|
||||
|
||||
func parse_escape_sequence(buf []byte) (size int, event KeyEvent) {
|
||||
bufstr := string(buf)
|
||||
for i, key := range keys {
|
||||
if strings.HasPrefix(bufstr, key) {
|
||||
event.Rune = 0
|
||||
event.Key = Key(0xFFFF - i)
|
||||
size = len(key)
|
||||
return
|
||||
}
|
||||
}
|
||||
return 0, event
|
||||
}
|
||||
|
||||
func extract_event(inbuf []byte) (int, KeyEvent) {
|
||||
if len(inbuf) == 0 {
|
||||
return 0, KeyEvent{}
|
||||
}
|
||||
|
||||
if inbuf[0] == '\033' {
|
||||
if len(inbuf) == 1 {
|
||||
return 1, KeyEvent{Key: KeyEsc}
|
||||
}
|
||||
// possible escape sequence
|
||||
if size, event := parse_escape_sequence(inbuf); size != 0 {
|
||||
return size, event
|
||||
} else {
|
||||
// it's not a recognized escape sequence, return error
|
||||
i := 1 // check for multiple sequences in the buffer
|
||||
for ; i < len(inbuf) && inbuf[i] != '\033'; i++ {
|
||||
}
|
||||
return i, KeyEvent{Key: KeyEsc, Err: errors.New("Unrecognized escape sequence")}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're here, this is not an escape sequence and not an alt sequence
|
||||
// so, it's a FUNCTIONAL KEY or a UNICODE character
|
||||
|
||||
// first of all check if it's a functional key
|
||||
if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 {
|
||||
return 1, KeyEvent{Key: Key(inbuf[0])}
|
||||
}
|
||||
|
||||
// the only possible option is utf8 rune
|
||||
if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError {
|
||||
return n, KeyEvent{Rune: r}
|
||||
}
|
||||
|
||||
return 0, KeyEvent{}
|
||||
}
|
||||
|
||||
// Wait for an event and return it. This is a blocking function call.
|
||||
func inputEventsProducer() {
|
||||
for {
|
||||
select {
|
||||
case <-quitEvProd:
|
||||
return
|
||||
case ev := <-input_buf:
|
||||
if ev.err != nil {
|
||||
select {
|
||||
case <-quitEvProd:
|
||||
return
|
||||
case inputComm <- KeyEvent{Err: ev.err}:
|
||||
}
|
||||
break
|
||||
}
|
||||
inbuf = append(inbuf, ev.data...)
|
||||
for {
|
||||
size, event := extract_event(inbuf)
|
||||
if size > 0 {
|
||||
select {
|
||||
case <-quitEvProd:
|
||||
return
|
||||
case inputComm <- event:
|
||||
}
|
||||
copy(inbuf, inbuf[size:])
|
||||
inbuf = inbuf[:len(inbuf)-size]
|
||||
}
|
||||
if size == 0 || len(inbuf) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initConsole() (err error) {
|
||||
out, err = os.OpenFile("/dev/tty", unix.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
in, err = syscall.Open("/dev/tty", unix.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = setup_term()
|
||||
if err != nil {
|
||||
return errors.New("Error while reading terminfo data:" + err.Error())
|
||||
}
|
||||
|
||||
signal.Notify(sigio, unix.SIGIO)
|
||||
|
||||
if _, err = unix.FcntlInt(uintptr(in), unix.F_SETFL, unix.O_ASYNC|unix.O_NONBLOCK); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = unix.FcntlInt(uintptr(in), unix.F_SETOWN, unix.Getpid())
|
||||
if runtime.GOOS != "darwin" && err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = unix.IoctlSetTermios(int(out.Fd()), ioctl_GETATTR, &orig_tios); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tios := orig_tios
|
||||
tios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK |
|
||||
unix.ISTRIP | unix.INLCR | unix.IGNCR |
|
||||
unix.ICRNL | unix.IXON
|
||||
tios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON |
|
||||
unix.ISIG | unix.IEXTEN
|
||||
tios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||
tios.Cflag |= unix.CS8
|
||||
tios.Cc[unix.VMIN] = 1
|
||||
tios.Cc[unix.VTIME] = 0
|
||||
|
||||
if err = unix.IoctlSetTermios(int(out.Fd()), ioctl_SETATTR, &tios); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for {
|
||||
select {
|
||||
case <-quitConsole:
|
||||
return
|
||||
case <-sigio:
|
||||
for {
|
||||
bytesRead, err := syscall.Read(in, buf)
|
||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
bytesRead = 0
|
||||
}
|
||||
data := make([]byte, bytesRead)
|
||||
copy(data, buf)
|
||||
select {
|
||||
case <-quitConsole:
|
||||
return
|
||||
case input_buf <- input_event{data, err}:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go inputEventsProducer()
|
||||
return
|
||||
}
|
||||
|
||||
func releaseConsole() {
|
||||
quitConsole <- true
|
||||
quitEvProd <- true
|
||||
unix.IoctlSetTermios(int(out.Fd()), ioctl_SETATTR, &orig_tios)
|
||||
out.Close()
|
||||
unix.Close(in)
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
package keyboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Key uint16
|
||||
|
||||
KeyEvent struct {
|
||||
Key Key // One of Key* constants, invalid if 'Ch' is not 0
|
||||
Rune rune // A unicode character
|
||||
Err error // Error in case if input failed
|
||||
}
|
||||
)
|
||||
|
||||
// Key constants, see GetKey() function.
|
||||
const (
|
||||
KeyF1 Key = 0xFFFF - iota
|
||||
KeyF2
|
||||
KeyF3
|
||||
KeyF4
|
||||
KeyF5
|
||||
KeyF6
|
||||
KeyF7
|
||||
KeyF8
|
||||
KeyF9
|
||||
KeyF10
|
||||
KeyF11
|
||||
KeyF12
|
||||
KeyInsert
|
||||
KeyDelete
|
||||
KeyHome
|
||||
KeyEnd
|
||||
KeyPgup
|
||||
KeyPgdn
|
||||
KeyArrowUp
|
||||
KeyArrowDown
|
||||
KeyArrowLeft
|
||||
KeyArrowRight
|
||||
key_min // see terminfo
|
||||
)
|
||||
|
||||
const (
|
||||
KeyCtrlTilde Key = 0x00
|
||||
KeyCtrl2 Key = 0x00
|
||||
KeyCtrlSpace Key = 0x00
|
||||
KeyCtrlA Key = 0x01
|
||||
KeyCtrlB Key = 0x02
|
||||
KeyCtrlC Key = 0x03
|
||||
KeyCtrlD Key = 0x04
|
||||
KeyCtrlE Key = 0x05
|
||||
KeyCtrlF Key = 0x06
|
||||
KeyCtrlG Key = 0x07
|
||||
KeyBackspace Key = 0x08
|
||||
KeyCtrlH Key = 0x08
|
||||
KeyTab Key = 0x09
|
||||
KeyCtrlI Key = 0x09
|
||||
KeyCtrlJ Key = 0x0A
|
||||
KeyCtrlK Key = 0x0B
|
||||
KeyCtrlL Key = 0x0C
|
||||
KeyEnter Key = 0x0D
|
||||
KeyCtrlM Key = 0x0D
|
||||
KeyCtrlN Key = 0x0E
|
||||
KeyCtrlO Key = 0x0F
|
||||
KeyCtrlP Key = 0x10
|
||||
KeyCtrlQ Key = 0x11
|
||||
KeyCtrlR Key = 0x12
|
||||
KeyCtrlS Key = 0x13
|
||||
KeyCtrlT Key = 0x14
|
||||
KeyCtrlU Key = 0x15
|
||||
KeyCtrlV Key = 0x16
|
||||
KeyCtrlW Key = 0x17
|
||||
KeyCtrlX Key = 0x18
|
||||
KeyCtrlY Key = 0x19
|
||||
KeyCtrlZ Key = 0x1A
|
||||
KeyEsc Key = 0x1B
|
||||
KeyCtrlLsqBracket Key = 0x1B
|
||||
KeyCtrl3 Key = 0x1B
|
||||
KeyCtrl4 Key = 0x1C
|
||||
KeyCtrlBackslash Key = 0x1C
|
||||
KeyCtrl5 Key = 0x1D
|
||||
KeyCtrlRsqBracket Key = 0x1D
|
||||
KeyCtrl6 Key = 0x1E
|
||||
KeyCtrl7 Key = 0x1F
|
||||
KeyCtrlSlash Key = 0x1F
|
||||
KeyCtrlUnderscore Key = 0x1F
|
||||
KeySpace Key = 0x20
|
||||
KeyBackspace2 Key = 0x7F
|
||||
KeyCtrl8 Key = 0x7F
|
||||
)
|
||||
|
||||
var (
|
||||
inputComm chan KeyEvent
|
||||
|
||||
ping = make(chan bool)
|
||||
doneClosing = make(chan bool, 1)
|
||||
busy = make(chan bool)
|
||||
waitingForKey = make(chan bool)
|
||||
)
|
||||
|
||||
func IsStarted(timeout time.Duration) bool {
|
||||
select {
|
||||
case ping <- true:
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func GetKeys(bufferSize int) (<-chan KeyEvent, error) {
|
||||
if IsStarted(time.Millisecond * 1) {
|
||||
if cap(inputComm) == bufferSize {
|
||||
return inputComm, nil
|
||||
}
|
||||
return nil, errors.New("channel already started with a different capacity")
|
||||
}
|
||||
select {
|
||||
case busy <- true:
|
||||
return nil, errors.New("cannot open keyboard because program is busy")
|
||||
default:
|
||||
}
|
||||
// Signal busy operation
|
||||
go func() {
|
||||
for <-busy {
|
||||
} // Close the routine when busy is false
|
||||
}()
|
||||
|
||||
inputComm = make(chan KeyEvent, bufferSize)
|
||||
err := initConsole()
|
||||
if err != nil {
|
||||
close(inputComm)
|
||||
busy <- false
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Signal ping subroutine started
|
||||
go func() {
|
||||
defer func() {
|
||||
releaseConsole()
|
||||
close(inputComm)
|
||||
doneClosing <- true
|
||||
}()
|
||||
for <-ping {
|
||||
} // Close the routine when ping is false
|
||||
}()
|
||||
busy <- false
|
||||
// Wait for ping subroutine to start
|
||||
ping <- true
|
||||
|
||||
return inputComm, nil
|
||||
}
|
||||
|
||||
func Open() (err error) {
|
||||
_, err = GetKeys(10)
|
||||
return
|
||||
}
|
||||
|
||||
// Should be called after successful initialization when functionality isn't required anymore.
|
||||
func Close() (err error) {
|
||||
// Checks if already closing
|
||||
select {
|
||||
case busy <- true:
|
||||
return errors.New("cannot close keyboard because program is busy")
|
||||
default:
|
||||
}
|
||||
// Checks if already closed
|
||||
if !IsStarted(time.Millisecond * 1) {
|
||||
return
|
||||
}
|
||||
|
||||
// Signal busy operation
|
||||
go func() {
|
||||
for <-busy {
|
||||
} // Close the routine when busy is false
|
||||
}()
|
||||
|
||||
// Stop responding to ping and closes initial subroutine
|
||||
ping <- false
|
||||
|
||||
// Cancel GetKey() operations
|
||||
select {
|
||||
case waitingForKey <- false:
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
// Wait for closing finished
|
||||
<-doneClosing
|
||||
|
||||
busy <- false
|
||||
return
|
||||
}
|
||||
|
||||
func GetKey() (rune, Key, error) {
|
||||
// Check if opened
|
||||
if !IsStarted(time.Millisecond * 50) {
|
||||
return 0, 0, errors.New("keyboard not opened")
|
||||
}
|
||||
// Check if already waiting for key
|
||||
select {
|
||||
case waitingForKey <- true:
|
||||
return 0, 0, errors.New("already waiting for key")
|
||||
default:
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-inputComm:
|
||||
return ev.Rune, ev.Key, ev.Err
|
||||
|
||||
case keepAlive := <-waitingForKey:
|
||||
if !keepAlive {
|
||||
return 0, 0, errors.New("operation canceled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetSingleKey() (ch rune, key Key, err error) {
|
||||
err = Open()
|
||||
if err == nil {
|
||||
ch, key, err = GetKey()
|
||||
errClosing := Close()
|
||||
if err == nil {
|
||||
err = errClosing
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
package keyboard
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
vk_backspace = 0x8
|
||||
vk_tab = 0x9
|
||||
vk_enter = 0xd
|
||||
vk_esc = 0x1b
|
||||
vk_space = 0x20
|
||||
vk_pgup = 0x21
|
||||
vk_pgdn = 0x22
|
||||
vk_end = 0x23
|
||||
vk_home = 0x24
|
||||
vk_arrow_left = 0x25
|
||||
vk_arrow_up = 0x26
|
||||
vk_arrow_right = 0x27
|
||||
vk_arrow_down = 0x28
|
||||
vk_insert = 0x2d
|
||||
vk_delete = 0x2e
|
||||
|
||||
vk_f1 = 0x70
|
||||
vk_f2 = 0x71
|
||||
vk_f3 = 0x72
|
||||
vk_f4 = 0x73
|
||||
vk_f5 = 0x74
|
||||
vk_f6 = 0x75
|
||||
vk_f7 = 0x76
|
||||
vk_f8 = 0x77
|
||||
vk_f9 = 0x78
|
||||
vk_f10 = 0x79
|
||||
vk_f11 = 0x7a
|
||||
vk_f12 = 0x7b
|
||||
|
||||
right_alt_pressed = 0x1
|
||||
left_alt_pressed = 0x2
|
||||
right_ctrl_pressed = 0x4
|
||||
left_ctrl_pressed = 0x8
|
||||
shift_pressed = 0x10
|
||||
|
||||
k32_keyEvent = 0x1
|
||||
)
|
||||
|
||||
type (
|
||||
wchar uint16
|
||||
dword uint32
|
||||
word uint16
|
||||
|
||||
k32_event struct {
|
||||
key_down int32
|
||||
repeat_count word
|
||||
virtual_key_code word
|
||||
virtual_scan_code word
|
||||
unicode_char wchar
|
||||
control_key_state dword
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
k32_WaitForMultipleObjects = kernel32.NewProc("WaitForMultipleObjects")
|
||||
k32_ReadConsoleInputW = kernel32.NewProc("ReadConsoleInputW")
|
||||
|
||||
hConsoleIn syscall.Handle
|
||||
hInterrupt windows.Handle
|
||||
|
||||
quit = make(chan bool)
|
||||
|
||||
// This is just to prevent heap allocs at all costs
|
||||
tmpArg dword
|
||||
)
|
||||
|
||||
func getError(errno syscall.Errno) error {
|
||||
if errno != 0 {
|
||||
return error(errno)
|
||||
} else {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
func getKeyEvent(r *k32_event) (KeyEvent, bool) {
|
||||
e := KeyEvent{}
|
||||
|
||||
if r.key_down == 0 {
|
||||
return e, false
|
||||
}
|
||||
|
||||
ctrlPressed := r.control_key_state&(left_ctrl_pressed|right_ctrl_pressed) != 0
|
||||
|
||||
if r.virtual_key_code >= vk_f1 && r.virtual_key_code <= vk_f12 {
|
||||
switch r.virtual_key_code {
|
||||
case vk_f1:
|
||||
e.Key = KeyF1
|
||||
case vk_f2:
|
||||
e.Key = KeyF2
|
||||
case vk_f3:
|
||||
e.Key = KeyF3
|
||||
case vk_f4:
|
||||
e.Key = KeyF4
|
||||
case vk_f5:
|
||||
e.Key = KeyF5
|
||||
case vk_f6:
|
||||
e.Key = KeyF6
|
||||
case vk_f7:
|
||||
e.Key = KeyF7
|
||||
case vk_f8:
|
||||
e.Key = KeyF8
|
||||
case vk_f9:
|
||||
e.Key = KeyF9
|
||||
case vk_f10:
|
||||
e.Key = KeyF10
|
||||
case vk_f11:
|
||||
e.Key = KeyF11
|
||||
case vk_f12:
|
||||
e.Key = KeyF12
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
return e, true
|
||||
}
|
||||
|
||||
if r.virtual_key_code <= vk_delete {
|
||||
switch r.virtual_key_code {
|
||||
case vk_insert:
|
||||
e.Key = KeyInsert
|
||||
case vk_delete:
|
||||
e.Key = KeyDelete
|
||||
case vk_home:
|
||||
e.Key = KeyHome
|
||||
case vk_end:
|
||||
e.Key = KeyEnd
|
||||
case vk_pgup:
|
||||
e.Key = KeyPgup
|
||||
case vk_pgdn:
|
||||
e.Key = KeyPgdn
|
||||
case vk_arrow_up:
|
||||
e.Key = KeyArrowUp
|
||||
case vk_arrow_down:
|
||||
e.Key = KeyArrowDown
|
||||
case vk_arrow_left:
|
||||
e.Key = KeyArrowLeft
|
||||
case vk_arrow_right:
|
||||
e.Key = KeyArrowRight
|
||||
case vk_backspace:
|
||||
if ctrlPressed {
|
||||
e.Key = KeyBackspace2
|
||||
} else {
|
||||
e.Key = KeyBackspace
|
||||
}
|
||||
case vk_tab:
|
||||
e.Key = KeyTab
|
||||
case vk_enter:
|
||||
e.Key = KeyEnter
|
||||
case vk_esc:
|
||||
e.Key = KeyEsc
|
||||
case vk_space:
|
||||
if ctrlPressed {
|
||||
// manual return here, because KeyCtrlSpace is zero
|
||||
e.Key = KeyCtrlSpace
|
||||
return e, true
|
||||
} else {
|
||||
e.Key = KeySpace
|
||||
}
|
||||
}
|
||||
|
||||
if e.Key != 0 {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
|
||||
if ctrlPressed {
|
||||
if Key(r.unicode_char) >= KeyCtrlA && Key(r.unicode_char) <= KeyCtrlRsqBracket {
|
||||
e.Key = Key(r.unicode_char)
|
||||
return e, true
|
||||
}
|
||||
switch r.virtual_key_code {
|
||||
case 192, 50:
|
||||
// manual return here, because KeyCtrl2 is zero
|
||||
e.Key = KeyCtrl2
|
||||
return e, true
|
||||
case 51:
|
||||
e.Key = KeyCtrl3
|
||||
case 52:
|
||||
e.Key = KeyCtrl4
|
||||
case 53:
|
||||
e.Key = KeyCtrl5
|
||||
case 54:
|
||||
e.Key = KeyCtrl6
|
||||
case 189, 191, 55:
|
||||
e.Key = KeyCtrl7
|
||||
case 8, 56:
|
||||
e.Key = KeyCtrl8
|
||||
}
|
||||
|
||||
if e.Key != 0 {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
|
||||
if r.unicode_char != 0 {
|
||||
e.Rune = rune(r.unicode_char)
|
||||
return e, true
|
||||
}
|
||||
|
||||
return e, false
|
||||
}
|
||||
|
||||
func produceEvent(event KeyEvent) bool {
|
||||
select {
|
||||
case <-quit:
|
||||
return false
|
||||
case inputComm <- event:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func inputEventsProducer() {
|
||||
var input [20]uint16
|
||||
for {
|
||||
// Wait for events
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects
|
||||
r0, _, e1 := syscall.Syscall6(k32_WaitForMultipleObjects.Addr(), 4,
|
||||
uintptr(2), uintptr(unsafe.Pointer(&hConsoleIn)), 0, windows.INFINITE, 0, 0)
|
||||
if uint32(r0) == windows.WAIT_FAILED && false == produceEvent(KeyEvent{Err: getError(e1)}) {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Get console input
|
||||
r0, _, e1 = syscall.Syscall6(k32_ReadConsoleInputW.Addr(), 4,
|
||||
uintptr(hConsoleIn), uintptr(unsafe.Pointer(&input[0])), 1, uintptr(unsafe.Pointer(&tmpArg)), 0, 0)
|
||||
if int(r0) == 0 {
|
||||
if false == produceEvent(KeyEvent{Err: getError(e1)}) {
|
||||
return
|
||||
}
|
||||
} else if input[0] == k32_keyEvent {
|
||||
kEvent := (*k32_event)(unsafe.Pointer(&input[2]))
|
||||
ev, ok := getKeyEvent(kEvent)
|
||||
if ok {
|
||||
for i := 0; i < int(kEvent.repeat_count); i++ {
|
||||
if false == produceEvent(ev) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initConsole() (err error) {
|
||||
// Create an interrupt event
|
||||
hInterrupt, err = windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hConsoleIn, err = syscall.Open("CONIN$", windows.O_RDWR, 0)
|
||||
if err != nil {
|
||||
windows.Close(hInterrupt)
|
||||
return
|
||||
}
|
||||
|
||||
go inputEventsProducer()
|
||||
return
|
||||
}
|
||||
|
||||
func releaseConsole() {
|
||||
// Stop events producer
|
||||
windows.SetEvent(hInterrupt)
|
||||
quit <- true
|
||||
|
||||
syscall.Close(hConsoleIn)
|
||||
windows.Close(hInterrupt)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// +build !windows,!linux
|
||||
|
||||
package keyboard
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
ioctl_GETATTR = unix.TIOCGETA
|
||||
ioctl_SETATTR = unix.TIOCSETA
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package keyboard
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
ioctl_GETATTR = unix.TCGETS
|
||||
ioctl_SETATTR = unix.TCSETS
|
||||
)
|
|
@ -0,0 +1,257 @@
|
|||
// +build !windows
|
||||
|
||||
// This file is imported from https://github.com/nsf/termbox-go
|
||||
// Last update: 2020-04-30
|
||||
|
||||
// This file contains a simple and incomplete implementation of the terminfo
|
||||
// database. Information was taken from the ncurses manpages term(5) and
|
||||
// terminfo(5). Currently, only the string capabilities for special keys and for
|
||||
// functions without parameters are actually used. Colors are still done with
|
||||
// ANSI escape sequences. Other special features that are not (yet?) supported
|
||||
// are reading from ~/.terminfo, the TERMINFO_DIRS variable, Berkeley database
|
||||
// format and extended capabilities.
|
||||
|
||||
package keyboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ti_header_length = 12
|
||||
)
|
||||
|
||||
var (
|
||||
eterm_keys = []string{
|
||||
"\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
|
||||
}
|
||||
screen_keys = []string{
|
||||
"\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC",
|
||||
}
|
||||
xterm_keys = []string{
|
||||
"\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[H", "\x1b[F", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
|
||||
}
|
||||
rxvt_keys = []string{
|
||||
"\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
|
||||
}
|
||||
linux_keys = []string{
|
||||
"\x1b[[A", "\x1b[[B", "\x1b[[C", "\x1b[[D", "\x1b[[E", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C",
|
||||
}
|
||||
|
||||
terms = []struct {
|
||||
name string
|
||||
keys []string
|
||||
}{
|
||||
{"Eterm", eterm_keys},
|
||||
{"screen", screen_keys},
|
||||
{"xterm", xterm_keys},
|
||||
{"xterm-256color", xterm_keys},
|
||||
{"rxvt-unicode", rxvt_keys},
|
||||
{"rxvt-256color", rxvt_keys},
|
||||
{"linux", linux_keys},
|
||||
}
|
||||
)
|
||||
|
||||
func load_terminfo() ([]byte, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
term := os.Getenv("TERM")
|
||||
if term == "" {
|
||||
return nil, errors.New("terminfo: TERM not set")
|
||||
}
|
||||
// Check if is a builtin terminal
|
||||
for _, t := range terms {
|
||||
if t.name == term {
|
||||
return nil, errors.New("use built in!")
|
||||
}
|
||||
}
|
||||
|
||||
// The following behaviour follows the one described in terminfo(5) as
|
||||
// distributed by ncurses.
|
||||
|
||||
terminfo := os.Getenv("TERMINFO")
|
||||
if terminfo != "" {
|
||||
// if TERMINFO is set, no other directory should be searched
|
||||
return ti_try_path(terminfo)
|
||||
}
|
||||
|
||||
// next, consider ~/.terminfo
|
||||
home := os.Getenv("HOME")
|
||||
if home != "" {
|
||||
data, err = ti_try_path(home + "/.terminfo")
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// next, TERMINFO_DIRS
|
||||
dirs := os.Getenv("TERMINFO_DIRS")
|
||||
if dirs != "" {
|
||||
for _, dir := range strings.Split(dirs, ":") {
|
||||
if dir == "" {
|
||||
// "" -> "/usr/share/terminfo"
|
||||
dir = "/usr/share/terminfo"
|
||||
}
|
||||
data, err = ti_try_path(dir)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// next, /lib/terminfo
|
||||
data, err = ti_try_path("/lib/terminfo")
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// fall back to /usr/share/terminfo
|
||||
return ti_try_path("/usr/share/terminfo")
|
||||
}
|
||||
|
||||
func ti_try_path(path string) (data []byte, err error) {
|
||||
// load_terminfo already made sure it is set
|
||||
term := os.Getenv("TERM")
|
||||
|
||||
// first try, the typical *nix path
|
||||
terminfo := path + "/" + term[0:1] + "/" + term
|
||||
data, err = ioutil.ReadFile(terminfo)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// fallback to darwin specific dirs structure
|
||||
terminfo = path + "/" + hex.EncodeToString([]byte(term[:1])) + "/" + term
|
||||
data, err = ioutil.ReadFile(terminfo)
|
||||
return
|
||||
}
|
||||
|
||||
func setup_term_builtin() error {
|
||||
name := os.Getenv("TERM")
|
||||
if name == "" {
|
||||
return errors.New("terminfo: TERM environment variable not set")
|
||||
}
|
||||
|
||||
for _, t := range terms {
|
||||
if t.name == name {
|
||||
keys = t.keys
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
compat_table := []struct {
|
||||
partial string
|
||||
keys []string
|
||||
}{
|
||||
{"xterm", xterm_keys},
|
||||
{"xterm-256color", xterm_keys},
|
||||
{"rxvt", rxvt_keys},
|
||||
{"rxvt-unicode", rxvt_keys},
|
||||
{"rxvt-256color", rxvt_keys},
|
||||
{"linux", linux_keys},
|
||||
{"Eterm", eterm_keys},
|
||||
{"screen", screen_keys},
|
||||
// let's assume that 'cygwin' is xterm compatible
|
||||
{"cygwin", xterm_keys},
|
||||
{"st", xterm_keys},
|
||||
}
|
||||
|
||||
// try compatibility variants
|
||||
for _, it := range compat_table {
|
||||
if strings.Contains(name, it.partial) {
|
||||
keys = it.keys
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("termbox: unsupported terminal")
|
||||
}
|
||||
|
||||
func setup_term() (err error) {
|
||||
var data []byte
|
||||
var header [6]int16
|
||||
var str_offset, table_offset int16
|
||||
|
||||
data, err = load_terminfo()
|
||||
if err != nil {
|
||||
return setup_term_builtin()
|
||||
}
|
||||
|
||||
rd := bytes.NewReader(data)
|
||||
// 0: magic number, 1: size of names section, 2: size of boolean section, 3:
|
||||
// size of numbers section (in integers), 4: size of the strings section (in
|
||||
// integers), 5: size of the string table
|
||||
|
||||
err = binary.Read(rd, binary.LittleEndian, header[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if header[0] != 542 && header[0] != 282 {
|
||||
return setup_term_builtin()
|
||||
}
|
||||
|
||||
number_sec_len := int16(2)
|
||||
if header[0] == 542 { // doc says it should be octal 0542, but what I see it terminfo files is 542, learn to program please... thank you..
|
||||
number_sec_len = 4
|
||||
}
|
||||
|
||||
if (header[1]+header[2])%2 != 0 {
|
||||
// old quirk to align everything on word boundaries
|
||||
header[2] += 1
|
||||
}
|
||||
str_offset = ti_header_length + header[1] + header[2] + number_sec_len*header[3]
|
||||
table_offset = str_offset + 2*header[4]
|
||||
|
||||
keys = make([]string, 0xFFFF-key_min)
|
||||
for i := range keys {
|
||||
keys[i], err = ti_read_string(rd, str_offset+2*ti_keys[i], table_offset)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ti_read_string(rd *bytes.Reader, str_off, table int16) (string, error) {
|
||||
var off int16
|
||||
|
||||
_, err := rd.Seek(int64(str_off), 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = binary.Read(rd, binary.LittleEndian, &off)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = rd.Seek(int64(table+off), 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var bs []byte
|
||||
for {
|
||||
b, err := rd.ReadByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if b == byte(0x00) {
|
||||
break
|
||||
}
|
||||
bs = append(bs, b)
|
||||
}
|
||||
return string(bs), nil
|
||||
}
|
||||
|
||||
// "Maps" special keys constants from termbox.go to the number of the respective
|
||||
// string capability in the terminfo file. Taken from (ncurses) term.h.
|
||||
var ti_keys = []int16{
|
||||
66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69, 70,
|
||||
71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61, 79, 83,
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1 @@
|
|||
go.sum linguist-generated
|
|
@ -0,0 +1,6 @@
|
|||
# Setup a Global .gitignore for OS and editor generated files:
|
||||
# https://help.github.com/articles/ignoring-files
|
||||
# git config --global core.excludesfile ~/.gitignore_global
|
||||
|
||||
.vagrant
|
||||
*.sublime-project
|
|
@ -0,0 +1,36 @@
|
|||
sudo: false
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "stable"
|
||||
- "1.11.x"
|
||||
- "1.10.x"
|
||||
- "1.9.x"
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: "stable"
|
||||
env: GOLINT=true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
|
||||
|
||||
before_install:
|
||||
- if [ ! -z "${GOLINT}" ]; then go get -u golang.org/x/lint/golint; fi
|
||||
|
||||
script:
|
||||
- go test --race ./...
|
||||
|
||||
after_script:
|
||||
- test -z "$(gofmt -s -l -w . | tee /dev/stderr)"
|
||||
- if [ ! -z "${GOLINT}" ]; then echo running golint; golint --set_exit_status ./...; else echo skipping golint; fi
|
||||
- go vet ./...
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,52 @@
|
|||
# Names should be added to this file as
|
||||
# Name or Organization <email address>
|
||||
# The email address is not required for organizations.
|
||||
|
||||
# You can update this list using the following command:
|
||||
#
|
||||
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}'
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Aaron L <aaron@bettercoder.net>
|
||||
Adrien Bustany <adrien@bustany.org>
|
||||
Amit Krishnan <amit.krishnan@oracle.com>
|
||||
Anmol Sethi <me@anmol.io>
|
||||
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
|
||||
Bruno Bigras <bigras.bruno@gmail.com>
|
||||
Caleb Spare <cespare@gmail.com>
|
||||
Case Nelson <case@teammating.com>
|
||||
Chris Howey <chris@howey.me> <howeyc@gmail.com>
|
||||
Christoffer Buchholz <christoffer.buchholz@gmail.com>
|
||||
Daniel Wagner-Hall <dawagner@gmail.com>
|
||||
Dave Cheney <dave@cheney.net>
|
||||
Evan Phoenix <evan@fallingsnow.net>
|
||||
Francisco Souza <f@souza.cc>
|
||||
Hari haran <hariharan.uno@gmail.com>
|
||||
John C Barstow
|
||||
Kelvin Fo <vmirage@gmail.com>
|
||||
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp>
|
||||
Matt Layher <mdlayher@gmail.com>
|
||||
Nathan Youngman <git@nathany.com>
|
||||
Nickolai Zeldovich <nickolai@csail.mit.edu>
|
||||
Patrick <patrick@dropbox.com>
|
||||
Paul Hammond <paul@paulhammond.org>
|
||||
Pawel Knap <pawelknap88@gmail.com>
|
||||
Pieter Droogendijk <pieter@binky.org.uk>
|
||||
Pursuit92 <JoshChase@techpursuit.net>
|
||||
Riku Voipio <riku.voipio@linaro.org>
|
||||
Rob Figueiredo <robfig@gmail.com>
|
||||
Rodrigo Chiossi <rodrigochiossi@gmail.com>
|
||||
Slawek Ligus <root@ooz.ie>
|
||||
Soge Zhang <zhssoge@gmail.com>
|
||||
Tiffany Jernigan <tiffany.jernigan@intel.com>
|
||||
Tilak Sharma <tilaks@google.com>
|
||||
Tom Payne <twpayne@gmail.com>
|
||||
Travis Cline <travis.cline@gmail.com>
|
||||
Tudor Golubenco <tudor.g@gmail.com>
|
||||
Vahe Khachikyan <vahe@live.ca>
|
||||
Yukang <moorekang@gmail.com>
|
||||
bronze1man <bronze1man@gmail.com>
|
||||
debrando <denis.brandolini@gmail.com>
|
||||
henrikedwards <henrik.edwards@gmail.com>
|
||||
铁哥 <guotie.9@gmail.com>
|
|
@ -0,0 +1,317 @@
|
|||
# Changelog
|
||||
|
||||
## v1.4.7 / 2018-01-09
|
||||
|
||||
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
|
||||
* Tests: Fix missing verb on format string (thanks @rchiossi)
|
||||
* Linux: Fix deadlock in Remove (thanks @aarondl)
|
||||
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
|
||||
* Docs: Moved FAQ into the README (thanks @vahe)
|
||||
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
|
||||
* Docs: replace references to OS X with macOS
|
||||
|
||||
## v1.4.2 / 2016-10-10
|
||||
|
||||
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
|
||||
|
||||
## v1.4.1 / 2016-10-04
|
||||
|
||||
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack)
|
||||
|
||||
## v1.4.0 / 2016-10-01
|
||||
|
||||
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie)
|
||||
|
||||
## v1.3.1 / 2016-06-28
|
||||
|
||||
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc)
|
||||
|
||||
## v1.3.0 / 2016-04-19
|
||||
|
||||
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135)
|
||||
|
||||
## v1.2.10 / 2016-03-02
|
||||
|
||||
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj)
|
||||
|
||||
## v1.2.9 / 2016-01-13
|
||||
|
||||
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep)
|
||||
|
||||
## v1.2.8 / 2015-12-17
|
||||
|
||||
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test)
|
||||
* inotify: fix race in test
|
||||
* enable race detection for continuous integration (Linux, Mac, Windows)
|
||||
|
||||
## v1.2.5 / 2015-10-17
|
||||
|
||||
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki)
|
||||
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken)
|
||||
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie)
|
||||
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion)
|
||||
|
||||
## v1.2.1 / 2015-10-14
|
||||
|
||||
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx)
|
||||
|
||||
## v1.2.0 / 2015-02-08
|
||||
|
||||
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD)
|
||||
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD)
|
||||
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59)
|
||||
|
||||
## v1.1.1 / 2015-02-05
|
||||
|
||||
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD)
|
||||
|
||||
## v1.1.0 / 2014-12-12
|
||||
|
||||
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43)
|
||||
* add low-level functions
|
||||
* only need to store flags on directories
|
||||
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13)
|
||||
* done can be an unbuffered channel
|
||||
* remove calls to os.NewSyscallError
|
||||
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher)
|
||||
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48)
|
||||
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
|
||||
|
||||
## v1.0.4 / 2014-09-07
|
||||
|
||||
* kqueue: add dragonfly to the build tags.
|
||||
* Rename source code files, rearrange code so exported APIs are at the top.
|
||||
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang)
|
||||
|
||||
## v1.0.3 / 2014-08-19
|
||||
|
||||
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36)
|
||||
|
||||
## v1.0.2 / 2014-08-17
|
||||
|
||||
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
|
||||
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
|
||||
|
||||
## v1.0.0 / 2014-08-15
|
||||
|
||||
* [API] Remove AddWatch on Windows, use Add.
|
||||
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30)
|
||||
* Minor updates based on feedback from golint.
|
||||
|
||||
## dev / 2014-07-09
|
||||
|
||||
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify).
|
||||
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno)
|
||||
|
||||
## dev / 2014-07-04
|
||||
|
||||
* kqueue: fix incorrect mutex used in Close()
|
||||
* Update example to demonstrate usage of Op.
|
||||
|
||||
## dev / 2014-06-28
|
||||
|
||||
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4)
|
||||
* Fix for String() method on Event (thanks Alex Brainman)
|
||||
* Don't build on Plan 9 or Solaris (thanks @4ad)
|
||||
|
||||
## dev / 2014-06-21
|
||||
|
||||
* Events channel of type Event rather than *Event.
|
||||
* [internal] use syscall constants directly for inotify and kqueue.
|
||||
* [internal] kqueue: rename events to kevents and fileEvent to event.
|
||||
|
||||
## dev / 2014-06-19
|
||||
|
||||
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally).
|
||||
* [internal] remove cookie from Event struct (unused).
|
||||
* [internal] Event struct has the same definition across every OS.
|
||||
* [internal] remove internal watch and removeWatch methods.
|
||||
|
||||
## dev / 2014-06-12
|
||||
|
||||
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove().
|
||||
* [API] Pluralized channel names: Events and Errors.
|
||||
* [API] Renamed FileEvent struct to Event.
|
||||
* [API] Op constants replace methods like IsCreate().
|
||||
|
||||
## dev / 2014-06-12
|
||||
|
||||
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
|
||||
|
||||
## dev / 2014-05-23
|
||||
|
||||
* [API] Remove current implementation of WatchFlags.
|
||||
* current implementation doesn't take advantage of OS for efficiency
|
||||
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes
|
||||
* no tests for the current implementation
|
||||
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
|
||||
|
||||
## v0.9.3 / 2014-12-31
|
||||
|
||||
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
|
||||
|
||||
## v0.9.2 / 2014-08-17
|
||||
|
||||
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
|
||||
|
||||
## v0.9.1 / 2014-06-12
|
||||
|
||||
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
|
||||
|
||||
## v0.9.0 / 2014-01-17
|
||||
|
||||
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
|
||||
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
|
||||
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
|
||||
|
||||
## v0.8.12 / 2013-11-13
|
||||
|
||||
* [API] Remove FD_SET and friends from Linux adapter
|
||||
|
||||
## v0.8.11 / 2013-11-02
|
||||
|
||||
* [Doc] Add Changelog [#72][] (thanks @nathany)
|
||||
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
|
||||
|
||||
## v0.8.10 / 2013-10-19
|
||||
|
||||
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
|
||||
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
|
||||
* [Doc] specify OS-specific limits in README (thanks @debrando)
|
||||
|
||||
## v0.8.9 / 2013-09-08
|
||||
|
||||
* [Doc] Contributing (thanks @nathany)
|
||||
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
|
||||
* [Doc] GoCI badge in README (Linux only) [#60][]
|
||||
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
|
||||
|
||||
## v0.8.8 / 2013-06-17
|
||||
|
||||
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
|
||||
|
||||
## v0.8.7 / 2013-06-03
|
||||
|
||||
* [API] Make syscall flags internal
|
||||
* [Fix] inotify: ignore event changes
|
||||
* [Fix] race in symlink test [#45][] (reported by @srid)
|
||||
* [Fix] tests on Windows
|
||||
* lower case error messages
|
||||
|
||||
## v0.8.6 / 2013-05-23
|
||||
|
||||
* kqueue: Use EVT_ONLY flag on Darwin
|
||||
* [Doc] Update README with full example
|
||||
|
||||
## v0.8.5 / 2013-05-09
|
||||
|
||||
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
|
||||
|
||||
## v0.8.4 / 2013-04-07
|
||||
|
||||
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
|
||||
|
||||
## v0.8.3 / 2013-03-13
|
||||
|
||||
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
|
||||
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
|
||||
|
||||
## v0.8.2 / 2013-02-07
|
||||
|
||||
* [Doc] add Authors
|
||||
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
|
||||
|
||||
## v0.8.1 / 2013-01-09
|
||||
|
||||
* [Fix] Windows path separators
|
||||
* [Doc] BSD License
|
||||
|
||||
## v0.8.0 / 2012-11-09
|
||||
|
||||
* kqueue: directory watching improvements (thanks @vmirage)
|
||||
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
|
||||
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
|
||||
|
||||
## v0.7.4 / 2012-10-09
|
||||
|
||||
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
|
||||
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
|
||||
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
|
||||
* [Fix] kqueue: modify after recreation of file
|
||||
|
||||
## v0.7.3 / 2012-09-27
|
||||
|
||||
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
|
||||
* [Fix] kqueue: no longer get duplicate CREATE events
|
||||
|
||||
## v0.7.2 / 2012-09-01
|
||||
|
||||
* kqueue: events for created directories
|
||||
|
||||
## v0.7.1 / 2012-07-14
|
||||
|
||||
* [Fix] for renaming files
|
||||
|
||||
## v0.7.0 / 2012-07-02
|
||||
|
||||
* [Feature] FSNotify flags
|
||||
* [Fix] inotify: Added file name back to event path
|
||||
|
||||
## v0.6.0 / 2012-06-06
|
||||
|
||||
* kqueue: watch files after directory created (thanks @tmc)
|
||||
|
||||
## v0.5.1 / 2012-05-22
|
||||
|
||||
* [Fix] inotify: remove all watches before Close()
|
||||
|
||||
## v0.5.0 / 2012-05-03
|
||||
|
||||
* [API] kqueue: return errors during watch instead of sending over channel
|
||||
* kqueue: match symlink behavior on Linux
|
||||
* inotify: add `DELETE_SELF` (requested by @taralx)
|
||||
* [Fix] kqueue: handle EINTR (reported by @robfig)
|
||||
* [Doc] Godoc example [#1][] (thanks @davecheney)
|
||||
|
||||
## v0.4.0 / 2012-03-30
|
||||
|
||||
* Go 1 released: build with go tool
|
||||
* [Feature] Windows support using winfsnotify
|
||||
* Windows does not have attribute change notifications
|
||||
* Roll attribute notifications into IsModify
|
||||
|
||||
## v0.3.0 / 2012-02-19
|
||||
|
||||
* kqueue: add files when watch directory
|
||||
|
||||
## v0.2.0 / 2011-12-30
|
||||
|
||||
* update to latest Go weekly code
|
||||
|
||||
## v0.1.0 / 2011-10-19
|
||||
|
||||
* kqueue: add watch on file creation to match inotify
|
||||
* kqueue: create file event
|
||||
* inotify: ignore `IN_IGNORED` events
|
||||
* event String()
|
||||
* linux: common FileEvent functions
|
||||
* initial commit
|
||||
|
||||
[#79]: https://github.com/howeyc/fsnotify/pull/79
|
||||
[#77]: https://github.com/howeyc/fsnotify/pull/77
|
||||
[#72]: https://github.com/howeyc/fsnotify/issues/72
|
||||
[#71]: https://github.com/howeyc/fsnotify/issues/71
|
||||
[#70]: https://github.com/howeyc/fsnotify/issues/70
|
||||
[#63]: https://github.com/howeyc/fsnotify/issues/63
|
||||
[#62]: https://github.com/howeyc/fsnotify/issues/62
|
||||
[#60]: https://github.com/howeyc/fsnotify/issues/60
|
||||
[#59]: https://github.com/howeyc/fsnotify/issues/59
|
||||
[#49]: https://github.com/howeyc/fsnotify/issues/49
|
||||
[#45]: https://github.com/howeyc/fsnotify/issues/45
|
||||
[#40]: https://github.com/howeyc/fsnotify/issues/40
|
||||
[#36]: https://github.com/howeyc/fsnotify/issues/36
|
||||
[#33]: https://github.com/howeyc/fsnotify/issues/33
|
||||
[#29]: https://github.com/howeyc/fsnotify/issues/29
|
||||
[#25]: https://github.com/howeyc/fsnotify/issues/25
|
||||
[#24]: https://github.com/howeyc/fsnotify/issues/24
|
||||
[#21]: https://github.com/howeyc/fsnotify/issues/21
|
|
@ -0,0 +1,77 @@
|
|||
# Contributing
|
||||
|
||||
## Issues
|
||||
|
||||
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues).
|
||||
* Please indicate the platform you are using fsnotify on.
|
||||
* A code example to reproduce the problem is appreciated.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
### Contributor License Agreement
|
||||
|
||||
fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual).
|
||||
|
||||
Please indicate that you have signed the CLA in your pull request.
|
||||
|
||||
### How fsnotify is Developed
|
||||
|
||||
* Development is done on feature branches.
|
||||
* Tests are run on BSD, Linux, macOS and Windows.
|
||||
* Pull requests are reviewed and [applied to master][am] using [hub][].
|
||||
* Maintainers may modify or squash commits rather than asking contributors to.
|
||||
* To issue a new release, the maintainers will:
|
||||
* Update the CHANGELOG
|
||||
* Tag a version, which will become available through gopkg.in.
|
||||
|
||||
### How to Fork
|
||||
|
||||
For smooth sailing, always use the original import path. Installing with `go get` makes this easy.
|
||||
|
||||
1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`)
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Ensure everything works and the tests pass (see below)
|
||||
4. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
|
||||
Contribute upstream:
|
||||
|
||||
1. Fork fsnotify on GitHub
|
||||
2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`)
|
||||
3. Push to the branch (`git push fork my-new-feature`)
|
||||
4. Create a new Pull Request on GitHub
|
||||
|
||||
This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/).
|
||||
|
||||
### Testing
|
||||
|
||||
fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows.
|
||||
|
||||
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on.
|
||||
|
||||
To aid in cross-platform testing there is a Vagrantfile for Linux and BSD.
|
||||
|
||||
* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/)
|
||||
* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder.
|
||||
* Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password)
|
||||
* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd fsnotify/fsnotify; go test'`.
|
||||
* When you're done, you will want to halt or destroy the Vagrant boxes.
|
||||
|
||||
Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory.
|
||||
|
||||
Right now there is no equivalent solution for Windows and macOS, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads).
|
||||
|
||||
### Maintainers
|
||||
|
||||
Help maintaining fsnotify is welcome. To be a maintainer:
|
||||
|
||||
* Submit a pull request and sign the CLA as above.
|
||||
* You must be able to run the test suite on Mac, Windows, Linux and BSD.
|
||||
|
||||
To keep master clean, the fsnotify project uses the "apply mail" workflow outlined in Nathaniel Talbott's post ["Merge pull request" Considered Harmful][am]. This requires installing [hub][].
|
||||
|
||||
All code changes should be internal pull requests.
|
||||
|
||||
Releases are tagged using [Semantic Versioning](http://semver.org/).
|
||||
|
||||
[hub]: https://github.com/github/hub
|
||||
[am]: http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.VGa5yZPF_Zs
|
|
@ -0,0 +1,28 @@
|
|||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2012-2019 fsnotify 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.
|
|
@ -0,0 +1,130 @@
|
|||
# File system notifications for Go
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/fsnotify/fsnotify?status.svg)](https://godoc.org/github.com/fsnotify/fsnotify) [![Go Report Card](https://goreportcard.com/badge/github.com/fsnotify/fsnotify)](https://goreportcard.com/report/github.com/fsnotify/fsnotify)
|
||||
|
||||
fsnotify utilizes [golang.org/x/sys](https://godoc.org/golang.org/x/sys) rather than `syscall` from the standard library. Ensure you have the latest version installed by running:
|
||||
|
||||
```console
|
||||
go get -u golang.org/x/sys/...
|
||||
```
|
||||
|
||||
Cross platform: Windows, Linux, BSD and macOS.
|
||||
|
||||
| Adapter | OS | Status |
|
||||
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| inotify | Linux 2.6.27 or later, Android\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
|
||||
| kqueue | BSD, macOS, iOS\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
|
||||
| ReadDirectoryChangesW | Windows | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
|
||||
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) |
|
||||
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/issues/12) |
|
||||
| fanotify | Linux 2.6.37+ | [Planned](https://github.com/fsnotify/fsnotify/issues/114) |
|
||||
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) |
|
||||
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) |
|
||||
|
||||
\* Android and iOS are untested.
|
||||
|
||||
Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information.
|
||||
|
||||
## API stability
|
||||
|
||||
fsnotify is a fork of [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify) with a new API as of v1.0. The API is based on [this design document](http://goo.gl/MrYxyA).
|
||||
|
||||
All [releases](https://github.com/fsnotify/fsnotify/releases) are tagged based on [Semantic Versioning](http://semver.org/). Further API changes are [planned](https://github.com/fsnotify/fsnotify/milestones), and will be tagged with a new major revision number.
|
||||
|
||||
Go 1.6 supports dependencies located in the `vendor/` folder. Unless you are creating a library, it is recommended that you copy fsnotify into `vendor/github.com/fsnotify/fsnotify` within your project, and likewise for `golang.org/x/sys`.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func main() {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("event:", event)
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
log.Println("modified file:", event.Name)
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add("/tmp/foo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING][] before opening an issue or pull request.
|
||||
|
||||
## Example
|
||||
|
||||
See [example_test.go](https://github.com/fsnotify/fsnotify/blob/master/example_test.go).
|
||||
|
||||
## FAQ
|
||||
|
||||
**When a file is moved to another directory is it still being watched?**
|
||||
|
||||
No (it shouldn't be, unless you are watching where it was moved to).
|
||||
|
||||
**When I watch a directory, are all subdirectories watched as well?**
|
||||
|
||||
No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]).
|
||||
|
||||
**Do I have to watch the Error and Event channels in a separate goroutine?**
|
||||
|
||||
As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7])
|
||||
|
||||
**Why am I receiving multiple events for the same file on OS X?**
|
||||
|
||||
Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]).
|
||||
|
||||
**How many files can be watched at once?**
|
||||
|
||||
There are OS-specific limits as to how many watches can be created:
|
||||
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error.
|
||||
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error.
|
||||
|
||||
**Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?**
|
||||
|
||||
fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications.
|
||||
|
||||
[#62]: https://github.com/howeyc/fsnotify/issues/62
|
||||
[#18]: https://github.com/fsnotify/fsnotify/issues/18
|
||||
[#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||
[#7]: https://github.com/howeyc/fsnotify/issues/7
|
||||
|
||||
[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [notify](https://github.com/rjeczalik/notify)
|
||||
* [fsevents](https://github.com/fsnotify/fsevents)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Watcher watches a set of files, delivering events to a channel.
|
||||
type Watcher struct {
|
||||
Events chan Event
|
||||
Errors chan error
|
||||
}
|
||||
|
||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
func NewWatcher() (*Watcher, error) {
|
||||
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
|
||||
}
|
||||
|
||||
// Close removes all watches and closes the events channel.
|
||||
func (w *Watcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add starts watching the named file or directory (non-recursively).
|
||||
func (w *Watcher) Add(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove stops watching the the named file or directory (non-recursively).
|
||||
func (w *Watcher) Remove(name string) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !plan9
|
||||
|
||||
// Package fsnotify provides a platform-independent interface for file system notifications.
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Event represents a single file system notification.
|
||||
type Event struct {
|
||||
Name string // Relative path to the file or directory.
|
||||
Op Op // File operation that triggered the event.
|
||||
}
|
||||
|
||||
// Op describes a set of file operations.
|
||||
type Op uint32
|
||||
|
||||
// These are the generalized file operations that can trigger a notification.
|
||||
const (
|
||||
Create Op = 1 << iota
|
||||
Write
|
||||
Remove
|
||||
Rename
|
||||
Chmod
|
||||
)
|
||||
|
||||
func (op Op) String() string {
|
||||
// Use a buffer for efficient string concatenation
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if op&Create == Create {
|
||||
buffer.WriteString("|CREATE")
|
||||
}
|
||||
if op&Remove == Remove {
|
||||
buffer.WriteString("|REMOVE")
|
||||
}
|
||||
if op&Write == Write {
|
||||
buffer.WriteString("|WRITE")
|
||||
}
|
||||
if op&Rename == Rename {
|
||||
buffer.WriteString("|RENAME")
|
||||
}
|
||||
if op&Chmod == Chmod {
|
||||
buffer.WriteString("|CHMOD")
|
||||
}
|
||||
if buffer.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
return buffer.String()[1:] // Strip leading pipe
|
||||
}
|
||||
|
||||
// String returns a string representation of the event in the form
|
||||
// "file: REMOVE|WRITE|..."
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
|
||||
}
|
||||
|
||||
// Common errors that can be reported by a watcher
|
||||
var (
|
||||
ErrEventOverflow = errors.New("fsnotify queue overflow")
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/fsnotify/fsnotify
|
||||
|
||||
go 1.13
|
||||
|
||||
require golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9
|
|
@ -0,0 +1,2 @@
|
|||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
@ -0,0 +1,337 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Watcher watches a set of files, delivering events to a channel.
|
||||
type Watcher struct {
|
||||
Events chan Event
|
||||
Errors chan error
|
||||
mu sync.Mutex // Map access
|
||||
fd int
|
||||
poller *fdPoller
|
||||
watches map[string]*watch // Map of inotify watches (key: path)
|
||||
paths map[int]string // Map of watched paths (key: watch descriptor)
|
||||
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||
doneResp chan struct{} // Channel to respond to Close
|
||||
}
|
||||
|
||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
func NewWatcher() (*Watcher, error) {
|
||||
// Create inotify fd
|
||||
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
|
||||
if fd == -1 {
|
||||
return nil, errno
|
||||
}
|
||||
// Create epoll
|
||||
poller, err := newFdPoller(fd)
|
||||
if err != nil {
|
||||
unix.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
w := &Watcher{
|
||||
fd: fd,
|
||||
poller: poller,
|
||||
watches: make(map[string]*watch),
|
||||
paths: make(map[int]string),
|
||||
Events: make(chan Event),
|
||||
Errors: make(chan error),
|
||||
done: make(chan struct{}),
|
||||
doneResp: make(chan struct{}),
|
||||
}
|
||||
|
||||
go w.readEvents()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *Watcher) isClosed() bool {
|
||||
select {
|
||||
case <-w.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Close removes all watches and closes the events channel.
|
||||
func (w *Watcher) Close() error {
|
||||
if w.isClosed() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send 'close' signal to goroutine, and set the Watcher to closed.
|
||||
close(w.done)
|
||||
|
||||
// Wake up goroutine
|
||||
w.poller.wake()
|
||||
|
||||
// Wait for goroutine to close
|
||||
<-w.doneResp
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add starts watching the named file or directory (non-recursively).
|
||||
func (w *Watcher) Add(name string) error {
|
||||
name = filepath.Clean(name)
|
||||
if w.isClosed() {
|
||||
return errors.New("inotify instance already closed")
|
||||
}
|
||||
|
||||
const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
|
||||
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
|
||||
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
|
||||
|
||||
var flags uint32 = agnosticEvents
|
||||
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
watchEntry := w.watches[name]
|
||||
if watchEntry != nil {
|
||||
flags |= watchEntry.flags | unix.IN_MASK_ADD
|
||||
}
|
||||
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
|
||||
if wd == -1 {
|
||||
return errno
|
||||
}
|
||||
|
||||
if watchEntry == nil {
|
||||
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
|
||||
w.paths[wd] = name
|
||||
} else {
|
||||
watchEntry.wd = uint32(wd)
|
||||
watchEntry.flags = flags
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove stops watching the named file or directory (non-recursively).
|
||||
func (w *Watcher) Remove(name string) error {
|
||||
name = filepath.Clean(name)
|
||||
|
||||
// Fetch the watch.
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
watch, ok := w.watches[name]
|
||||
|
||||
// Remove it from inotify.
|
||||
if !ok {
|
||||
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
|
||||
}
|
||||
|
||||
// We successfully removed the watch if InotifyRmWatch doesn't return an
|
||||
// error, we need to clean up our internal state to ensure it matches
|
||||
// inotify's kernel state.
|
||||
delete(w.paths, int(watch.wd))
|
||||
delete(w.watches, name)
|
||||
|
||||
// inotify_rm_watch will return EINVAL if the file has been deleted;
|
||||
// the inotify will already have been removed.
|
||||
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
|
||||
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
|
||||
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
|
||||
// by another thread and we have not received IN_IGNORE event.
|
||||
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
|
||||
if success == -1 {
|
||||
// TODO: Perhaps it's not helpful to return an error here in every case.
|
||||
// the only two possible errors are:
|
||||
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
|
||||
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
|
||||
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
|
||||
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
|
||||
return errno
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type watch struct {
|
||||
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||
}
|
||||
|
||||
// readEvents reads from the inotify file descriptor, converts the
|
||||
// received events into Event objects and sends them via the Events channel
|
||||
func (w *Watcher) readEvents() {
|
||||
var (
|
||||
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
||||
n int // Number of bytes read with read()
|
||||
errno error // Syscall errno
|
||||
ok bool // For poller.wait
|
||||
)
|
||||
|
||||
defer close(w.doneResp)
|
||||
defer close(w.Errors)
|
||||
defer close(w.Events)
|
||||
defer unix.Close(w.fd)
|
||||
defer w.poller.close()
|
||||
|
||||
for {
|
||||
// See if we have been closed.
|
||||
if w.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
ok, errno = w.poller.wait()
|
||||
if errno != nil {
|
||||
select {
|
||||
case w.Errors <- errno:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
n, errno = unix.Read(w.fd, buf[:])
|
||||
// If a signal interrupted execution, see if we've been asked to close, and try again.
|
||||
// http://man7.org/linux/man-pages/man7/signal.7.html :
|
||||
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
|
||||
if errno == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
|
||||
// unix.Read might have been woken up by Close. If so, we're done.
|
||||
if w.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
if n < unix.SizeofInotifyEvent {
|
||||
var err error
|
||||
if n == 0 {
|
||||
// If EOF is received. This should really never happen.
|
||||
err = io.EOF
|
||||
} else if n < 0 {
|
||||
// If an error occurred while reading.
|
||||
err = errno
|
||||
} else {
|
||||
// Read was too short.
|
||||
err = errors.New("notify: short read in readEvents()")
|
||||
}
|
||||
select {
|
||||
case w.Errors <- err:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var offset uint32
|
||||
// We don't know how many events we just read into the buffer
|
||||
// While the offset points to at least one whole event...
|
||||
for offset <= uint32(n-unix.SizeofInotifyEvent) {
|
||||
// Point "raw" to the event in the buffer
|
||||
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
|
||||
|
||||
mask := uint32(raw.Mask)
|
||||
nameLen := uint32(raw.Len)
|
||||
|
||||
if mask&unix.IN_Q_OVERFLOW != 0 {
|
||||
select {
|
||||
case w.Errors <- ErrEventOverflow:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the event happened to the watched directory or the watched file, the kernel
|
||||
// doesn't append the filename to the event, but we would like to always fill the
|
||||
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||
// the "paths" map.
|
||||
w.mu.Lock()
|
||||
name, ok := w.paths[int(raw.Wd)]
|
||||
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
|
||||
// This is a sign to clean up the maps, otherwise we are no longer in sync
|
||||
// with the inotify kernel state which has already deleted the watch
|
||||
// automatically.
|
||||
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
|
||||
delete(w.paths, int(raw.Wd))
|
||||
delete(w.watches, name)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
if nameLen > 0 {
|
||||
// Point "bytes" at the first byte of the filename
|
||||
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))
|
||||
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
|
||||
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
|
||||
}
|
||||
|
||||
event := newEvent(name, mask)
|
||||
|
||||
// Send the events that are not ignored on the events channel
|
||||
if !event.ignoreLinux(mask) {
|
||||
select {
|
||||
case w.Events <- event:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the next event in the buffer
|
||||
offset += unix.SizeofInotifyEvent + nameLen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Certain types of events can be "ignored" and not sent over the Events
|
||||
// channel. Such as events marked ignore by the kernel, or MODIFY events
|
||||
// against files that do not exist.
|
||||
func (e *Event) ignoreLinux(mask uint32) bool {
|
||||
// Ignore anything the inotify API says to ignore
|
||||
if mask&unix.IN_IGNORED == unix.IN_IGNORED {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the event is not a DELETE or RENAME, the file must exist.
|
||||
// Otherwise the event is ignored.
|
||||
// *Note*: this was put in place because it was seen that a MODIFY
|
||||
// event was sent after the DELETE. This ignores that MODIFY and
|
||||
// assumes a DELETE will come or has come if the file doesn't exist.
|
||||
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
|
||||
_, statErr := os.Lstat(e.Name)
|
||||
return os.IsNotExist(statErr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newEvent returns an platform-independent Event based on an inotify mask.
|
||||
func newEvent(name string, mask uint32) Event {
|
||||
e := Event{Name: name}
|
||||
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
|
||||
e.Op |= Create
|
||||
}
|
||||
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
|
||||
e.Op |= Remove
|
||||
}
|
||||
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
|
||||
e.Op |= Write
|
||||
}
|
||||
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
|
||||
e.Op |= Rename
|
||||
}
|
||||
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
|
||||
e.Op |= Chmod
|
||||
}
|
||||
return e
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type fdPoller struct {
|
||||
fd int // File descriptor (as returned by the inotify_init() syscall)
|
||||
epfd int // Epoll file descriptor
|
||||
pipe [2]int // Pipe for waking up
|
||||
}
|
||||
|
||||
func emptyPoller(fd int) *fdPoller {
|
||||
poller := new(fdPoller)
|
||||
poller.fd = fd
|
||||
poller.epfd = -1
|
||||
poller.pipe[0] = -1
|
||||
poller.pipe[1] = -1
|
||||
return poller
|
||||
}
|
||||
|
||||
// Create a new inotify poller.
|
||||
// This creates an inotify handler, and an epoll handler.
|
||||
func newFdPoller(fd int) (*fdPoller, error) {
|
||||
var errno error
|
||||
poller := emptyPoller(fd)
|
||||
defer func() {
|
||||
if errno != nil {
|
||||
poller.close()
|
||||
}
|
||||
}()
|
||||
poller.fd = fd
|
||||
|
||||
// Create epoll fd
|
||||
poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC)
|
||||
if poller.epfd == -1 {
|
||||
return nil, errno
|
||||
}
|
||||
// Create pipe; pipe[0] is the read end, pipe[1] the write end.
|
||||
errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC)
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
// Register inotify fd with epoll
|
||||
event := unix.EpollEvent{
|
||||
Fd: int32(poller.fd),
|
||||
Events: unix.EPOLLIN,
|
||||
}
|
||||
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event)
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
// Register pipe fd with epoll
|
||||
event = unix.EpollEvent{
|
||||
Fd: int32(poller.pipe[0]),
|
||||
Events: unix.EPOLLIN,
|
||||
}
|
||||
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event)
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
return poller, nil
|
||||
}
|
||||
|
||||
// Wait using epoll.
|
||||
// Returns true if something is ready to be read,
|
||||
// false if there is not.
|
||||
func (poller *fdPoller) wait() (bool, error) {
|
||||
// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
|
||||
// I don't know whether epoll_wait returns the number of events returned,
|
||||
// or the total number of events ready.
|
||||
// I decided to catch both by making the buffer one larger than the maximum.
|
||||
events := make([]unix.EpollEvent, 7)
|
||||
for {
|
||||
n, errno := unix.EpollWait(poller.epfd, events, -1)
|
||||
if n == -1 {
|
||||
if errno == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
return false, errno
|
||||
}
|
||||
if n == 0 {
|
||||
// If there are no events, try again.
|
||||
continue
|
||||
}
|
||||
if n > 6 {
|
||||
// This should never happen. More events were returned than should be possible.
|
||||
return false, errors.New("epoll_wait returned more events than I know what to do with")
|
||||
}
|
||||
ready := events[:n]
|
||||
epollhup := false
|
||||
epollerr := false
|
||||
epollin := false
|
||||
for _, event := range ready {
|
||||
if event.Fd == int32(poller.fd) {
|
||||
if event.Events&unix.EPOLLHUP != 0 {
|
||||
// This should not happen, but if it does, treat it as a wakeup.
|
||||
epollhup = true
|
||||
}
|
||||
if event.Events&unix.EPOLLERR != 0 {
|
||||
// If an error is waiting on the file descriptor, we should pretend
|
||||
// something is ready to read, and let unix.Read pick up the error.
|
||||
epollerr = true
|
||||
}
|
||||
if event.Events&unix.EPOLLIN != 0 {
|
||||
// There is data to read.
|
||||
epollin = true
|
||||
}
|
||||
}
|
||||
if event.Fd == int32(poller.pipe[0]) {
|
||||
if event.Events&unix.EPOLLHUP != 0 {
|
||||
// Write pipe descriptor was closed, by us. This means we're closing down the
|
||||
// watcher, and we should wake up.
|
||||
}
|
||||
if event.Events&unix.EPOLLERR != 0 {
|
||||
// If an error is waiting on the pipe file descriptor.
|
||||
// This is an absolute mystery, and should never ever happen.
|
||||
return false, errors.New("Error on the pipe descriptor.")
|
||||
}
|
||||
if event.Events&unix.EPOLLIN != 0 {
|
||||
// This is a regular wakeup, so we have to clear the buffer.
|
||||
err := poller.clearWake()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if epollhup || epollerr || epollin {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Close the write end of the poller.
|
||||
func (poller *fdPoller) wake() error {
|
||||
buf := make([]byte, 1)
|
||||
n, errno := unix.Write(poller.pipe[1], buf)
|
||||
if n == -1 {
|
||||
if errno == unix.EAGAIN {
|
||||
// Buffer is full, poller will wake.
|
||||
return nil
|
||||
}
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (poller *fdPoller) clearWake() error {
|
||||
// You have to be woken up a LOT in order to get to 100!
|
||||
buf := make([]byte, 100)
|
||||
n, errno := unix.Read(poller.pipe[0], buf)
|
||||
if n == -1 {
|
||||
if errno == unix.EAGAIN {
|
||||
// Buffer is empty, someone else cleared our wake.
|
||||
return nil
|
||||
}
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close all poller file descriptors, but not the one passed to it.
|
||||
func (poller *fdPoller) close() {
|
||||
if poller.pipe[1] != -1 {
|
||||
unix.Close(poller.pipe[1])
|
||||
}
|
||||
if poller.pipe[0] != -1 {
|
||||
unix.Close(poller.pipe[0])
|
||||
}
|
||||
if poller.epfd != -1 {
|
||||
unix.Close(poller.epfd)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,521 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd openbsd netbsd dragonfly darwin
|
||||
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Watcher watches a set of files, delivering events to a channel.
|
||||
type Watcher struct {
|
||||
Events chan Event
|
||||
Errors chan error
|
||||
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||
|
||||
kq int // File descriptor (as returned by the kqueue() syscall).
|
||||
|
||||
mu sync.Mutex // Protects access to watcher data
|
||||
watches map[string]int // Map of watched file descriptors (key: path).
|
||||
externalWatches map[string]bool // Map of watches added by user of the library.
|
||||
dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue.
|
||||
paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events.
|
||||
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events).
|
||||
isClosed bool // Set to true when Close() is first called
|
||||
}
|
||||
|
||||
type pathInfo struct {
|
||||
name string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
func NewWatcher() (*Watcher, error) {
|
||||
kq, err := kqueue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Watcher{
|
||||
kq: kq,
|
||||
watches: make(map[string]int),
|
||||
dirFlags: make(map[string]uint32),
|
||||
paths: make(map[int]pathInfo),
|
||||
fileExists: make(map[string]bool),
|
||||
externalWatches: make(map[string]bool),
|
||||
Events: make(chan Event),
|
||||
Errors: make(chan error),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go w.readEvents()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Close removes all watches and closes the events channel.
|
||||
func (w *Watcher) Close() error {
|
||||
w.mu.Lock()
|
||||
if w.isClosed {
|
||||
w.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
w.isClosed = true
|
||||
|
||||
// copy paths to remove while locked
|
||||
var pathsToRemove = make([]string, 0, len(w.watches))
|
||||
for name := range w.watches {
|
||||
pathsToRemove = append(pathsToRemove, name)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
// unlock before calling Remove, which also locks
|
||||
|
||||
for _, name := range pathsToRemove {
|
||||
w.Remove(name)
|
||||
}
|
||||
|
||||
// send a "quit" message to the reader goroutine
|
||||
close(w.done)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add starts watching the named file or directory (non-recursively).
|
||||
func (w *Watcher) Add(name string) error {
|
||||
w.mu.Lock()
|
||||
w.externalWatches[name] = true
|
||||
w.mu.Unlock()
|
||||
_, err := w.addWatch(name, noteAllEvents)
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove stops watching the the named file or directory (non-recursively).
|
||||
func (w *Watcher) Remove(name string) error {
|
||||
name = filepath.Clean(name)
|
||||
w.mu.Lock()
|
||||
watchfd, ok := w.watches[name]
|
||||
w.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("can't remove non-existent kevent watch for: %s", name)
|
||||
}
|
||||
|
||||
const registerRemove = unix.EV_DELETE
|
||||
if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unix.Close(watchfd)
|
||||
|
||||
w.mu.Lock()
|
||||
isDir := w.paths[watchfd].isDir
|
||||
delete(w.watches, name)
|
||||
delete(w.paths, watchfd)
|
||||
delete(w.dirFlags, name)
|
||||
w.mu.Unlock()
|
||||
|
||||
// Find all watched paths that are in this directory that are not external.
|
||||
if isDir {
|
||||
var pathsToRemove []string
|
||||
w.mu.Lock()
|
||||
for _, path := range w.paths {
|
||||
wdir, _ := filepath.Split(path.name)
|
||||
if filepath.Clean(wdir) == name {
|
||||
if !w.externalWatches[path.name] {
|
||||
pathsToRemove = append(pathsToRemove, path.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.mu.Unlock()
|
||||
for _, name := range pathsToRemove {
|
||||
// Since these are internal, not much sense in propagating error
|
||||
// to the user, as that will just confuse them with an error about
|
||||
// a path they did not explicitly watch themselves.
|
||||
w.Remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
||||
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
|
||||
|
||||
// keventWaitTime to block on each read from kevent
|
||||
var keventWaitTime = durationToTimespec(100 * time.Millisecond)
|
||||
|
||||
// addWatch adds name to the watched file set.
|
||||
// The flags are interpreted as described in kevent(2).
|
||||
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
|
||||
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||
var isDir bool
|
||||
// Make ./name and name equivalent
|
||||
name = filepath.Clean(name)
|
||||
|
||||
w.mu.Lock()
|
||||
if w.isClosed {
|
||||
w.mu.Unlock()
|
||||
return "", errors.New("kevent instance already closed")
|
||||
}
|
||||
watchfd, alreadyWatching := w.watches[name]
|
||||
// We already have a watch, but we can still override flags.
|
||||
if alreadyWatching {
|
||||
isDir = w.paths[watchfd].isDir
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
if !alreadyWatching {
|
||||
fi, err := os.Lstat(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Don't watch sockets.
|
||||
if fi.Mode()&os.ModeSocket == os.ModeSocket {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Don't watch named pipes.
|
||||
if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Follow Symlinks
|
||||
// Unfortunately, Linux can add bogus symlinks to watch list without
|
||||
// issue, and Windows can't do symlinks period (AFAIK). To maintain
|
||||
// consistency, we will act like everything is fine. There will simply
|
||||
// be no file events for broken symlinks.
|
||||
// Hence the returns of nil on errors.
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
name, err = filepath.EvalSymlinks(name)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
_, alreadyWatching = w.watches[name]
|
||||
w.mu.Unlock()
|
||||
|
||||
if alreadyWatching {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
fi, err = os.Lstat(name)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
watchfd, err = unix.Open(name, openMode, 0700)
|
||||
if watchfd == -1 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
isDir = fi.IsDir()
|
||||
}
|
||||
|
||||
const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE
|
||||
if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil {
|
||||
unix.Close(watchfd)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !alreadyWatching {
|
||||
w.mu.Lock()
|
||||
w.watches[name] = watchfd
|
||||
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
if isDir {
|
||||
// Watch the directory if it has not been watched before,
|
||||
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
||||
w.mu.Lock()
|
||||
|
||||
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
|
||||
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
|
||||
// Store flags so this watch can be updated later
|
||||
w.dirFlags[name] = flags
|
||||
w.mu.Unlock()
|
||||
|
||||
if watchDir {
|
||||
if err := w.watchDirectoryFiles(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// readEvents reads from kqueue and converts the received kevents into
|
||||
// Event values that it sends down the Events channel.
|
||||
func (w *Watcher) readEvents() {
|
||||
eventBuffer := make([]unix.Kevent_t, 10)
|
||||
|
||||
loop:
|
||||
for {
|
||||
// See if there is a message on the "done" channel
|
||||
select {
|
||||
case <-w.done:
|
||||
break loop
|
||||
default:
|
||||
}
|
||||
|
||||
// Get new events
|
||||
kevents, err := read(w.kq, eventBuffer, &keventWaitTime)
|
||||
// EINTR is okay, the syscall was interrupted before timeout expired.
|
||||
if err != nil && err != unix.EINTR {
|
||||
select {
|
||||
case w.Errors <- err:
|
||||
case <-w.done:
|
||||
break loop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Flush the events we received to the Events channel
|
||||
for len(kevents) > 0 {
|
||||
kevent := &kevents[0]
|
||||
watchfd := int(kevent.Ident)
|
||||
mask := uint32(kevent.Fflags)
|
||||
w.mu.Lock()
|
||||
path := w.paths[watchfd]
|
||||
w.mu.Unlock()
|
||||
event := newEvent(path.name, mask)
|
||||
|
||||
if path.isDir && !(event.Op&Remove == Remove) {
|
||||
// Double check to make sure the directory exists. This can happen when
|
||||
// we do a rm -fr on a recursively watched folders and we receive a
|
||||
// modification event first but the folder has been deleted and later
|
||||
// receive the delete event
|
||||
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
|
||||
// mark is as delete event
|
||||
event.Op |= Remove
|
||||
}
|
||||
}
|
||||
|
||||
if event.Op&Rename == Rename || event.Op&Remove == Remove {
|
||||
w.Remove(event.Name)
|
||||
w.mu.Lock()
|
||||
delete(w.fileExists, event.Name)
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) {
|
||||
w.sendDirectoryChangeEvents(event.Name)
|
||||
} else {
|
||||
// Send the event on the Events channel.
|
||||
select {
|
||||
case w.Events <- event:
|
||||
case <-w.done:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if event.Op&Remove == Remove {
|
||||
// Look for a file that may have overwritten this.
|
||||
// For example, mv f1 f2 will delete f2, then create f2.
|
||||
if path.isDir {
|
||||
fileDir := filepath.Clean(event.Name)
|
||||
w.mu.Lock()
|
||||
_, found := w.watches[fileDir]
|
||||
w.mu.Unlock()
|
||||
if found {
|
||||
// make sure the directory exists before we watch for changes. When we
|
||||
// do a recursive watch and perform rm -fr, the parent directory might
|
||||
// have gone missing, ignore the missing directory and let the
|
||||
// upcoming delete event remove the watch from the parent directory.
|
||||
if _, err := os.Lstat(fileDir); err == nil {
|
||||
w.sendDirectoryChangeEvents(fileDir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filePath := filepath.Clean(event.Name)
|
||||
if fileInfo, err := os.Lstat(filePath); err == nil {
|
||||
w.sendFileCreatedEventIfNew(filePath, fileInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next event
|
||||
kevents = kevents[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup
|
||||
err := unix.Close(w.kq)
|
||||
if err != nil {
|
||||
// only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors.
|
||||
select {
|
||||
case w.Errors <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(w.Events)
|
||||
close(w.Errors)
|
||||
}
|
||||
|
||||
// newEvent returns an platform-independent Event based on kqueue Fflags.
|
||||
func newEvent(name string, mask uint32) Event {
|
||||
e := Event{Name: name}
|
||||
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
|
||||
e.Op |= Remove
|
||||
}
|
||||
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE {
|
||||
e.Op |= Write
|
||||
}
|
||||
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME {
|
||||
e.Op |= Rename
|
||||
}
|
||||
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
|
||||
e.Op |= Chmod
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func newCreateEvent(name string) Event {
|
||||
return Event{Name: name, Op: Create}
|
||||
}
|
||||
|
||||
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
||||
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
||||
// Get all files
|
||||
files, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, fileInfo := range files {
|
||||
filePath := filepath.Join(dirPath, fileInfo.Name())
|
||||
filePath, err = w.internalWatch(filePath, fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
w.fileExists[filePath] = true
|
||||
w.mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendDirectoryEvents searches the directory for newly created files
|
||||
// and sends them over the event channel. This functionality is to have
|
||||
// the BSD version of fsnotify match Linux inotify which provides a
|
||||
// create event for files created in a watched directory.
|
||||
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
|
||||
// Get all files
|
||||
files, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
select {
|
||||
case w.Errors <- err:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Search for new files
|
||||
for _, fileInfo := range files {
|
||||
filePath := filepath.Join(dirPath, fileInfo.Name())
|
||||
err := w.sendFileCreatedEventIfNew(filePath, fileInfo)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
||||
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) {
|
||||
w.mu.Lock()
|
||||
_, doesExist := w.fileExists[filePath]
|
||||
w.mu.Unlock()
|
||||
if !doesExist {
|
||||
// Send create event
|
||||
select {
|
||||
case w.Events <- newCreateEvent(filePath):
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// like watchDirectoryFiles (but without doing another ReadDir)
|
||||
filePath, err = w.internalWatch(filePath, fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
w.fileExists[filePath] = true
|
||||
w.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) {
|
||||
if fileInfo.IsDir() {
|
||||
// mimic Linux providing delete events for subdirectories
|
||||
// but preserve the flags used if currently watching subdirectory
|
||||
w.mu.Lock()
|
||||
flags := w.dirFlags[name]
|
||||
w.mu.Unlock()
|
||||
|
||||
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
|
||||
return w.addWatch(name, flags)
|
||||
}
|
||||
|
||||
// watch file to mimic Linux inotify
|
||||
return w.addWatch(name, noteAllEvents)
|
||||
}
|
||||
|
||||
// kqueue creates a new kernel event queue and returns a descriptor.
|
||||
func kqueue() (kq int, err error) {
|
||||
kq, err = unix.Kqueue()
|
||||
if kq == -1 {
|
||||
return kq, err
|
||||
}
|
||||
return kq, nil
|
||||
}
|
||||
|
||||
// register events with the queue
|
||||
func register(kq int, fds []int, flags int, fflags uint32) error {
|
||||
changes := make([]unix.Kevent_t, len(fds))
|
||||
|
||||
for i, fd := range fds {
|
||||
// SetKevent converts int to the platform-specific types:
|
||||
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags)
|
||||
changes[i].Fflags = fflags
|
||||
}
|
||||
|
||||
// register the events
|
||||
success, err := unix.Kevent(kq, changes, nil, nil)
|
||||
if success == -1 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// read retrieves pending events, or waits until an event occurs.
|
||||
// A timeout of nil blocks indefinitely, while 0 polls the queue.
|
||||
func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) {
|
||||
n, err := unix.Kevent(kq, nil, events, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events[0:n], nil
|
||||
}
|
||||
|
||||
// durationToTimespec prepares a timeout value
|
||||
func durationToTimespec(d time.Duration) unix.Timespec {
|
||||
return unix.NsecToTimespec(d.Nanoseconds())
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd openbsd netbsd dragonfly
|
||||
|
||||
package fsnotify
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin
|
||||
|
||||
package fsnotify
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// note: this constant is not defined on BSD
|
||||
const openMode = unix.O_EVTONLY | unix.O_CLOEXEC
|
|
@ -0,0 +1,561 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package fsnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Watcher watches a set of files, delivering events to a channel.
|
||||
type Watcher struct {
|
||||
Events chan Event
|
||||
Errors chan error
|
||||
isClosed bool // Set to true when Close() is first called
|
||||
mu sync.Mutex // Map access
|
||||
port syscall.Handle // Handle to completion port
|
||||
watches watchMap // Map of watches (key: i-number)
|
||||
input chan *input // Inputs to the reader are sent on this channel
|
||||
quit chan chan<- error
|
||||
}
|
||||
|
||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
||||
func NewWatcher() (*Watcher, error) {
|
||||
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
|
||||
if e != nil {
|
||||
return nil, os.NewSyscallError("CreateIoCompletionPort", e)
|
||||
}
|
||||
w := &Watcher{
|
||||
port: port,
|
||||
watches: make(watchMap),
|
||||
input: make(chan *input, 1),
|
||||
Events: make(chan Event, 50),
|
||||
Errors: make(chan error),
|
||||
quit: make(chan chan<- error, 1),
|
||||
}
|
||||
go w.readEvents()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Close removes all watches and closes the events channel.
|
||||
func (w *Watcher) Close() error {
|
||||
if w.isClosed {
|
||||
return nil
|
||||
}
|
||||
w.isClosed = true
|
||||
|
||||
// Send "quit" message to the reader goroutine
|
||||
ch := make(chan error)
|
||||
w.quit <- ch
|
||||
if err := w.wakeupReader(); err != nil {
|
||||
return err
|
||||
}
|
||||
return <-ch
|
||||
}
|
||||
|
||||
// Add starts watching the named file or directory (non-recursively).
|
||||
func (w *Watcher) Add(name string) error {
|
||||
if w.isClosed {
|
||||
return errors.New("watcher already closed")
|
||||
}
|
||||
in := &input{
|
||||
op: opAddWatch,
|
||||
path: filepath.Clean(name),
|
||||
flags: sysFSALLEVENTS,
|
||||
reply: make(chan error),
|
||||
}
|
||||
w.input <- in
|
||||
if err := w.wakeupReader(); err != nil {
|
||||
return err
|
||||
}
|
||||
return <-in.reply
|
||||
}
|
||||
|
||||
// Remove stops watching the the named file or directory (non-recursively).
|
||||
func (w *Watcher) Remove(name string) error {
|
||||
in := &input{
|
||||
op: opRemoveWatch,
|
||||
path: filepath.Clean(name),
|
||||
reply: make(chan error),
|
||||
}
|
||||
w.input <- in
|
||||
if err := w.wakeupReader(); err != nil {
|
||||
return err
|
||||
}
|
||||
return <-in.reply
|
||||
}
|
||||
|
||||
const (
|
||||
// Options for AddWatch
|
||||
sysFSONESHOT = 0x80000000
|
||||
sysFSONLYDIR = 0x1000000
|
||||
|
||||
// Events
|
||||
sysFSACCESS = 0x1
|
||||
sysFSALLEVENTS = 0xfff
|
||||
sysFSATTRIB = 0x4
|
||||
sysFSCLOSE = 0x18
|
||||
sysFSCREATE = 0x100
|
||||
sysFSDELETE = 0x200
|
||||
sysFSDELETESELF = 0x400
|
||||
sysFSMODIFY = 0x2
|
||||
sysFSMOVE = 0xc0
|
||||
sysFSMOVEDFROM = 0x40
|
||||
sysFSMOVEDTO = 0x80
|
||||
sysFSMOVESELF = 0x800
|
||||
|
||||
// Special events
|
||||
sysFSIGNORED = 0x8000
|
||||
sysFSQOVERFLOW = 0x4000
|
||||
)
|
||||
|
||||
func newEvent(name string, mask uint32) Event {
|
||||
e := Event{Name: name}
|
||||
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
|
||||
e.Op |= Create
|
||||
}
|
||||
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
|
||||
e.Op |= Remove
|
||||
}
|
||||
if mask&sysFSMODIFY == sysFSMODIFY {
|
||||
e.Op |= Write
|
||||
}
|
||||
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
|
||||
e.Op |= Rename
|
||||
}
|
||||
if mask&sysFSATTRIB == sysFSATTRIB {
|
||||
e.Op |= Chmod
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
const (
|
||||
opAddWatch = iota
|
||||
opRemoveWatch
|
||||
)
|
||||
|
||||
const (
|
||||
provisional uint64 = 1 << (32 + iota)
|
||||
)
|
||||
|
||||
type input struct {
|
||||
op int
|
||||
path string
|
||||
flags uint32
|
||||
reply chan error
|
||||
}
|
||||
|
||||
type inode struct {
|
||||
handle syscall.Handle
|
||||
volume uint32
|
||||
index uint64
|
||||
}
|
||||
|
||||
type watch struct {
|
||||
ov syscall.Overlapped
|
||||
ino *inode // i-number
|
||||
path string // Directory path
|
||||
mask uint64 // Directory itself is being watched with these notify flags
|
||||
names map[string]uint64 // Map of names being watched and their notify flags
|
||||
rename string // Remembers the old name while renaming a file
|
||||
buf [4096]byte
|
||||
}
|
||||
|
||||
type indexMap map[uint64]*watch
|
||||
type watchMap map[uint32]indexMap
|
||||
|
||||
func (w *Watcher) wakeupReader() error {
|
||||
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil)
|
||||
if e != nil {
|
||||
return os.NewSyscallError("PostQueuedCompletionStatus", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDir(pathname string) (dir string, err error) {
|
||||
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname))
|
||||
if e != nil {
|
||||
return "", os.NewSyscallError("GetFileAttributes", e)
|
||||
}
|
||||
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||
dir = pathname
|
||||
} else {
|
||||
dir, _ = filepath.Split(pathname)
|
||||
dir = filepath.Clean(dir)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getIno(path string) (ino *inode, err error) {
|
||||
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path),
|
||||
syscall.FILE_LIST_DIRECTORY,
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
|
||||
nil, syscall.OPEN_EXISTING,
|
||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0)
|
||||
if e != nil {
|
||||
return nil, os.NewSyscallError("CreateFile", e)
|
||||
}
|
||||
var fi syscall.ByHandleFileInformation
|
||||
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil {
|
||||
syscall.CloseHandle(h)
|
||||
return nil, os.NewSyscallError("GetFileInformationByHandle", e)
|
||||
}
|
||||
ino = &inode{
|
||||
handle: h,
|
||||
volume: fi.VolumeSerialNumber,
|
||||
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
|
||||
}
|
||||
return ino, nil
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (m watchMap) get(ino *inode) *watch {
|
||||
if i := m[ino.volume]; i != nil {
|
||||
return i[ino.index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (m watchMap) set(ino *inode, watch *watch) {
|
||||
i := m[ino.volume]
|
||||
if i == nil {
|
||||
i = make(indexMap)
|
||||
m[ino.volume] = i
|
||||
}
|
||||
i[ino.index] = watch
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) addWatch(pathname string, flags uint64) error {
|
||||
dir, err := getDir(pathname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flags&sysFSONLYDIR != 0 && pathname != dir {
|
||||
return nil
|
||||
}
|
||||
ino, err := getIno(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.mu.Lock()
|
||||
watchEntry := w.watches.get(ino)
|
||||
w.mu.Unlock()
|
||||
if watchEntry == nil {
|
||||
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil {
|
||||
syscall.CloseHandle(ino.handle)
|
||||
return os.NewSyscallError("CreateIoCompletionPort", e)
|
||||
}
|
||||
watchEntry = &watch{
|
||||
ino: ino,
|
||||
path: dir,
|
||||
names: make(map[string]uint64),
|
||||
}
|
||||
w.mu.Lock()
|
||||
w.watches.set(ino, watchEntry)
|
||||
w.mu.Unlock()
|
||||
flags |= provisional
|
||||
} else {
|
||||
syscall.CloseHandle(ino.handle)
|
||||
}
|
||||
if pathname == dir {
|
||||
watchEntry.mask |= flags
|
||||
} else {
|
||||
watchEntry.names[filepath.Base(pathname)] |= flags
|
||||
}
|
||||
if err = w.startRead(watchEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
if pathname == dir {
|
||||
watchEntry.mask &= ^provisional
|
||||
} else {
|
||||
watchEntry.names[filepath.Base(pathname)] &= ^provisional
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) remWatch(pathname string) error {
|
||||
dir, err := getDir(pathname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ino, err := getIno(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.mu.Lock()
|
||||
watch := w.watches.get(ino)
|
||||
w.mu.Unlock()
|
||||
if watch == nil {
|
||||
return fmt.Errorf("can't remove non-existent watch for: %s", pathname)
|
||||
}
|
||||
if pathname == dir {
|
||||
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
|
||||
watch.mask = 0
|
||||
} else {
|
||||
name := filepath.Base(pathname)
|
||||
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
|
||||
delete(watch.names, name)
|
||||
}
|
||||
return w.startRead(watch)
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) deleteWatch(watch *watch) {
|
||||
for name, mask := range watch.names {
|
||||
if mask&provisional == 0 {
|
||||
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
|
||||
}
|
||||
delete(watch.names, name)
|
||||
}
|
||||
if watch.mask != 0 {
|
||||
if watch.mask&provisional == 0 {
|
||||
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
|
||||
}
|
||||
watch.mask = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) startRead(watch *watch) error {
|
||||
if e := syscall.CancelIo(watch.ino.handle); e != nil {
|
||||
w.Errors <- os.NewSyscallError("CancelIo", e)
|
||||
w.deleteWatch(watch)
|
||||
}
|
||||
mask := toWindowsFlags(watch.mask)
|
||||
for _, m := range watch.names {
|
||||
mask |= toWindowsFlags(m)
|
||||
}
|
||||
if mask == 0 {
|
||||
if e := syscall.CloseHandle(watch.ino.handle); e != nil {
|
||||
w.Errors <- os.NewSyscallError("CloseHandle", e)
|
||||
}
|
||||
w.mu.Lock()
|
||||
delete(w.watches[watch.ino.volume], watch.ino.index)
|
||||
w.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
|
||||
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
|
||||
if e != nil {
|
||||
err := os.NewSyscallError("ReadDirectoryChanges", e)
|
||||
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
|
||||
// Watched directory was probably removed
|
||||
if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) {
|
||||
if watch.mask&sysFSONESHOT != 0 {
|
||||
watch.mask = 0
|
||||
}
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
w.deleteWatch(watch)
|
||||
w.startRead(watch)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readEvents reads from the I/O completion port, converts the
|
||||
// received events into Event objects and sends them via the Events channel.
|
||||
// Entry point to the I/O thread.
|
||||
func (w *Watcher) readEvents() {
|
||||
var (
|
||||
n, key uint32
|
||||
ov *syscall.Overlapped
|
||||
)
|
||||
runtime.LockOSThread()
|
||||
|
||||
for {
|
||||
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
|
||||
watch := (*watch)(unsafe.Pointer(ov))
|
||||
|
||||
if watch == nil {
|
||||
select {
|
||||
case ch := <-w.quit:
|
||||
w.mu.Lock()
|
||||
var indexes []indexMap
|
||||
for _, index := range w.watches {
|
||||
indexes = append(indexes, index)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
for _, index := range indexes {
|
||||
for _, watch := range index {
|
||||
w.deleteWatch(watch)
|
||||
w.startRead(watch)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if e := syscall.CloseHandle(w.port); e != nil {
|
||||
err = os.NewSyscallError("CloseHandle", e)
|
||||
}
|
||||
close(w.Events)
|
||||
close(w.Errors)
|
||||
ch <- err
|
||||
return
|
||||
case in := <-w.input:
|
||||
switch in.op {
|
||||
case opAddWatch:
|
||||
in.reply <- w.addWatch(in.path, uint64(in.flags))
|
||||
case opRemoveWatch:
|
||||
in.reply <- w.remWatch(in.path)
|
||||
}
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch e {
|
||||
case syscall.ERROR_MORE_DATA:
|
||||
if watch == nil {
|
||||
w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")
|
||||
} else {
|
||||
// The i/o succeeded but the buffer is full.
|
||||
// In theory we should be building up a full packet.
|
||||
// In practice we can get away with just carrying on.
|
||||
n = uint32(unsafe.Sizeof(watch.buf))
|
||||
}
|
||||
case syscall.ERROR_ACCESS_DENIED:
|
||||
// Watched directory was probably removed
|
||||
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
|
||||
w.deleteWatch(watch)
|
||||
w.startRead(watch)
|
||||
continue
|
||||
case syscall.ERROR_OPERATION_ABORTED:
|
||||
// CancelIo was called on this handle
|
||||
continue
|
||||
default:
|
||||
w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e)
|
||||
continue
|
||||
case nil:
|
||||
}
|
||||
|
||||
var offset uint32
|
||||
for {
|
||||
if n == 0 {
|
||||
w.Events <- newEvent("", sysFSQOVERFLOW)
|
||||
w.Errors <- errors.New("short read in readEvents()")
|
||||
break
|
||||
}
|
||||
|
||||
// Point "raw" to the event in the buffer
|
||||
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
|
||||
buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName))
|
||||
name := syscall.UTF16ToString(buf[:raw.FileNameLength/2])
|
||||
fullname := filepath.Join(watch.path, name)
|
||||
|
||||
var mask uint64
|
||||
switch raw.Action {
|
||||
case syscall.FILE_ACTION_REMOVED:
|
||||
mask = sysFSDELETESELF
|
||||
case syscall.FILE_ACTION_MODIFIED:
|
||||
mask = sysFSMODIFY
|
||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
|
||||
watch.rename = name
|
||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
|
||||
if watch.names[watch.rename] != 0 {
|
||||
watch.names[name] |= watch.names[watch.rename]
|
||||
delete(watch.names, watch.rename)
|
||||
mask = sysFSMOVESELF
|
||||
}
|
||||
}
|
||||
|
||||
sendNameEvent := func() {
|
||||
if w.sendEvent(fullname, watch.names[name]&mask) {
|
||||
if watch.names[name]&sysFSONESHOT != 0 {
|
||||
delete(watch.names, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME {
|
||||
sendNameEvent()
|
||||
}
|
||||
if raw.Action == syscall.FILE_ACTION_REMOVED {
|
||||
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
|
||||
delete(watch.names, name)
|
||||
}
|
||||
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) {
|
||||
if watch.mask&sysFSONESHOT != 0 {
|
||||
watch.mask = 0
|
||||
}
|
||||
}
|
||||
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME {
|
||||
fullname = filepath.Join(watch.path, watch.rename)
|
||||
sendNameEvent()
|
||||
}
|
||||
|
||||
// Move to the next event in the buffer
|
||||
if raw.NextEntryOffset == 0 {
|
||||
break
|
||||
}
|
||||
offset += raw.NextEntryOffset
|
||||
|
||||
// Error!
|
||||
if offset >= n {
|
||||
w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.startRead(watch); err != nil {
|
||||
w.Errors <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) sendEvent(name string, mask uint64) bool {
|
||||
if mask == 0 {
|
||||
return false
|
||||
}
|
||||
event := newEvent(name, uint32(mask))
|
||||
select {
|
||||
case ch := <-w.quit:
|
||||
w.quit <- ch
|
||||
case w.Events <- event:
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toWindowsFlags(mask uint64) uint32 {
|
||||
var m uint32
|
||||
if mask&sysFSACCESS != 0 {
|
||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS
|
||||
}
|
||||
if mask&sysFSMODIFY != 0 {
|
||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||
}
|
||||
if mask&sysFSATTRIB != 0 {
|
||||
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES
|
||||
}
|
||||
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
|
||||
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func toFSnotifyFlags(action uint32) uint64 {
|
||||
switch action {
|
||||
case syscall.FILE_ACTION_ADDED:
|
||||
return sysFSCREATE
|
||||
case syscall.FILE_ACTION_REMOVED:
|
||||
return sysFSDELETE
|
||||
case syscall.FILE_ACTION_MODIFIED:
|
||||
return sysFSMODIFY
|
||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
|
||||
return sysFSMOVEDFROM
|
||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
|
||||
return sysFSMOVEDTO
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# procmeminfo
|
||||
|
||||
[![GoDoc](http://godoc.org/github.com/guillermo/go.procmeminfo?status.png)](http://godoc.org/github.com/guillermo/go.procmeminfo)
|
||||
|
||||
Package procmeminfo provides an interface for /proc/meminfo
|
||||
|
||||
```golang
|
||||
import "github.com/guillermo/go.procmeminfo"
|
||||
meminfo := &procmeminfo.MemInfo{}
|
||||
meminfo.Update()
|
||||
|
||||
(*meminfo)['Cached'] // Get cached memory
|
||||
(*meminfo)['Buffers'] // Get buffers size
|
||||
(*meminfo)['...'] // Any field in /proc/meminfo
|
||||
|
||||
meminfo.Total() // Total memory size in bytes
|
||||
meminfo.Free() // Free Memory (Free + Cached + Buffers)
|
||||
meminfo.Used() // Total - Used
|
||||
```
|
||||
|
||||
|
||||
## Docs
|
||||
|
||||
Visit: http://godoc.org/github.com/guillermo/go.procmeminfo
|
||||
|
||||
## LICENSE
|
||||
|
||||
BSD
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
Package procmeminfo provides an interface for /proc/meminfo
|
||||
|
||||
import "github.com/guillermo/go.procmeminfo"
|
||||
|
||||
meminfo := &procmeminfo.MemInfo{}
|
||||
meminfo.Update()
|
||||
|
||||
Once the info was updated you can access like a normal map[string]float64
|
||||
|
||||
v := (*meminfo)["MemTotal"] // 1809379328 (1766972 * 1024)
|
||||
|
||||
It also implement some handy methods, like:
|
||||
|
||||
meminfo.Total() // (*meminfo)["MemTotal"]
|
||||
meminfo.Free() // MemFree + Buffers + Cached
|
||||
meminfo.Used() // Total - Free
|
||||
|
||||
|
||||
Return all the values in units, so while you get this from cat /proc/meminfo
|
||||
|
||||
MemTotal: 1766972 kB
|
||||
MemFree: 115752 kB
|
||||
Buffers: 3172 kB
|
||||
Cached: 182552 kB
|
||||
SwapCached: 83572 kB
|
||||
Active: 1055284 kB
|
||||
Inactive: 382872 kB
|
||||
Active(anon): 932712 kB
|
||||
Inactive(anon): 329508 kB
|
||||
Active(file): 122572 kB
|
||||
Inactive(file): 53364 kB
|
||||
Unevictable: 10640 kB
|
||||
Mlocked: 10640 kB
|
||||
SwapTotal: 1808668 kB
|
||||
SwapFree: 1205672 kB
|
||||
Dirty: 100 kB
|
||||
Writeback: 0 kB
|
||||
AnonPages: 1214740 kB
|
||||
Mapped: 115636 kB
|
||||
Shmem: 4840 kB
|
||||
Slab: 77412 kB
|
||||
SReclaimable: 34344 kB
|
||||
SUnreclaim: 43068 kB
|
||||
KernelStack: 4328 kB
|
||||
PageTables: 39428 kB
|
||||
NFS_Unstable: 0 kB
|
||||
Bounce: 0 kB
|
||||
WritebackTmp: 0 kB
|
||||
CommitLimit: 2692152 kB
|
||||
Committed_AS: 5448372 kB
|
||||
VmallocTotal: 34359738367 kB
|
||||
VmallocUsed: 106636 kB
|
||||
VmallocChunk: 34359618556 kB
|
||||
HardwareCorrupted: 0 kB
|
||||
AnonHugePages: 0 kB
|
||||
HugePages_Total: 0
|
||||
HugePages_Free: 0
|
||||
HugePages_Rsvd: 0
|
||||
HugePages_Surp: 0
|
||||
Hugepagesize: 2048 kB
|
||||
DirectMap4k: 216236 kB
|
||||
DirectMap2M: 1593344 kB
|
||||
|
||||
|
||||
All the kB values are multiply by 1024
|
||||
|
||||
This is the info extracted from the man page of proc:
|
||||
|
||||
|
||||
/proc/meminfo
|
||||
This file reports statistics about memory usage on the system. It is used by free(1) to report the amount of free and used memory (both physical and swap) on the system as well as the shared memory and buffers used by the kernel. Each line of the file consists of a parameter name, followed by a colon, the value of
|
||||
the parameter, and an option unit of measurement (e.g., "kB"). The list below describes the parameter names and the format specifier required to read the field value. Except as noted below, all of the fields have been present since at least Linux 2.6.0. Some fileds are displayed only if the kernel was configured
|
||||
with various options; those dependencies are noted in the list.
|
||||
|
||||
MemTotal %lu
|
||||
Total usable RAM (i.e. physical RAM minus a few reserved bits and the kernel binary code).
|
||||
|
||||
MemFree %lu
|
||||
The sum of LowFree+HighFree.
|
||||
|
||||
Buffers %lu
|
||||
Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).
|
||||
|
||||
Cached %lu
|
||||
In-memory cache for files read from the disk (the page cache). Doesn't include SwapCached.
|
||||
|
||||
SwapCached %lu
|
||||
Memory that once was swapped out, is swapped back in but still also is in the swap file. (If memory pressure is high, these pages don't need to be swapped out again because they are already in the swap file. This saves I/O.)
|
||||
|
||||
Active %lu
|
||||
Memory that has been used more recently and usually not reclaimed unless absolutely necessary.
|
||||
|
||||
Inactive %lu
|
||||
Memory which has been less recently used. It is more eligible to be reclaimed for other purposes.
|
||||
|
||||
Active(anon) %lu (since Linux 2.6.28)
|
||||
[To be documented.]
|
||||
|
||||
Inactive(anon) %lu (since Linux 2.6.28)
|
||||
[To be documented.]
|
||||
|
||||
Active(file) %lu (since Linux 2.6.28)
|
||||
[To be documented.]
|
||||
|
||||
Inactive(file) %lu (since Linux 2.6.28)
|
||||
[To be documented.]
|
||||
|
||||
Unevictable %lu (since Linux 2.6.28)
|
||||
(From Linux 2.6.28 to 2.6.30, CONFIG_UNEVICTABLE_LRU was required.) [To be documented.]
|
||||
|
||||
Mlocked %lu (since Linux 2.6.28)
|
||||
(From Linux 2.6.28 to 2.6.30, CONFIG_UNEVICTABLE_LRU was required.) [To be documented.]
|
||||
|
||||
HighTotal %lu
|
||||
(Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.) Total amount of highmem. Highmem is all memory above ~860MB of physical memory. Highmem areas are for use by user-space programs, or for the page cache. The kernel must use tricks to access this memory, making it slower to access than lowmem.
|
||||
|
||||
HighFree %lu
|
||||
(Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.) Amount of free highmem.
|
||||
|
||||
LowTotal %lu
|
||||
(Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.) Total amount of lowmem. Lowmem is memory which can be used for everything that highmem can be used for, but it is also available for the kernel's use for its own data structures. Among many other things, it is where everything from Slab is allo‐
|
||||
cated. Bad things happen when you're out of lowmem.
|
||||
|
||||
LowFree %lu
|
||||
(Starting with Linux 2.6.19, CONFIG_HIGHMEM is required.) Amount of free lowmem.
|
||||
|
||||
MmapCopy %lu (since Linux 2.6.29)
|
||||
(CONFIG_MMU is required.) [To be documented.]
|
||||
|
||||
SwapTotal %lu
|
||||
Total amount of swap space available.
|
||||
|
||||
SwapFree %lu
|
||||
Amount of swap space that is currently unused.
|
||||
|
||||
Dirty %lu
|
||||
Memory which is waiting to get written back to the disk.
|
||||
|
||||
Writeback %lu
|
||||
Memory which is actively being written back to the disk.
|
||||
|
||||
AnonPages %lu (since Linux 2.6.18)
|
||||
Non-file backed pages mapped into user-space page tables.
|
||||
|
||||
Mapped %lu
|
||||
Files which have been mmaped, such as libraries.
|
||||
|
||||
Shmem %lu (since Linux 2.6.32)
|
||||
[To be documented.]
|
||||
|
||||
Slab %lu
|
||||
In-kernel data structures cache.
|
||||
|
||||
SReclaimable %lu (since Linux 2.6.19)
|
||||
Part of Slab, that might be reclaimed, such as caches.
|
||||
|
||||
SUnreclaim %lu (since Linux 2.6.19)
|
||||
Part of Slab, that cannot be reclaimed on memory pressure.
|
||||
|
||||
KernelStack %lu (since Linux 2.6.32)
|
||||
Amount of memory allocated to kernel stacks.
|
||||
|
||||
PageTables %lu (since Linux 2.6.18)
|
||||
Amount of memory dedicated to the lowest level of page tables.
|
||||
|
||||
Quicklists %lu (since Linux 2.6.27)
|
||||
(CONFIG_QUICKLIST is required.) [To be documented.]
|
||||
|
||||
NFS_Unstable %lu (since Linux 2.6.18)
|
||||
NFS pages sent to the server, but not yet committed to stable storage.
|
||||
|
||||
Bounce %lu (since Linux 2.6.18)
|
||||
Memory used for block device "bounce buffers".
|
||||
|
||||
WritebackTmp %lu (since Linux 2.6.26)
|
||||
Memory used by FUSE for temporary writeback buffers.
|
||||
|
||||
CommitLimit %lu (since Linux 2.6.10)
|
||||
Based on the overcommit ratio ('vm.overcommit_ratio'), this is the total amount of memory currently available to be allocated on the system. This limit is adhered to only if strict overcommit accounting is enabled (mode 2 in /proc/sys/vm/overcommit_ratio). The CommitLimit is calculated using the following
|
||||
formula:
|
||||
|
||||
CommitLimit = (overcommit_ratio * Physical RAM) + Swap
|
||||
|
||||
For example, on a system with 1GB of physical RAM and 7GB of swap with a overcommit_ratio of 30, this formula yields a CommitLimit of 7.3GB. For more details, see the memory overcommit documentation in the kernel source file Documentation/vm/overcommit-accounting.
|
||||
|
||||
Committed_AS %lu
|
||||
The amount of memory presently allocated on the system. The committed memory is a sum of all of the memory which has been allocated by processes, even if it has not been "used" by them as of yet. A process which allocates 1GB of memory (using malloc(3) or similar), but touches only 300MB of that memory will
|
||||
show up as using only 300MB of memory even if it has the address space allocated for the entire 1GB. This 1GB is memory which has been "committed" to by the VM and can be used at any time by the allocating application. With strict overcommit enabled on the system (mode 2 /proc/sys/vm/overcommit_memory),
|
||||
allocations which would exceed the CommitLimit (detailed above) will not be permitted. This is useful if one needs to guarantee that processes will not fail due to lack of memory once that memory has been successfully allocated.
|
||||
|
||||
VmallocTotal %lu
|
||||
Total size of vmalloc memory area.
|
||||
|
||||
VmallocUsed %lu
|
||||
Amount of vmalloc area which is used.
|
||||
|
||||
VmallocChunk %lu
|
||||
Largest contiguous block of vmalloc area which is free.
|
||||
|
||||
HardwareCorrupted %lu (since Linux 2.6.32)
|
||||
(CONFIG_MEMORY_FAILURE is required.) [To be documented.]
|
||||
|
||||
AnonHugePages %lu (since Linux 2.6.38)
|
||||
(CONFIG_TRANSPARENT_HUGEPAGE is required.) Non-file backed huge pages mapped into user-space page tables.
|
||||
|
||||
HugePages_Total %lu
|
||||
(CONFIG_HUGETLB_PAGE is required.) The size of the pool of huge pages.
|
||||
|
||||
HugePages_Free %lu
|
||||
(CONFIG_HUGETLB_PAGE is required.) The number of huge pages in the pool that are not yet allocated.
|
||||
|
||||
HugePages_Rsvd %lu (since Linux 2.6.17)
|
||||
(CONFIG_HUGETLB_PAGE is required.) This is the number of huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made. These reserved huge pages guarantee that an application will be able to allocate a huge page from the pool of huge pages at fault time.
|
||||
|
||||
HugePages_Surp %lu (since Linux 2.6.24)
|
||||
(CONFIG_HUGETLB_PAGE is required.) This is the number of huge pages in the pool above the value in /proc/sys/vm/nr_hugepages. The maximum number of surplus huge pages is controlled by /proc/sys/vm/nr_overcommit_hugepages.
|
||||
|
||||
Hugepagesize %lu
|
||||
(CONFIG_HUGETLB_PAGE is required.) The size of huge pages.
|
||||
*/
|
||||
package procmeminfo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MemInfo is a map[string]uint64 with all the values found in /proc/meminfo
|
||||
type MemInfo map[string]uint64
|
||||
|
||||
// Update s with current values, usign the pid stored in the Stat
|
||||
func (m *MemInfo) Update() error {
|
||||
var err error
|
||||
|
||||
path := filepath.Join("/proc/meminfo")
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
|
||||
n := strings.Index(text, ":")
|
||||
if n == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := text[:n] // metric
|
||||
data := strings.Split(strings.Trim(text[(n+1):], " "), " ")
|
||||
if len(data) == 1 {
|
||||
value, err := strconv.ParseUint(data[0], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
(*m)[key] = value
|
||||
} else if len(data) == 2 {
|
||||
if data[1] == "kB" {
|
||||
value, err := strconv.ParseUint(data[0], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
(*m)[key] = value * 1024
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Total return the size of the memory in bytes.
|
||||
// It is an alias of (*m)["MemInfo"]
|
||||
func (m *MemInfo) Total() uint64 {
|
||||
return (*m)["MemTotal"]
|
||||
}
|
||||
|
||||
// Available return the available memory following this formula:
|
||||
//
|
||||
// Available = Free + Buffers + Cached
|
||||
func (m *MemInfo) Available() uint64 {
|
||||
d := *m
|
||||
return d["MemFree"] + d["Buffers"] + d["Cached"]
|
||||
}
|
||||
|
||||
// Used is a generic way of reporting used memory. It follows the next formula:
|
||||
//
|
||||
// Used = Total - Available
|
||||
func (m *MemInfo) Used() uint64 {
|
||||
return m.Total() - m.Available()
|
||||
}
|
||||
|
||||
// Swap returns the % of swap used
|
||||
func (m *MemInfo) Swap() int {
|
||||
total := (*m)["SwapTotal"]
|
||||
free := (*m)["SwapFree"]
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((100 * (total - free)) / total)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
.env
|
|
@ -0,0 +1,17 @@
|
|||
go_import_path: github.com/knadh/koanf
|
||||
|
||||
language: go
|
||||
|
||||
env:
|
||||
global:
|
||||
- GO111MODULE="on"
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
|
||||
install: go get -v ./...
|
||||
script: go test -v -cover ./...
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License
|
||||
|
||||
Copyright (c) 2019, Kailash Nadh. https://github.com/knadh
|
||||
|
||||
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.
|
|
@ -0,0 +1,594 @@
|
|||
![koanf](https://user-images.githubusercontent.com/547147/72681838-6981dd00-3aed-11ea-8f5d-310816c70c08.png)
|
||||
|
||||
**koanf** (pronounced _conf_; a play on the Japanese _Koan_) is a library for reading configuration from different sources in different formats in Go applications. It is a cleaner, lighter [alternative to spf13/viper](#alternative-to-viper) with better abstractions and extensibility and fewer dependencies.
|
||||
|
||||
koanf comes with built in support for reading configuration from files, command line flags, and environment variables, and can parse JSON, YAML, TOML, and Hashicorp HCL.
|
||||
|
||||
[![Build Status](https://travis-ci.com/knadh/koanf.svg?branch=master)](https://travis-ci.com/knadh/koanf) [![GoDoc](https://godoc.org/github.com/knadh/koanf?status.svg)](https://godoc.org/github.com/knadh/koanf)
|
||||
|
||||
### Installation
|
||||
|
||||
`go get -u github.com/knadh/koanf`
|
||||
|
||||
### Contents
|
||||
|
||||
- [Concepts](#concepts)
|
||||
- [Reading config from files](#reading-config-from-files)
|
||||
- [Watching files for changes](#watching-files-for-changes)
|
||||
- [Reading from command line](#reading-from-command-line)
|
||||
- [Reading environment variables](#reading-environment-variables)
|
||||
- [Reading raw bytes](#reading-raw-bytes)
|
||||
- [Unmarshalling and marshalling](#unmarshalling-and-marshalling)
|
||||
- [Unmarshalling with flat paths](#unmarshalling-with-flat-paths)
|
||||
- [Setting default values](#setting-default-values)
|
||||
- [Order of merge and key case senstivity](#order-of-merge-and-key-case-senstivity)
|
||||
- [Custom Providers and Parsers](#custom-providers-and-parsers)
|
||||
- [API](#api)
|
||||
|
||||
### Concepts
|
||||
|
||||
- `koanf.Provider` is a generic interface that provides configuration, for example, from files, environment variables, HTTP sources, or anywhere. The configuration can either be raw bytes that a parser can parse, or it can be a nested map[string]interface{} that can be directly loaded.
|
||||
- `koanf.Parser` is a generic interface that takes raw bytes, parses, and returns a nested map[string]interface{} representation. For example, JSON and YAML parsers.
|
||||
- Once loaded into koanf, configuration are values queried by a delimited key path syntax. eg: `app.server.port`. Any delimiter can be chosen.
|
||||
- Configuration from multiple sources can be loaded and merged into a koanf instance, for example, load from a file first and override certain values with flags from the command line.
|
||||
|
||||
With these two interface implementations, koanf can obtain a configuration from multiple sources and parse any format and make it available to an application.
|
||||
|
||||
### Reading config from files
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Load JSON config.
|
||||
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Load YAML config and merge into the previously loaded config (because we can).
|
||||
k.Load(file.Provider("mock/mock.yaml"), yaml.Parser())
|
||||
|
||||
fmt.Println("parent's name is = ", k.String("parent1.name"))
|
||||
fmt.Println("parent's ID is = ", k.Int("parent1.id"))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Watching files for changes
|
||||
The `koanf.Provider` interface has a `Watch(cb)` method that asks a provider
|
||||
to watch for changes and trigger the given callback that can live reload the
|
||||
configuration.
|
||||
|
||||
Currently, `file.Provider` supports this.
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Load JSON config.
|
||||
f := file.Provider("mock/mock.json")
|
||||
if err := k.Load(f, json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Load YAML config and merge into the previously loaded config (because we can).
|
||||
k.Load(file.Provider("mock/mock.yaml"), yaml.Parser())
|
||||
|
||||
fmt.Println("parent's name is = ", k.String("parent1.name"))
|
||||
fmt.Println("parent's ID is = ", k.Int("parent1.id"))
|
||||
|
||||
// Watch the file and get a callback on change. The callback can do whatever,
|
||||
// like re-load the configuration.
|
||||
// File provider always returns a nil `event`.
|
||||
f.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
log.Printf("watch error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("config changed. Reloading ...")
|
||||
k.Load(f, json.Parser())
|
||||
k.Print()
|
||||
})
|
||||
|
||||
// Block forever (and manually make a change to mock/mock.json) to
|
||||
// reload the config.
|
||||
log.Println("waiting forever. Try making a change to mock/mock.json to live reload")
|
||||
<-make(chan bool)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Reading from command line
|
||||
|
||||
The following example shows the use of `posflag.Provider`, a wrapper over [spf13/pflag](/spf13/pflag) library, an advanced commandline lib. For Go's built in `flag` package, use `basicflag.Provider`.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Use the POSIX compliant pflag lib instead of Go's flag lib.
|
||||
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
||||
f.Usage = func() {
|
||||
fmt.Println(f.FlagUsages())
|
||||
os.Exit(0)
|
||||
}
|
||||
// Path to one or more config files to load into koanf along with some config params.
|
||||
f.StringSlice("conf", []string{"mock/mock.toml"}, "path to one or more .toml config files")
|
||||
f.String("time", "2020-01-01", "a time string")
|
||||
f.String("type", "xxx", "type of the app")
|
||||
f.Parse(os.Args[1:])
|
||||
|
||||
// Load the config files provided in the commandline.
|
||||
cFiles, _ := f.GetStringSlice("conf")
|
||||
for _, c := range cFiles {
|
||||
if err := k.Load(file.Provider(c), toml.Parser()); err != nil {
|
||||
log.Fatalf("error loading file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// "time" and "type" may have been loaded from the config file, but
|
||||
// they can still be overridden with the values from the command line.
|
||||
// The bundled posflag.Provider takes a flagset from the spf13/pflag lib.
|
||||
// Passing the Koanf instance to posflag helps it deal with default command
|
||||
// line flag values that are not present in conf maps from previously loaded
|
||||
// providers.
|
||||
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("time is = ", k.String("time"))
|
||||
}
|
||||
```
|
||||
|
||||
### Reading environment variables
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Load JSON config.
|
||||
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Load environment variables and merge into the loaded config.
|
||||
// "MYVAR" is the prefix to filter the env vars by.
|
||||
// "." is the delimiter used to represent the key hierarchy in env vars.
|
||||
// The (optional, or can be nil) function can be used to transform
|
||||
// the env var names, for instance, to lowercase them.
|
||||
//
|
||||
// For example, env vars: MYVAR_TYPE and MYVAR_PARENT1_CHILD1_NAME
|
||||
// will be merged into the "type" and the nested "parent1.child1.name"
|
||||
// keys in the config file here as we lowercase the key,
|
||||
// replace `_` with `.` and strip the MYVAR_ prefix so that
|
||||
// only "parent1.child1.name" remains.
|
||||
k.Load(env.Provider("MYVAR_", ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(
|
||||
strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
|
||||
}), nil)
|
||||
|
||||
fmt.Println("name is = ", k.String("parent1.child1.name"))
|
||||
}
|
||||
```
|
||||
|
||||
### Reading from an S3 bucket
|
||||
|
||||
```go
|
||||
// Load JSON config from s3.
|
||||
if err := k.Load(s3.Provider(s3.Config{
|
||||
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
Region: os.Getenv("AWS_S3_REGION"),
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "dir/config.json",
|
||||
}), json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Reading raw bytes
|
||||
|
||||
The bundled `rawbytes` Provider can be used to read arbitrary bytes from a source, like a database or an HTTP call.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/rawbytes"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
b := []byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`)
|
||||
k.Load(rawbytes.Provider(b), json.Parser())
|
||||
fmt.Println("type is = ", k.String("parent1.child1.type"))
|
||||
}
|
||||
```
|
||||
|
||||
### Unmarshalling and marshalling
|
||||
`Parser`s can be used to unmarshal and scan the values in a Koanf instance into a struct based on the field tags, and to marshal a Koanf instance back into serialized bytes, for example, back to JSON or YAML, to write back to files.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
|
||||
var (
|
||||
k = koanf.New(".")
|
||||
parser = json.Parser()
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load JSON config.
|
||||
if err := k.Load(file.Provider("mock/mock.json"), parser); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Structure to unmarshal nested conf to.
|
||||
type childStruct struct {
|
||||
Name string `koanf:"name"`
|
||||
Type string `koanf:"type"`
|
||||
Empty map[string]string `koanf:"empty"`
|
||||
GrandChild struct {
|
||||
Ids []int `koanf:"ids"`
|
||||
On bool `koanf:"on"`
|
||||
} `koanf:"grandchild1"`
|
||||
}
|
||||
|
||||
var out childStruct
|
||||
|
||||
// Quick unmarshal.
|
||||
k.Unmarshal("parent1.child1", &out)
|
||||
fmt.Println(out)
|
||||
|
||||
// Unmarshal with advanced config.
|
||||
out = childStruct{}
|
||||
k.UnmarshalWithConf("parent1.child1", &out, koanf.UnmarshalConf{Tag: "koanf"})
|
||||
fmt.Println(out)
|
||||
|
||||
// Marshal the instance back to JSON.
|
||||
// The paser instance can be anything, eg: json.Paser(), yaml.Parser() etc.
|
||||
b, _ := k.Marshal(parser)
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
```
|
||||
|
||||
### Unmarshalling with flat paths
|
||||
|
||||
Sometimes it is necessary to unmarshal an assortment of keys from various nested structures into a flat target structure. This is possible with the `UnmarshalConf.FlatPaths` flag.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use . as the key path delimiter. This can be / or anything.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Load JSON config.
|
||||
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
type rootFlat struct {
|
||||
Type string `koanf:"type"`
|
||||
Empty map[string]string `koanf:"empty"`
|
||||
Parent1Name string `koanf:"parent1.name"`
|
||||
Parent1ID int `koanf:"parent1.id"`
|
||||
Parent1Child1Name string `koanf:"parent1.child1.name"`
|
||||
Parent1Child1Type string `koanf:"parent1.child1.type"`
|
||||
Parent1Child1Empty map[string]string `koanf:"parent1.child1.empty"`
|
||||
Parent1Child1Grandchild1IDs []int `koanf:"parent1.child1.grandchild1.ids"`
|
||||
Parent1Child1Grandchild1On bool `koanf:"parent1.child1.grandchild1.on"`
|
||||
}
|
||||
|
||||
// Unmarshal the whole root with FlatPaths: True.
|
||||
var o1 rootFlat
|
||||
k.UnmarshalWithConf("", &o1, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true})
|
||||
fmt.Println(o1)
|
||||
|
||||
// Unmarshal a child structure of "parent1".
|
||||
type subFlat struct {
|
||||
Name string `koanf:"name"`
|
||||
ID int `koanf:"id"`
|
||||
Child1Name string `koanf:"child1.name"`
|
||||
Child1Type string `koanf:"child1.type"`
|
||||
Child1Empty map[string]string `koanf:"child1.empty"`
|
||||
Child1Grandchild1IDs []int `koanf:"child1.grandchild1.ids"`
|
||||
Child1Grandchild1On bool `koanf:"child1.grandchild1.on"`
|
||||
}
|
||||
|
||||
var o2 subFlat
|
||||
k.UnmarshalWithConf("parent1", &o2, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true})
|
||||
fmt.Println(o2)
|
||||
}
|
||||
```
|
||||
|
||||
### Marshalling and writing config
|
||||
It is possible to marshal and serialize the conf map into TOML, YAML etc.
|
||||
|
||||
### Setting default values.
|
||||
|
||||
koanf does not provide any special functions to set default values but uses the Provider interface to enable it.
|
||||
|
||||
#### From a map
|
||||
|
||||
The bundled `confmap` provider takes a `map[string]interface{}` that can be loaded into a koanf instance.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
|
||||
var k = koanf.New(".")
|
||||
|
||||
func main() {
|
||||
// Load default values using the confmap provider.
|
||||
// We provide a flat map with the "." delimiter.
|
||||
// A nested map can be loaded by setting the delimiter to an empty string "".
|
||||
k.Load(confmap.Provider(map[string]interface{}{
|
||||
"parent1.name": "Default Name",
|
||||
"parent3.name": "New name here",
|
||||
}, "."), nil)
|
||||
|
||||
// Load JSON config on top of the default values.
|
||||
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Load YAML config and merge into the previously loaded config (because we can).
|
||||
k.Load(file.Provider("mock/mock.yaml"), yaml.Parser())
|
||||
|
||||
fmt.Println("parent's name is = ", k.String("parent1.name"))
|
||||
fmt.Println("parent's ID is = ", k.Int("parent1.id"))
|
||||
}
|
||||
```
|
||||
|
||||
#### From a struct
|
||||
|
||||
The bundled `structs` provider can be used to read data from a struct to load into a koanf instance.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/providers/structs"
|
||||
)
|
||||
|
||||
// Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character.
|
||||
var k = koanf.New(".")
|
||||
|
||||
type parentStruct struct {
|
||||
Name string `koanf:"name"`
|
||||
ID int `koanf:"id"`
|
||||
Child1 childStruct `koanf:"child1"`
|
||||
}
|
||||
type childStruct struct {
|
||||
Name string `koanf:"name"`
|
||||
Type string `koanf:"type"`
|
||||
Empty map[string]string `koanf:"empty"`
|
||||
Grandchild1 grandchildStruct `koanf:"grandchild1"`
|
||||
}
|
||||
type grandchildStruct struct {
|
||||
Ids []int `koanf:"ids"`
|
||||
On bool `koanf:"on"`
|
||||
}
|
||||
type sampleStruct struct {
|
||||
Type string `koanf:"type"`
|
||||
Empty map[string]string `koanf:"empty"`
|
||||
Parent1 parentStruct `koanf:"parent1"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load default values using the structs provider.
|
||||
// We provide a struct along with the struct tag `koanf` to the
|
||||
// provider.
|
||||
k.Load(structs.Provider(sampleStruct{
|
||||
Type: "json",
|
||||
Empty: make(map[string]string),
|
||||
Parent1: parentStruct{
|
||||
Name: "parent1",
|
||||
ID: 1234,
|
||||
Child1: childStruct{
|
||||
Name: "child1",
|
||||
Type: "json",
|
||||
Empty: make(map[string]string),
|
||||
Grandchild1: grandchildStruct{
|
||||
Ids: []int{1, 2, 3},
|
||||
On: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "koanf"), nil)
|
||||
|
||||
fmt.Printf("name is = `%s`\n", k.String("parent1.child1.name"))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Order of merge and key case senstivity
|
||||
|
||||
- Config keys are case-sensitive in koanf. For example, `app.server.port` and `APP.SERVER.port` are not the same.
|
||||
- koanf does not impose any ordering on loading config from various providers. Every successive `Load()` or `Load()` merges new config into existing config. That means it is possible to load environment variables first, then files on top of it, and then command line variables on top of it, or any such order.
|
||||
|
||||
### Custom Providers and Parsers
|
||||
|
||||
A Provider can provide a nested map[string]interface{} config that can be loaded into koanf with `koanf.Load()` or raw bytes that can be parsed with a Parser (loaded using `koanf.Load()`.
|
||||
|
||||
Writing Providers and Parsers are easy. See the bundled implementations in the `providers` and `parses` directory.
|
||||
|
||||
## API
|
||||
|
||||
### Bundled providers
|
||||
|
||||
| Package | Provider | Description |
|
||||
| ------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| providers/file | `file.Provider(filepath string)` | Reads a file and returns the raw bytes to be parsed. |
|
||||
| providers/basicflag | `basicflag.Provider(f *flag.FlagSet, delim string)` | Takes an stdlib `flag.FlagSet` |
|
||||
| providers/posflag | `posflag.Provider(f *pflag.FlagSet, delim string)` | Takes an `spft3/pflag.FlagSet` (advanced POSIX compatible flags with multiple types) and provides a nested config map based on delim. |
|
||||
| providers/env | `env.Provider(prefix, delim string, f func(s string) string)` | Takes an optional prefix to filter env variables by, an optional function that takes and returns a string to transform env variables, and returns a nested config map based on delim. |
|
||||
| providers/confmap | `confmap.Provider(mp map[string]interface{}, delim string)` | Takes a premade `map[string]interface{}` conf map. If delim is provided, the keys are assumed to be flattened, thus unflattened using delim. |
|
||||
| providers/structs | `structs.Provider(s interface{}, tag string)` | Takes a struct and struct tag. |
|
||||
| providers/s3 | `s3.Provider(s3.S3Config{})` | Takes a s3 config struct. |
|
||||
| providers/rawbytes | `rawbytes.Provider(b []byte)` | Takes a raw `[]byte` slice to be parsed with a koanf.Parser |
|
||||
|
||||
### Bundled parsers
|
||||
|
||||
| Package | Parser | Description |
|
||||
| ------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| parsers/json | `json.Parser()` | Parses JSON bytes into a nested map |
|
||||
| parsers/yaml | `yaml.Parser()` | Parses YAML bytes into a nested map |
|
||||
| parsers/toml | `toml.Parser()` | Parses TOML bytes into a nested map |
|
||||
| parsers/dotenv | `dotenv.Parser()` | Parses DotEnv bytes into a flat map |
|
||||
| parsers/hcl | `hcl.Parser(flattenSlices bool)` | Parses Hashicorp HCL bytes into a nested map. `flattenSlices` is recommended to be set to true. [Read more](https://github.com/hashicorp/hcl/issues/162). |
|
||||
### Instance functions
|
||||
|
||||
| Method | Description |
|
||||
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Load(p Provider, pa Parser) error` | Loads config from a Provider. If a koanf.Parser is provided, the config is assumed to be raw bytes that's then parsed with the Parser. |
|
||||
| `Keys() []string` | Returns the list of flattened key paths that can be used to access config values |
|
||||
| `KeyMap() map[string][]string` | Returns a map of all possible key path combinations possible in the loaded nested conf map |
|
||||
| `All() map[string]interface{}` | Returns a flat map of flattened key paths and their corresponding config values |
|
||||
| `Raw() map[string]interface{}` | Returns a copy of the raw nested conf map |
|
||||
| `Print()` | Prints a human readable copy of the flattened key paths and their values for debugging |
|
||||
| `Sprint()` | Returns a human readable copy of the flattened key paths and their values for debugging |
|
||||
| `Cut(path string) *Koanf` | Cuts the loaded nested conf map at the given path and returns a new Koanf instance with the children |
|
||||
| `Copy() *Koanf` | Returns a copy of the Koanf instance |
|
||||
| `Merge(*Koanf)` | Merges the config map of a Koanf instance into the current instance |
|
||||
| `MergeAt(in *Koanf, path string)` | Merges the config map of a Koanf instance into the current instance, at the given key path. |
|
||||
| `Unmarshal(path string, o interface{}) error` | Scans the given nested key path into a given struct (like json.Unmarshal) where fields are denoted by the `koanf` tag |
|
||||
| `UnmarshalWithConf(path string, o interface{}, c UnmarshalConf) error` | Like Unmarshal but with customizable options |
|
||||
|
||||
### Getter functions
|
||||
|
||||
| | |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `Get(path string) interface{}` | Returns the value for the given key path, and if it doesn’t exist, returns nil |
|
||||
| `Exists(path string) bool` | Returns true if the given key path exists in the conf map |
|
||||
| `Int64(path string) int64` | |
|
||||
| `Int64s(path string) []int64` | |
|
||||
| `Int64Map(path string) map[string]int64` | |
|
||||
| `Int(path string) int` | |
|
||||
| `Ints(path string) []int` | |
|
||||
| `IntMap(path string) map[string]int` | |
|
||||
| `Float64(path string) float64` | |
|
||||
| `Float64s(path string) []float64` | |
|
||||
| `Float64Map(path string) map[string]float64` | |
|
||||
| `Duration(path string) time.Duration` | Returns the time.Duration value of the given key path if it’s numeric (attempts a parse+convert if string) or a string representation like "3s". |
|
||||
| `Time(path, layout string) time.Time` | Parses the string value of the the given key path with the given layout format and returns time.Time. If the key path is numeric, treats it as a UNIX timestamp and returns its time.Time. |
|
||||
| `String(path string) string` | |
|
||||
| `Strings(path string) []string` | |
|
||||
| `StringMap(path string) map[string]string` | |
|
||||
| `StringsMap(path string) map[string][]string` | |
|
||||
| `Byte(path string) []byte` | |
|
||||
| `Bool(path string) bool` | |
|
||||
| `Bools(path string) []bool` | |
|
||||
| `BoolMap(path string) map[string]bool` | |
|
||||
| `MapKeys(path string) []string` | Returns the list of keys in any map |
|
||||
| `Slices(path string) []Koanf` | Returns `[]map[string]interface{}`, a slice of confmaps loaded into a slice of new Koanf instances. |
|
||||
|
||||
### Alternative to viper
|
||||
|
||||
koanf is a lightweight alternative to the popular [spf13/viper](https://github.com/spf13/viper). It does not aim to do everything viper does (such as mutating config maps and writing them back to files), but provides simpler primitives for reading and accessing configuration. It was written as a result of multiple stumbling blocks encountered with some of viper's fundamental flaws.
|
||||
|
||||
- viper breaks JSON, YAML, TOML, HCL language specs by [forcibly lowercasing keys](https://github.com/spf13/viper/pull/635).
|
||||
- Significantly bloats [build sizes](https://github.com/knadh/koanf/wiki/Comparison-with-spf13-viper).
|
||||
- Tightly couples config parsing with file extensions.
|
||||
- Has poor semantics and abstractions. Commandline, env, file etc. and various parses are hardcoded in the core. There are no primitives that can be extended.
|
||||
- Pulls a large number of [third party dependencies](https://github.com/spf13/viper/issues/707) into the core package. For instance, even if you do not use YAML or flags, the dependencies are still pulled as a result of the coupling.
|
||||
- Imposes arbitrary ordering conventions (eg: flag -> env -> config etc.)
|
||||
- `Get()` returns references to slices and maps. Mutations made outside change the underlying values inside the conf map.
|
||||
- Does non-idiomatic things such as [throwing away O(1) on flat maps](https://github.com/spf13/viper/blob/3b4aca75714a37276c4b1883630bd98c02498b73/viper.go#L1524).
|
||||
- There are a large number of [open issues](https://github.com/spf13/viper/issues).
|
|
@ -0,0 +1,588 @@
|
|||
package koanf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Int64 returns the int64 value of a given key path or 0 if the path
|
||||
// does not exist or if the value is not a valid int64.
|
||||
func (ko *Koanf) Int64(path string) int64 {
|
||||
if v := ko.Get(path); v != nil {
|
||||
i, _ := toInt64(v)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MustInt64 returns the int64 value of a given key path or panics
|
||||
// if the value is not set or set to default value of 0.
|
||||
func (ko *Koanf) MustInt64(path string) int64 {
|
||||
val := ko.Int64(path)
|
||||
if val == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Int64s returns the []int64 slice value of a given key path or an
|
||||
// empty []int64 slice if the path does not exist or if the value
|
||||
// is not a valid int slice.
|
||||
func (ko *Koanf) Int64s(path string) []int64 {
|
||||
o := ko.Get(path)
|
||||
if o == nil {
|
||||
return []int64{}
|
||||
}
|
||||
|
||||
var out []int64
|
||||
switch v := o.(type) {
|
||||
case []interface{}:
|
||||
out = make([]int64, 0, len(v))
|
||||
for _, vi := range v {
|
||||
i, err := toInt64(vi)
|
||||
|
||||
// On error, return as it's not a valid
|
||||
// int slice.
|
||||
if err != nil {
|
||||
return []int64{}
|
||||
}
|
||||
out = append(out, i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
return []int64{}
|
||||
}
|
||||
|
||||
// MustInt64s returns the []int64 slice value of a given key path or panics
|
||||
// if the value is not set or its default value.
|
||||
func (ko *Koanf) MustInt64s(path string) []int64 {
|
||||
val := ko.Int64s(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Int64Map returns the map[string]int64 value of a given key path
|
||||
// or an empty map[string]int64 if the path does not exist or if the
|
||||
// value is not a valid int64 map.
|
||||
func (ko *Koanf) Int64Map(path string) map[string]int64 {
|
||||
var (
|
||||
out = map[string]int64{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
|
||||
out = make(map[string]int64, len(mp))
|
||||
for k, v := range mp {
|
||||
switch i := v.(type) {
|
||||
case int64:
|
||||
out[k] = i
|
||||
default:
|
||||
// Attempt a conversion.
|
||||
iv, err := toInt64(i)
|
||||
if err != nil {
|
||||
return map[string]int64{}
|
||||
}
|
||||
out[k] = iv
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MustInt64Map returns the map[string]int64 value of a given key path
|
||||
// or panics if its not set or set to default value.
|
||||
func (ko *Koanf) MustInt64Map(path string) map[string]int64 {
|
||||
val := ko.Int64Map(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Int returns the int value of a given key path or 0 if the path
|
||||
// does not exist or if the value is not a valid int.
|
||||
func (ko *Koanf) Int(path string) int {
|
||||
return int(ko.Int64(path))
|
||||
}
|
||||
|
||||
// MustInt returns the int value of a given key path or panics
|
||||
// or panics if its not set or set to default value of 0.
|
||||
func (ko *Koanf) MustInt(path string) int {
|
||||
val := ko.Int(path)
|
||||
if val == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Ints returns the []int slice value of a given key path or an
|
||||
// empty []int slice if the path does not exist or if the value
|
||||
// is not a valid int slice.
|
||||
func (ko *Koanf) Ints(path string) []int {
|
||||
ints := ko.Int64s(path)
|
||||
if len(ints) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
out := make([]int, len(ints))
|
||||
for i, v := range ints {
|
||||
out[i] = int(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MustInts returns the []int slice value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustInts(path string) []int {
|
||||
val := ko.Ints(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// IntMap returns the map[string]int value of a given key path
|
||||
// or an empty map[string]int if the path does not exist or if the
|
||||
// value is not a valid int map.
|
||||
func (ko *Koanf) IntMap(path string) map[string]int {
|
||||
var (
|
||||
mp = ko.Int64Map(path)
|
||||
out = make(map[string]int, len(mp))
|
||||
)
|
||||
for k, v := range mp {
|
||||
out[k] = int(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MustIntMap returns the map[string]int value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustIntMap(path string) map[string]int {
|
||||
val := ko.IntMap(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Float64 returns the float64 value of a given key path or 0 if the path
|
||||
// does not exist or if the value is not a valid float64.
|
||||
func (ko *Koanf) Float64(path string) float64 {
|
||||
if v := ko.Get(path); v != nil {
|
||||
f, _ := toFloat64(v)
|
||||
return f
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MustFloat64 returns the float64 value of a given key path or panics
|
||||
// or panics if its not set or set to default value 0.
|
||||
func (ko *Koanf) MustFloat64(path string) float64 {
|
||||
val := ko.Float64(path)
|
||||
if val == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Float64s returns the []float64 slice value of a given key path or an
|
||||
// empty []float64 slice if the path does not exist or if the value
|
||||
// is not a valid float64 slice.
|
||||
func (ko *Koanf) Float64s(path string) []float64 {
|
||||
o := ko.Get(path)
|
||||
if o == nil {
|
||||
return []float64{}
|
||||
}
|
||||
|
||||
var out []float64
|
||||
switch v := o.(type) {
|
||||
case []interface{}:
|
||||
out = make([]float64, 0, len(v))
|
||||
for _, vi := range v {
|
||||
i, err := toFloat64(vi)
|
||||
|
||||
// On error, return as it's not a valid
|
||||
// int slice.
|
||||
if err != nil {
|
||||
return []float64{}
|
||||
}
|
||||
out = append(out, float64(i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
return []float64{}
|
||||
}
|
||||
|
||||
// MustFloat64s returns the []Float64 slice value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustFloat64s(path string) []float64 {
|
||||
val := ko.Float64s(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Float64Map returns the map[string]float64 value of a given key path
|
||||
// or an empty map[string]float64 if the path does not exist or if the
|
||||
// value is not a valid float64 map.
|
||||
func (ko *Koanf) Float64Map(path string) map[string]float64 {
|
||||
var (
|
||||
out = map[string]float64{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
|
||||
out = make(map[string]float64, len(mp))
|
||||
for k, v := range mp {
|
||||
switch i := v.(type) {
|
||||
case float64:
|
||||
out[k] = i
|
||||
default:
|
||||
// Attempt a conversion.
|
||||
iv, err := toFloat64(i)
|
||||
if err != nil {
|
||||
return map[string]float64{}
|
||||
}
|
||||
out[k] = iv
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MustFloat64Map returns the map[string]float64 value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustFloat64Map(path string) map[string]float64 {
|
||||
val := ko.Float64Map(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Duration returns the time.Duration value of a given key path assuming
|
||||
// that the key contains a valid numeric value.
|
||||
func (ko *Koanf) Duration(path string) time.Duration {
|
||||
// Look for a parsable string representation first.
|
||||
if v := ko.Int64(path); v > 0 {
|
||||
return time.Duration(v)
|
||||
}
|
||||
|
||||
v, _ := time.ParseDuration(ko.String(path))
|
||||
return v
|
||||
}
|
||||
|
||||
// MustDuration returns the time.Duration value of a given key path or panics
|
||||
// if its not set or set to default value 0.
|
||||
func (ko *Koanf) MustDuration(path string) time.Duration {
|
||||
val := ko.Duration(path)
|
||||
if val == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Time attempts to parse the value of a given key path and return time.Time
|
||||
// representation. If the value is numeric, it is treated as a UNIX timestamp
|
||||
// and if it's string, a parse is attempted with the given layout.
|
||||
func (ko *Koanf) Time(path, layout string) time.Time {
|
||||
// Unix timestamp?
|
||||
v := ko.Int64(path)
|
||||
if v != 0 {
|
||||
return time.Unix(v, 0)
|
||||
}
|
||||
|
||||
// String representation.
|
||||
s := ko.String(path)
|
||||
if s != "" {
|
||||
t, _ := time.Parse(layout, s)
|
||||
return t
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// MustTime attempts to parse the value of a given key path and return time.Time
|
||||
// representation. If the value is numeric, it is treated as a UNIX timestamp
|
||||
// and if it's string, a parse is attempted with the given layout. It panics if
|
||||
// the parsed time is zero.
|
||||
func (ko *Koanf) MustTime(path, layout string) time.Time {
|
||||
val := ko.Time(path, layout)
|
||||
if val.IsZero() {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// String returns the string value of a given key path or "" if the path
|
||||
// does not exist or if the value is not a valid string.
|
||||
func (ko *Koanf) String(path string) string {
|
||||
if v := ko.Get(path); v != nil {
|
||||
if i, ok := v.(string); ok {
|
||||
return i
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MustString returns the string value of a given key path
|
||||
// or panics if its not set or set to default value "".
|
||||
func (ko *Koanf) MustString(path string) string {
|
||||
val := ko.String(path)
|
||||
if val == "" {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Strings returns the []string slice value of a given key path or an
|
||||
// empty []string slice if the path does not exist or if the value
|
||||
// is not a valid string slice.
|
||||
func (ko *Koanf) Strings(path string) []string {
|
||||
o := ko.Get(path)
|
||||
if o == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var out []string
|
||||
switch v := o.(type) {
|
||||
case []interface{}:
|
||||
out = make([]string, 0, len(v))
|
||||
for _, u := range v {
|
||||
if s, ok := u.(string); ok {
|
||||
out = append(out, s)
|
||||
} else {
|
||||
out = append(out, fmt.Sprintf("%v", u))
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]string, len(v))
|
||||
copy(out[:], v[:])
|
||||
return out
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// MustStrings returns the []string slice value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustStrings(path string) []string {
|
||||
val := ko.Strings(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// StringMap returns the map[string]string value of a given key path
|
||||
// or an empty map[string]string if the path does not exist or if the
|
||||
// value is not a valid string map.
|
||||
func (ko *Koanf) StringMap(path string) map[string]string {
|
||||
var (
|
||||
out = map[string]string{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = make(map[string]string, len(mp))
|
||||
for k, v := range mp {
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
out[k] = s
|
||||
default:
|
||||
// There's a non string type. Return.
|
||||
return map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// MustStringMap returns the map[string]string value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustStringMap(path string) map[string]string {
|
||||
val := ko.StringMap(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// StringsMap returns the map[string][]string value of a given key path
|
||||
// or an empty map[string][]string if the path does not exist or if the
|
||||
// value is not a valid strings map.
|
||||
func (ko *Koanf) StringsMap(path string) map[string][]string {
|
||||
var (
|
||||
out = map[string][]string{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = make(map[string][]string, len(mp))
|
||||
for k, v := range mp {
|
||||
switch s := v.(type) {
|
||||
case []interface{}:
|
||||
for _, v := range s {
|
||||
switch sv := v.(type) {
|
||||
case string:
|
||||
out[k] = append(out[k], sv)
|
||||
default:
|
||||
return map[string][]string{}
|
||||
}
|
||||
}
|
||||
default:
|
||||
// There's a non []interface type. Return.
|
||||
return map[string][]string{}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// MustStringsMap returns the map[string][]string value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustStringsMap(path string) map[string][]string {
|
||||
val := ko.StringsMap(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Bytes returns the []byte value of a given key path or an empty
|
||||
// []byte slice if the path does not exist or if the value is not a valid string.
|
||||
func (ko *Koanf) Bytes(path string) []byte {
|
||||
return []byte(ko.String(path))
|
||||
}
|
||||
|
||||
// MustBytes returns the []byte value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustBytes(path string) []byte {
|
||||
val := ko.Bytes(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Bool returns the bool value of a given key path or false if the path
|
||||
// does not exist or if the value is not a valid bool representation.
|
||||
// Accepted string representations of bool are the ones supported by strconv.ParseBool.
|
||||
func (ko *Koanf) Bool(path string) bool {
|
||||
if v := ko.Get(path); v != nil {
|
||||
b, _ := toBool(v)
|
||||
return b
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Bools returns the []bool slice value of a given key path or an
|
||||
// empty []bool slice if the path does not exist or if the value
|
||||
// is not a valid bool slice.
|
||||
func (ko *Koanf) Bools(path string) []bool {
|
||||
o := ko.Get(path)
|
||||
if o == nil {
|
||||
return []bool{}
|
||||
}
|
||||
|
||||
var out []bool
|
||||
switch v := o.(type) {
|
||||
case []interface{}:
|
||||
out = make([]bool, 0, len(v))
|
||||
for _, u := range v {
|
||||
b, err := toBool(u)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out
|
||||
case []bool:
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustBools returns the []bool value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustBools(path string) []bool {
|
||||
val := ko.Bools(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// BoolMap returns the map[string]bool value of a given key path
|
||||
// or an empty map[string]bool if the path does not exist or if the
|
||||
// value is not a valid bool map.
|
||||
func (ko *Koanf) BoolMap(path string) map[string]bool {
|
||||
var (
|
||||
out = map[string]bool{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = make(map[string]bool, len(mp))
|
||||
for k, v := range mp {
|
||||
switch i := v.(type) {
|
||||
case bool:
|
||||
out[k] = i
|
||||
default:
|
||||
// Attempt a conversion.
|
||||
b, err := toBool(i)
|
||||
if err != nil {
|
||||
return map[string]bool{}
|
||||
}
|
||||
out[k] = b
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// MustBoolMap returns the map[string]bool value of a given key path or panics
|
||||
// if the value is not set or set to default value.
|
||||
func (ko *Koanf) MustBoolMap(path string) map[string]bool {
|
||||
val := ko.BoolMap(path)
|
||||
if len(val) == 0 {
|
||||
panic(fmt.Sprintf("invalid value: %s=%v", path, val))
|
||||
}
|
||||
return val
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
module github.com/knadh/koanf
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/hashicorp/hcl v1.0.0
|
||||
github.com/joho/godotenv v1.3.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.2.2
|
||||
github.com/pelletier/go-toml v1.7.0
|
||||
github.com/rhnvrm/simples3 v0.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
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/rhnvrm/simples3 v0.5.0 h1:X+WX0hqoKScdoJAw/G3GArfZ6Ygsn8q+6MdocTMKXOw=
|
||||
github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,25 @@
|
|||
package koanf
|
||||
|
||||
// Provider represents a configuration provider. Providers can
|
||||
// read configuration from a source (file, HTTP etc.)
|
||||
type Provider interface {
|
||||
// Read returns the entire configuration as raw []bytes to be parsed.
|
||||
// with a Parser.
|
||||
ReadBytes() ([]byte, error)
|
||||
|
||||
// Read returns the prased configuration as a nested map[string]interface{}.
|
||||
// It is important to note that the string keys should not be flat delimited
|
||||
// keys like `parent.child.key`, but nested like `{parent: {child: {key: 1}}}`.
|
||||
Read() (map[string]interface{}, error)
|
||||
|
||||
// Watch watches the source for changes, for instance, changes to a file,
|
||||
// and invokes a callback with an `event` interface, which a provider
|
||||
// is free to substitute with its own type, including nil.
|
||||
Watch(func(event interface{}, err error)) error
|
||||
}
|
||||
|
||||
// Parser represents a configuration format parser.
|
||||
type Parser interface {
|
||||
Unmarshal([]byte) (map[string]interface{}, error)
|
||||
Marshal(map[string]interface{}) ([]byte, error)
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
package koanf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/maps"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// Koanf is the configuration apparatus.
|
||||
type Koanf struct {
|
||||
confMap map[string]interface{}
|
||||
confMapFlat map[string]interface{}
|
||||
keyMap KeyMap
|
||||
delim string
|
||||
}
|
||||
|
||||
// KeyMap represents a map of flattened delimited keys and the non-delimited
|
||||
// parts as their slices. For nested keys, the map holds all levels of path combinations.
|
||||
// For example, the nested structure `parent -> child -> key` will produce the map:
|
||||
// parent.child.key => [parent, child, key]
|
||||
// parent.child => [parent, child]
|
||||
// parent => [parent]
|
||||
type KeyMap map[string][]string
|
||||
|
||||
// UnmarshalConf represents configuration options used by
|
||||
// Unmarshal() to unmarshal conf maps into arbitrary structs.
|
||||
type UnmarshalConf struct {
|
||||
// Tag is the struct field tag to unmarshal.
|
||||
// `koanf` is used if left empty.
|
||||
Tag string
|
||||
|
||||
// If this is set to true, instead of unmarshalling nested structures
|
||||
// based on the key path, keys are taken literally to unmarshal into
|
||||
// a flat struct. For example:
|
||||
// ```
|
||||
// type MyStuff struct {
|
||||
// Child1Name string `koanf:"parent1.child1.name"`
|
||||
// Child2Name string `koanf:"parent2.child2.name"`
|
||||
// Type string `koanf:"json"`
|
||||
// }
|
||||
// ```
|
||||
FlatPaths bool
|
||||
DecoderConfig *mapstructure.DecoderConfig
|
||||
}
|
||||
|
||||
// New returns a new instance of Koanf. delim is the delimiter to use
|
||||
// when specifying config key paths, for instance a . for `parent.child.key`
|
||||
// or a / for `parent/child/key`.
|
||||
func New(delim string) *Koanf {
|
||||
return &Koanf{
|
||||
delim: delim,
|
||||
confMap: make(map[string]interface{}),
|
||||
confMapFlat: make(map[string]interface{}),
|
||||
keyMap: make(KeyMap),
|
||||
}
|
||||
}
|
||||
|
||||
// Load takes a Provider that either provides a parsed config map[string]interface{}
|
||||
// in which case pa (Parser) can be nil, or raw bytes to be parsed, where a Parser
|
||||
// can be provided to parse.
|
||||
func (ko *Koanf) Load(p Provider, pa Parser) error {
|
||||
var (
|
||||
mp map[string]interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
// No Parser is given. Call the Provider's Read() method to get
|
||||
// the config map.
|
||||
if pa == nil {
|
||||
mp, err = p.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// There's a Parser. Get raw bytes from the Provider to parse.
|
||||
b, err := p.ReadBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mp, err = pa.Unmarshal(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ko.merge(mp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keys returns the slice of all flattened keys in the loaded configuration
|
||||
// sorted alphabetically.
|
||||
func (ko *Koanf) Keys() []string {
|
||||
out := make([]string, 0, len(ko.confMapFlat))
|
||||
for k := range ko.confMapFlat {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// KeyMap returns a map of flattened keys and the individual parts of the
|
||||
// key as slices. eg: "parent.child.key" => ["parent", "child", "key"]
|
||||
func (ko *Koanf) KeyMap() KeyMap {
|
||||
out := make(KeyMap, len(ko.keyMap))
|
||||
for key, parts := range ko.keyMap {
|
||||
out[key] = make([]string, len(parts))
|
||||
copy(out[key][:], parts[:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// All returns a map of all flattened key paths and their values.
|
||||
// Note that it uses maps.Copy to create a copy that uses
|
||||
// json.Marshal which changes the numeric types to float64.
|
||||
func (ko *Koanf) All() map[string]interface{} {
|
||||
return maps.Copy(ko.confMapFlat)
|
||||
}
|
||||
|
||||
// Raw returns a copy of the full raw conf map.
|
||||
// Note that it uses maps.Copy to create a copy that uses
|
||||
// json.Marshal which changes the numeric types to float64.
|
||||
func (ko *Koanf) Raw() map[string]interface{} {
|
||||
return maps.Copy(ko.confMap)
|
||||
}
|
||||
|
||||
// Sprint returns a key -> value string representation
|
||||
// of the config map with keys sorted alphabetically.
|
||||
func (ko *Koanf) Sprint() string {
|
||||
b := bytes.Buffer{}
|
||||
for _, k := range ko.Keys() {
|
||||
b.Write([]byte(fmt.Sprintf("%s -> %v\n", k, ko.confMapFlat[k])))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Print prints a key -> value string representation
|
||||
// of the config map with keys sorted alphabetically.
|
||||
func (ko *Koanf) Print() {
|
||||
fmt.Print(ko.Sprint())
|
||||
}
|
||||
|
||||
// Cut cuts the config map at a given key path into a sub map and
|
||||
// returns a new Koanf instance with the cut config map loaded.
|
||||
// For instance, if the loaded config has a path that looks like
|
||||
// parent.child.sub.a.b, `Cut("parent.child")` returns a new Koanf
|
||||
// instance with the config map `sub.a.b` where everything above
|
||||
// `parent.child` are cut out.
|
||||
func (ko *Koanf) Cut(path string) *Koanf {
|
||||
out := make(map[string]interface{})
|
||||
|
||||
// Cut only makes sense if the requested key path is a map.
|
||||
if v, ok := ko.Get(path).(map[string]interface{}); ok {
|
||||
out = v
|
||||
}
|
||||
|
||||
n := New(ko.delim)
|
||||
n.merge(out)
|
||||
return n
|
||||
}
|
||||
|
||||
// Copy returns a copy of the Koanf instance.
|
||||
func (ko *Koanf) Copy() *Koanf {
|
||||
return ko.Cut("")
|
||||
}
|
||||
|
||||
// Merge merges the config map of a given Koanf instance into
|
||||
// the current instance.
|
||||
func (ko *Koanf) Merge(in *Koanf) {
|
||||
ko.merge(in.Raw())
|
||||
}
|
||||
|
||||
// MergeAt merges the config map of a given Koanf instance into
|
||||
// the current instance as a sub map, at the given key path.
|
||||
// If all or part of the key path is missing, it will be created.
|
||||
// If the key path is `""`, this is equivalent to Merge.
|
||||
func (ko *Koanf) MergeAt(in *Koanf, path string) {
|
||||
// No path. Merge the two config maps.
|
||||
if path == "" {
|
||||
ko.Merge(in)
|
||||
return
|
||||
}
|
||||
|
||||
// Unflatten the config map with the given key path.
|
||||
n := maps.Unflatten(map[string]interface{}{
|
||||
path: in.Raw(),
|
||||
}, ko.delim)
|
||||
|
||||
ko.merge(n)
|
||||
}
|
||||
|
||||
// Marshal takes a Parser implementation and marshals the config map into bytes,
|
||||
// for example, to TOML or JSON bytes.
|
||||
func (ko *Koanf) Marshal(p Parser) ([]byte, error) {
|
||||
return p.Marshal(ko.Raw())
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals a given key path into the given struct using
|
||||
// the mapstructure lib. If no path is specified, the whole map is unmarshalled.
|
||||
// `koanf` is the struct field tag used to match field names. To customize,
|
||||
// use UnmarshalWithConf(). It uses the mitchellh/mapstructure package.
|
||||
func (ko *Koanf) Unmarshal(path string, o interface{}) error {
|
||||
return ko.UnmarshalWithConf(path, o, UnmarshalConf{})
|
||||
}
|
||||
|
||||
// UnmarshalWithConf is like Unmarshal but takes configuration params in UnmarshalConf.
|
||||
// See mitchellh/mapstructure's DecoderConfig for advanced customization
|
||||
// of the unmarshal behaviour.
|
||||
func (ko *Koanf) UnmarshalWithConf(path string, o interface{}, c UnmarshalConf) error {
|
||||
if c.DecoderConfig == nil {
|
||||
c.DecoderConfig = &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
mapstructure.StringToTimeDurationHookFunc()),
|
||||
Metadata: nil,
|
||||
Result: o,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
}
|
||||
|
||||
if c.Tag == "" {
|
||||
c.DecoderConfig.TagName = "koanf"
|
||||
} else {
|
||||
c.DecoderConfig.TagName = c.Tag
|
||||
}
|
||||
|
||||
d, err := mapstructure.NewDecoder(c.DecoderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal using flat key paths.
|
||||
mp := ko.Get(path)
|
||||
if c.FlatPaths {
|
||||
if f, ok := mp.(map[string]interface{}); ok {
|
||||
fmp, _ := maps.Flatten(f, nil, ko.delim)
|
||||
mp = fmp
|
||||
}
|
||||
}
|
||||
|
||||
return d.Decode(mp)
|
||||
}
|
||||
|
||||
// Get returns the raw, uncast interface{} value of a given key path
|
||||
// in the config map. If the key path does not exist, nil is returned.
|
||||
func (ko *Koanf) Get(path string) interface{} {
|
||||
// No path. Return the whole conf map.
|
||||
if path == "" {
|
||||
return ko.Raw()
|
||||
}
|
||||
|
||||
// Does the path exist?
|
||||
p, ok := ko.keyMap[path]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
res := maps.Search(ko.confMap, p)
|
||||
|
||||
// Non-reference types are okay to return directly.
|
||||
// Other types are "copied" with maps.Copy or json.Marshal
|
||||
// that change the numeric types to float64.
|
||||
|
||||
switch v := res.(type) {
|
||||
case int, int8, int16, int32, int64, float32, float64, string, bool:
|
||||
return v
|
||||
case map[string]interface{}:
|
||||
return maps.Copy(v)
|
||||
}
|
||||
|
||||
// Inefficient, but marshal and unmarshal to create a copy
|
||||
// of reference types to not expose internal references to slices and maps.
|
||||
var out interface{}
|
||||
b, _ := json.Marshal(res)
|
||||
json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Slices returns a list of Koanf instances constructed out of a
|
||||
// []map[string]interface{} interface at the given path.
|
||||
func (ko *Koanf) Slices(path string) []*Koanf {
|
||||
out := []*Koanf{}
|
||||
if path == "" {
|
||||
return out
|
||||
}
|
||||
|
||||
// Does the path exist?
|
||||
sl, ok := ko.Get(path).([]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
|
||||
for _, s := range sl {
|
||||
v, ok := s.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
k := New(ko.delim)
|
||||
k.Load(confmap.Provider(v, ""), nil)
|
||||
out = append(out, k)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Exists returns true if the given key path exists in the conf map.
|
||||
func (ko *Koanf) Exists(path string) bool {
|
||||
_, ok := ko.keyMap[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
// MapKeys returns a sorted string list of keys in a map addressed by the
|
||||
// given path. If the path is not a map, an empty string slice is
|
||||
// returned.
|
||||
func (ko *Koanf) MapKeys(path string) []string {
|
||||
var (
|
||||
out = []string{}
|
||||
o = ko.Get(path)
|
||||
)
|
||||
if o == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
mp, ok := o.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = make([]string, 0, len(mp))
|
||||
for k := range mp {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (ko *Koanf) merge(c map[string]interface{}) {
|
||||
maps.IntfaceKeysToStrings(c)
|
||||
maps.Merge(c, ko.confMap)
|
||||
|
||||
// Maintain a flattened version as well.
|
||||
ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.delim)
|
||||
ko.keyMap = populateKeyParts(ko.keyMap, ko.delim)
|
||||
}
|
||||
|
||||
// toInt64 takes an interface value and if it is an integer type,
|
||||
// converts and returns int64. If it's any other type,
|
||||
// forces it to a string and attempts to an strconv.Atoi
|
||||
// to get an integer out.
|
||||
func toInt64(v interface{}) (int64, error) {
|
||||
switch i := v.(type) {
|
||||
case int:
|
||||
return int64(i), nil
|
||||
case int8:
|
||||
return int64(i), nil
|
||||
case int16:
|
||||
return int64(i), nil
|
||||
case int32:
|
||||
return int64(i), nil
|
||||
case int64:
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Force it to a string and try to convert.
|
||||
f, err := strconv.ParseFloat(fmt.Sprintf("%v", v), 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int64(f), nil
|
||||
}
|
||||
|
||||
// toInt64 takes a `v interface{}` value and if it is a float type,
|
||||
// converts and returns a `float64`. If it's any other type, forces it to a
|
||||
// string and attempts to get a float out using `strconv.ParseFloat`.
|
||||
func toFloat64(v interface{}) (float64, error) {
|
||||
switch i := v.(type) {
|
||||
case float32:
|
||||
return float64(i), nil
|
||||
case float64:
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Force it to a string and try to convert.
|
||||
f, err := strconv.ParseFloat(fmt.Sprintf("%v", v), 64)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// toBool takes an interface value and if it is a bool type,
|
||||
// returns it. If it's any other type, forces it to a string and attempts
|
||||
// to parse it as a bool using strconv.ParseBool.
|
||||
func toBool(v interface{}) (bool, error) {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Force it to a string and try to convert.
|
||||
b, err := strconv.ParseBool(fmt.Sprintf("%v", v))
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// populateKeyParts iterates a key map and generates all possible
|
||||
// traversal paths. For instance, `parent.child.key` generates
|
||||
// `parent`, and `parent.child`.
|
||||
func populateKeyParts(m KeyMap, delim string) KeyMap {
|
||||
out := make(KeyMap)
|
||||
for _, parts := range m {
|
||||
for i := range parts {
|
||||
nk := strings.Join(parts[0:i+1], delim)
|
||||
if _, ok := out[nk]; ok {
|
||||
continue
|
||||
}
|
||||
out[nk] = make([]string, i+1)
|
||||
copy(out[nk][:], parts[0:i+1])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
// Package maps provides reusable functions for manipulating nested
|
||||
// map[string]interface{} maps are common unmarshal products from
|
||||
// various serializers such as json, yaml etc.
|
||||
package maps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Flatten takes a map[string]interface{} and traverses it and flattens
|
||||
// nested children into keys delimited by delim.
|
||||
//
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
//
|
||||
// eg: `{ "parent": { "child": 123 }}` becomes `{ "parent.child": 123 }`
|
||||
// In addition, it keeps track of and returns a map of the delimited keypaths with
|
||||
// a slice of key parts, for eg: { "parent.child": ["parent", "child"] }. This
|
||||
// parts list is used to remember the key path's original structure to
|
||||
// unflatten later.
|
||||
func Flatten(m map[string]interface{}, keys []string, delim string) (map[string]interface{}, map[string][]string) {
|
||||
var (
|
||||
out = make(map[string]interface{})
|
||||
keyMap = make(map[string][]string)
|
||||
)
|
||||
for key, val := range m {
|
||||
// Copy the incoming key paths into a fresh list
|
||||
// and append the current key in the iteration.
|
||||
kp := make([]string, 0, len(keys)+1)
|
||||
kp = append(kp, keys...)
|
||||
kp = append(kp, key)
|
||||
|
||||
switch cur := val.(type) {
|
||||
case map[string]interface{}:
|
||||
// Empty map.
|
||||
if len(cur) == 0 {
|
||||
newKey := strings.Join(kp, delim)
|
||||
out[newKey] = val
|
||||
keyMap[newKey] = kp
|
||||
continue
|
||||
}
|
||||
|
||||
// It's a nested map. Flatten it recursively.
|
||||
next, parts := Flatten(cur, kp, delim)
|
||||
|
||||
// Copy the resultant key parts and the value maps.
|
||||
for k, p := range parts {
|
||||
keyMap[k] = p
|
||||
}
|
||||
for k, v := range next {
|
||||
out[k] = v
|
||||
}
|
||||
default:
|
||||
newKey := strings.Join(kp, delim)
|
||||
out[newKey] = val
|
||||
keyMap[newKey] = kp
|
||||
}
|
||||
}
|
||||
return out, keyMap
|
||||
}
|
||||
|
||||
// Unflatten takes a flattened key:value map (non-nested with delimited keys)
|
||||
// and returns a nested map where the keys are split into hierarchies by the given
|
||||
// delimiter. For instance, `parent.child.key: 1` to `{parent: {child: {key: 1}}}`
|
||||
//
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
func Unflatten(m map[string]interface{}, delim string) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
|
||||
// Iterate through the flat conf map.
|
||||
for k, v := range m {
|
||||
var (
|
||||
keys = strings.Split(k, delim)
|
||||
next = out
|
||||
)
|
||||
|
||||
// Iterate through key parts, for eg:, parent.child.key
|
||||
// will be ["parent", "child", "key"]
|
||||
for _, k := range keys[:len(keys)-1] {
|
||||
sub, ok := next[k]
|
||||
if !ok {
|
||||
// If the key does not exist in the map, create it.
|
||||
sub = make(map[string]interface{})
|
||||
next[k] = sub
|
||||
}
|
||||
if n, ok := sub.(map[string]interface{}); ok {
|
||||
next = n
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the value.
|
||||
next[keys[len(keys)-1]] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Merge recursively merges map a into b (left to right), mutating
|
||||
// and expanding map b. Note that there's no copying involved, so
|
||||
// map b will retain references to map a.
|
||||
//
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
func Merge(a, b map[string]interface{}) {
|
||||
for key, val := range a {
|
||||
// Does the key exist in the target map?
|
||||
// If no, add it and move on.
|
||||
bVal, ok := b[key]
|
||||
if !ok {
|
||||
b[key] = val
|
||||
continue
|
||||
}
|
||||
|
||||
// If the incoming val is not a map, do a direct merge.
|
||||
if _, ok := val.(map[string]interface{}); !ok {
|
||||
b[key] = val
|
||||
continue
|
||||
}
|
||||
|
||||
// The source key and target keys are both maps. Merge them.
|
||||
switch v := bVal.(type) {
|
||||
case map[string]interface{}:
|
||||
Merge(val.(map[string]interface{}), v)
|
||||
default:
|
||||
b[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search recursively searches a map for a given path. The path is
|
||||
// the key map slice, for eg:, parent.child.key -> [parent child key].
|
||||
//
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
func Search(mp map[string]interface{}, path []string) interface{} {
|
||||
next, ok := mp[path[0]]
|
||||
if ok {
|
||||
if len(path) == 1 {
|
||||
return next
|
||||
}
|
||||
switch next.(type) {
|
||||
case map[string]interface{}:
|
||||
return Search(next.(map[string]interface{}), path[1:])
|
||||
default:
|
||||
return nil
|
||||
} //
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy returns a copy of a conf map by doing a JSON marshal+unmarshal
|
||||
// pass. Inefficient, but creates a true deep copy. There is a side
|
||||
// effect, that is, all numeric types change to float64.
|
||||
//
|
||||
// It's important to note that all nested maps should be
|
||||
// map[string]interface{} and not map[interface{}]interface{}.
|
||||
// Use IntfaceKeysToStrings() to convert if necessary.
|
||||
func Copy(mp map[string]interface{}) map[string]interface{} {
|
||||
var out map[string]interface{}
|
||||
b, _ := json.Marshal(mp)
|
||||
json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// IntfaceKeysToStrings recursively converts map[interface{}]interface{} to
|
||||
// map[string]interface{}. Some parses such as YAML unmarshal return this.
|
||||
func IntfaceKeysToStrings(mp map[string]interface{}) {
|
||||
for key, val := range mp {
|
||||
switch cur := val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
x := make(map[string]interface{})
|
||||
for k, v := range cur {
|
||||
x[fmt.Sprintf("%v", k)] = v
|
||||
}
|
||||
mp[key] = x
|
||||
IntfaceKeysToStrings(x)
|
||||
case []interface{}:
|
||||
for i, v := range cur {
|
||||
switch sub := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
x := make(map[string]interface{})
|
||||
for k, v := range sub {
|
||||
x[fmt.Sprintf("%v", k)] = v
|
||||
}
|
||||
cur[i] = x
|
||||
IntfaceKeysToStrings(x)
|
||||
case map[string]interface{}:
|
||||
IntfaceKeysToStrings(sub)
|
||||
}
|
||||
}
|
||||
case map[string]interface{}:
|
||||
IntfaceKeysToStrings(cur)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StringSliceToLookupMap takes a slice of strings and returns a lookup map
|
||||
// with the slice values as keys with true values.
|
||||
func StringSliceToLookupMap(s []string) map[string]bool {
|
||||
mp := make(map[string]bool, len(s))
|
||||
for _, v := range s {
|
||||
mp[v] = true
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// Int64SliceToLookupMap takes a slice of int64s and returns a lookup map
|
||||
// with the slice values as keys with true values.
|
||||
func Int64SliceToLookupMap(s []int64) map[int64]bool {
|
||||
mp := make(map[int64]bool, len(s))
|
||||
for _, v := range s {
|
||||
mp[v] = true
|
||||
}
|
||||
return mp
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Package toml implements a koanf.Parser that parses TOML bytes as conf maps.
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
// TOML implements a TOML parser.
|
||||
type TOML struct{}
|
||||
|
||||
// Parser returns a TOML Parser.
|
||||
func Parser() *TOML {
|
||||
return &TOML{}
|
||||
}
|
||||
|
||||
// Unmarshal parses the given TOML bytes.
|
||||
func (p *TOML) Unmarshal(b []byte) (map[string]interface{}, error) {
|
||||
r, err := toml.LoadReader(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.ToMap(), err
|
||||
}
|
||||
|
||||
// Marshal marshals the given config map to TOML bytes.
|
||||
func (p *TOML) Marshal(o map[string]interface{}) ([]byte, error) {
|
||||
out, err := toml.TreeFromMap(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Marshal()
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Package confmap implements a koanf.Provider that takes nested
|
||||
// and flat map[string]interface{} config maps and provides them
|
||||
// to koanf.
|
||||
package confmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/knadh/koanf/maps"
|
||||
)
|
||||
|
||||
// Confmap implements a raw map[string]interface{} provider.
|
||||
type Confmap struct {
|
||||
mp map[string]interface{}
|
||||
}
|
||||
|
||||
// Provider returns a confmap Provider that takes a flat or nested
|
||||
// map[string]interface{}. If a delim is provided, it indicates that the
|
||||
// keys are flat and the map needs to be unflatted by delim.
|
||||
func Provider(mp map[string]interface{}, delim string) *Confmap {
|
||||
cp := maps.Copy(mp)
|
||||
maps.IntfaceKeysToStrings(cp)
|
||||
if delim != "" {
|
||||
cp = maps.Unflatten(cp, delim)
|
||||
}
|
||||
return &Confmap{mp: cp}
|
||||
}
|
||||
|
||||
// ReadBytes is not supported by the env provider.
|
||||
func (e *Confmap) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("confmap provider does not support this method")
|
||||
}
|
||||
|
||||
// Read returns the loaded map[string]interface{}.
|
||||
func (e *Confmap) Read() (map[string]interface{}, error) {
|
||||
return e.mp, nil
|
||||
}
|
||||
|
||||
// Watch is not supported.
|
||||
func (e *Confmap) Watch(cb func(event interface{}, err error)) error {
|
||||
return errors.New("confmap provider does not support this method")
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Package file implements a koanf.Provider that reads raw bytes
|
||||
// from files on disk to be used with a koanf.Parser to parse
|
||||
// into conf maps.
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// File implements a File provider.
|
||||
type File struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// Provider returns a file provider.
|
||||
func Provider(path string) *File {
|
||||
return &File{path: filepath.Clean(path)}
|
||||
}
|
||||
|
||||
// ReadBytes reads the contents of a file on disk and returns the bytes.
|
||||
func (f *File) ReadBytes() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
}
|
||||
|
||||
// Read is not supported by the file provider.
|
||||
func (f *File) Read() (map[string]interface{}, error) {
|
||||
return nil, errors.New("file provider does not support this method")
|
||||
}
|
||||
|
||||
// Watch watches the file and triggers a callback when it changes. It is a
|
||||
// blocking function that internally spawns a goroutine to watch for changes.
|
||||
func (f *File) Watch(cb func(event interface{}, err error)) error {
|
||||
// Resolve symlinks and save the original path so that changes to symlinks
|
||||
// can be detected.
|
||||
realPath, err := filepath.EvalSymlinks(f.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realPath = filepath.Clean(realPath)
|
||||
|
||||
// Although only a single file is being watched, fsnotify has to watch
|
||||
// the whole parent directory to pick up all events such as symlink changes.
|
||||
fDir, _ := filepath.Split(f.path)
|
||||
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
lastEvent string
|
||||
lastEventTime time.Time
|
||||
)
|
||||
|
||||
go func() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-w.Events:
|
||||
if !ok {
|
||||
cb(nil, errors.New("fsnotify watch channel closed"))
|
||||
break loop
|
||||
}
|
||||
|
||||
// Use a simple timer to buffer events as certain events fire
|
||||
// multiple times on some platforms.
|
||||
if event.String() == lastEvent && time.Since(lastEventTime) < time.Millisecond*5 {
|
||||
continue
|
||||
}
|
||||
lastEvent = event.String()
|
||||
lastEventTime = time.Now()
|
||||
|
||||
evFile := filepath.Clean(event.Name)
|
||||
|
||||
// Since the event is triggered on a directory, is this
|
||||
// one on the file being watched?
|
||||
if evFile != realPath && evFile != f.path {
|
||||
continue
|
||||
}
|
||||
|
||||
// The file was removed.
|
||||
if event.Op&fsnotify.Remove != 0 {
|
||||
cb(nil, fmt.Errorf("file %s was removed", event.Name))
|
||||
break loop
|
||||
}
|
||||
|
||||
// Resolve symlink to get the real path, in case the symlink's
|
||||
// target has changed.
|
||||
curPath, err := filepath.EvalSymlinks(f.path)
|
||||
if err != nil {
|
||||
cb(nil, err)
|
||||
break loop
|
||||
}
|
||||
realPath = filepath.Clean(curPath)
|
||||
|
||||
// Finally, we only care about create and write.
|
||||
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Trigger event.
|
||||
cb(nil, nil)
|
||||
|
||||
// There's an error.
|
||||
case err, ok := <-w.Errors:
|
||||
if !ok {
|
||||
cb(nil, errors.New("fsnotify err channel closed"))
|
||||
break loop
|
||||
}
|
||||
|
||||
// Pass the error to the callback.
|
||||
cb(nil, err)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
w.Close()
|
||||
}()
|
||||
|
||||
// Watch the directory for changes.
|
||||
return w.Add(fDir)
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// Package posflag implements a koanf.Provider that reads commandline
|
||||
// parameters as conf maps using spf13/pflag, a POSIX compliant
|
||||
// alternative to Go's stdlib flag package.
|
||||
package posflag
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/maps"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Posflag implements a pflag command line provider.
|
||||
type Posflag struct {
|
||||
delim string
|
||||
flagset *pflag.FlagSet
|
||||
ko *koanf.Koanf
|
||||
}
|
||||
|
||||
// Provider returns a commandline flags provider that returns
|
||||
// a nested map[string]interface{} of environment variable where the
|
||||
// nesting hierarchy of keys are defined by delim. For instance, the
|
||||
// delim "." will convert the key `parent.child.key: 1`
|
||||
// to `{parent: {child: {key: 1}}}`.
|
||||
//
|
||||
// It takes an optional (but recommended) Koanf instance to see if the
|
||||
// the flags defined have been set from other providers, for instance,
|
||||
// a config file. If they are not, then the default values of the flags
|
||||
// are merged. If they do exist, the flag values are not merged but only
|
||||
// the values that have been explicitly set in the command line are merged.
|
||||
func Provider(f *pflag.FlagSet, delim string, ko *koanf.Koanf) *Posflag {
|
||||
return &Posflag{
|
||||
flagset: f,
|
||||
delim: delim,
|
||||
ko: ko,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads the flag variables and returns a nested conf map.
|
||||
func (p *Posflag) Read() (map[string]interface{}, error) {
|
||||
mp := make(map[string]interface{})
|
||||
p.flagset.VisitAll(func(f *pflag.Flag) {
|
||||
// If no value was explicitly set in the command line,
|
||||
// check if the default value should be used.
|
||||
if !f.Changed {
|
||||
if p.ko != nil {
|
||||
if p.ko.Exists(f.Name) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
switch f.Value.Type() {
|
||||
case "int":
|
||||
i, _ := p.flagset.GetInt(f.Name)
|
||||
v = int64(i)
|
||||
case "int8":
|
||||
i, _ := p.flagset.GetInt8(f.Name)
|
||||
v = int64(i)
|
||||
case "int16":
|
||||
i, _ := p.flagset.GetInt16(f.Name)
|
||||
v = int64(i)
|
||||
case "int32":
|
||||
i, _ := p.flagset.GetInt32(f.Name)
|
||||
v = int64(i)
|
||||
case "int64":
|
||||
i, _ := p.flagset.GetInt64(f.Name)
|
||||
v = int64(i)
|
||||
case "float32":
|
||||
v, _ = p.flagset.GetFloat32(f.Name)
|
||||
case "float":
|
||||
v, _ = p.flagset.GetFloat64(f.Name)
|
||||
case "bool":
|
||||
v, _ = p.flagset.GetBool(f.Name)
|
||||
case "stringSlice":
|
||||
v, _ = p.flagset.GetStringSlice(f.Name)
|
||||
case "intSlice":
|
||||
v, _ = p.flagset.GetIntSlice(f.Name)
|
||||
default:
|
||||
v = f.Value.String()
|
||||
}
|
||||
|
||||
mp[f.Name] = v
|
||||
})
|
||||
return maps.Unflatten(mp, p.delim), nil
|
||||
}
|
||||
|
||||
// ReadBytes is not supported by the env koanf.
|
||||
func (p *Posflag) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("pflag provider does not support this method")
|
||||
}
|
||||
|
||||
// Watch is not supported.
|
||||
func (p *Posflag) Watch(cb func(event interface{}, err error)) error {
|
||||
return errors.New("posflag provider does not support this method")
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- "1.14.x"
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test
|
||||
- go test -bench . -benchmem
|
|
@ -0,0 +1,61 @@
|
|||
## 1.3.3
|
||||
|
||||
* Decoding maps from maps creates a settable value for decode hooks [GH-203]
|
||||
|
||||
## 1.3.2
|
||||
|
||||
* Decode into interface type with a struct value is supported [GH-187]
|
||||
|
||||
## 1.3.1
|
||||
|
||||
* Squash should only squash embedded structs. [GH-194]
|
||||
|
||||
## 1.3.0
|
||||
|
||||
* Added `",omitempty"` support. This will ignore zero values in the source
|
||||
structure when encoding. [GH-145]
|
||||
|
||||
## 1.2.3
|
||||
|
||||
* Fix duplicate entries in Keys list with pointer values. [GH-185]
|
||||
|
||||
## 1.2.2
|
||||
|
||||
* Do not add unsettable (unexported) values to the unused metadata key
|
||||
or "remain" value. [GH-150]
|
||||
|
||||
## 1.2.1
|
||||
|
||||
* Go modules checksum mismatch fix
|
||||
|
||||
## 1.2.0
|
||||
|
||||
* Added support to capture unused values in a field using the `",remain"` value
|
||||
in the mapstructure tag. There is an example to showcase usage.
|
||||
* Added `DecoderConfig` option to always squash embedded structs
|
||||
* `json.Number` can decode into `uint` types
|
||||
* Empty slices are preserved and not replaced with nil slices
|
||||
* Fix panic that can occur in when decoding a map into a nil slice of structs
|
||||
* Improved package documentation for godoc
|
||||
|
||||
## 1.1.2
|
||||
|
||||
* Fix error when decode hook decodes interface implementation into interface
|
||||
type. [GH-140]
|
||||
|
||||
## 1.1.1
|
||||
|
||||
* Fix panic that can happen in `decodePtr`
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133]
|
||||
* Support struct to struct decoding [GH-137]
|
||||
* If source map value is nil, then destination map value is nil (instead of empty)
|
||||
* If source slice value is nil, then destination slice value is nil (instead of empty)
|
||||
* If source pointer is nil, then destination pointer is set to nil (instead of
|
||||
allocated zero value of type)
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial tagged stable release.
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Mitchell Hashimoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,46 @@
|
|||
# mapstructure [![Godoc](https://godoc.org/github.com/mitchellh/mapstructure?status.svg)](https://godoc.org/github.com/mitchellh/mapstructure)
|
||||
|
||||
mapstructure is a Go library for decoding generic map values to structures
|
||||
and vice versa, while providing helpful error handling.
|
||||
|
||||
This library is most useful when decoding values from some data stream (JSON,
|
||||
Gob, etc.) where you don't _quite_ know the structure of the underlying data
|
||||
until you read a part of it. You can therefore read a `map[string]interface{}`
|
||||
and use this library to decode it into the proper underlying native Go
|
||||
structure.
|
||||
|
||||
## Installation
|
||||
|
||||
Standard `go get`:
|
||||
|
||||
```
|
||||
$ go get github.com/mitchellh/mapstructure
|
||||
```
|
||||
|
||||
## Usage & Example
|
||||
|
||||
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure).
|
||||
|
||||
The `Decode` function has examples associated with it there.
|
||||
|
||||
## But Why?!
|
||||
|
||||
Go offers fantastic standard libraries for decoding formats such as JSON.
|
||||
The standard method is to have a struct pre-created, and populate that struct
|
||||
from the bytes of the encoded format. This is great, but the problem is if
|
||||
you have configuration or an encoding that changes slightly depending on
|
||||
specific fields. For example, consider this JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "person",
|
||||
"name": "Mitchell"
|
||||
}
|
||||
```
|
||||
|
||||
Perhaps we can't populate a specific structure without first reading
|
||||
the "type" field from the JSON. We could always do two passes over the
|
||||
decoding of the JSON (reading the "type" first, and the rest later).
|
||||
However, it is much simpler to just decode this into a `map[string]interface{}`
|
||||
structure, read the "type" key, then use something like this library
|
||||
to decode it into the proper structure.
|
|
@ -0,0 +1,217 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
|
||||
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
|
||||
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
|
||||
// Create variables here so we can reference them with the reflect pkg
|
||||
var f1 DecodeHookFuncType
|
||||
var f2 DecodeHookFuncKind
|
||||
|
||||
// Fill in the variables into this interface and the rest is done
|
||||
// automatically using the reflect package.
|
||||
potential := []interface{}{f1, f2}
|
||||
|
||||
v := reflect.ValueOf(h)
|
||||
vt := v.Type()
|
||||
for _, raw := range potential {
|
||||
pt := reflect.ValueOf(raw).Type()
|
||||
if vt.ConvertibleTo(pt) {
|
||||
return v.Convert(pt).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeHookExec executes the given decode hook. This should be used
|
||||
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc
|
||||
// that took reflect.Kind instead of reflect.Type.
|
||||
func DecodeHookExec(
|
||||
raw DecodeHookFunc,
|
||||
from reflect.Type, to reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
switch f := typedDecodeHook(raw).(type) {
|
||||
case DecodeHookFuncType:
|
||||
return f(from, to, data)
|
||||
case DecodeHookFuncKind:
|
||||
return f(from.Kind(), to.Kind(), data)
|
||||
default:
|
||||
return nil, errors.New("invalid decode hook signature")
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
|
||||
// automatically composes multiple DecodeHookFuncs.
|
||||
//
|
||||
// The composed funcs are called in order, with the result of the
|
||||
// previous transformation.
|
||||
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
var err error
|
||||
for _, f1 := range fs {
|
||||
data, err = DecodeHookExec(f1, f, t, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Modify the from kind to be correct with the new data
|
||||
f = nil
|
||||
if val := reflect.ValueOf(data); val.IsValid() {
|
||||
f = val.Type()
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToSliceHookFunc returns a DecodeHookFunc that converts
|
||||
// string to []string by splitting on the given sep.
|
||||
func StringToSliceHookFunc(sep string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f != reflect.String || t != reflect.Slice {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
raw := data.(string)
|
||||
if raw == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(raw, sep), nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeDurationHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Duration.
|
||||
func StringToTimeDurationHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Duration(5)) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.ParseDuration(data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IP
|
||||
func StringToIPHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IP{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
ip := net.ParseIP(data.(string))
|
||||
if ip == nil {
|
||||
return net.IP{}, fmt.Errorf("failed parsing ip %v", data)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPNetHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IPNet
|
||||
func StringToIPNetHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IPNet{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
_, net, err := net.ParseCIDR(data.(string))
|
||||
return net, err
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Time.
|
||||
func StringToTimeHookFunc(layout string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Time{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.Parse(layout, data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to
|
||||
// the decoder.
|
||||
//
|
||||
// Note that this is significantly different from the WeaklyTypedInput option
|
||||
// of the DecoderConfig.
|
||||
func WeaklyTypedHook(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
switch t {
|
||||
case reflect.String:
|
||||
switch f {
|
||||
case reflect.Bool:
|
||||
if dataVal.Bool() {
|
||||
return "1", nil
|
||||
}
|
||||
return "0", nil
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
|
||||
case reflect.Int:
|
||||
return strconv.FormatInt(dataVal.Int(), 10), nil
|
||||
case reflect.Slice:
|
||||
dataType := dataVal.Type()
|
||||
elemKind := dataType.Elem().Kind()
|
||||
if elemKind == reflect.Uint8 {
|
||||
return string(dataVal.Interface().([]uint8)), nil
|
||||
}
|
||||
case reflect.Uint:
|
||||
return strconv.FormatUint(dataVal.Uint(), 10), nil
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error implements the error interface and can represents multiple
|
||||
// errors that occur in the course of a single decode.
|
||||
type Error struct {
|
||||
Errors []string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
points := make([]string, len(e.Errors))
|
||||
for i, err := range e.Errors {
|
||||
points[i] = fmt.Sprintf("* %s", err)
|
||||
}
|
||||
|
||||
sort.Strings(points)
|
||||
return fmt.Sprintf(
|
||||
"%d error(s) decoding:\n\n%s",
|
||||
len(e.Errors), strings.Join(points, "\n"))
|
||||
}
|
||||
|
||||
// WrappedErrors implements the errwrap.Wrapper interface to make this
|
||||
// return value more useful with the errwrap and go-multierror libraries.
|
||||
func (e *Error) WrappedErrors() []error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]error, len(e.Errors))
|
||||
for i, e := range e.Errors {
|
||||
result[i] = errors.New(e)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func appendErrors(errors []string, err error) []string {
|
||||
switch e := err.(type) {
|
||||
case *Error:
|
||||
return append(errors, e.Errors...)
|
||||
default:
|
||||
return append(errors, e.Error())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/mitchellh/mapstructure
|
||||
|
||||
go 1.14
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
|||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
|
@ -0,0 +1,5 @@
|
|||
test_program/test_program_bin
|
||||
fuzz/
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
cmd/tomltestgen/tomltestgen
|
|
@ -0,0 +1,132 @@
|
|||
## Contributing
|
||||
|
||||
Thank you for your interest in go-toml! We appreciate you considering
|
||||
contributing to go-toml!
|
||||
|
||||
The main goal is the project is to provide an easy-to-use TOML
|
||||
implementation for Go that gets the job done and gets out of your way –
|
||||
dealing with TOML is probably not the central piece of your project.
|
||||
|
||||
As the single maintainer of go-toml, time is scarce. All help, big or
|
||||
small, is more than welcomed!
|
||||
|
||||
### Ask questions
|
||||
|
||||
Any question you may have, somebody else might have it too. Always feel
|
||||
free to ask them on the [issues tracker][issues-tracker]. We will try to
|
||||
answer them as clearly and quickly as possible, time permitting.
|
||||
|
||||
Asking questions also helps us identify areas where the documentation needs
|
||||
improvement, or new features that weren't envisioned before. Sometimes, a
|
||||
seemingly innocent question leads to the fix of a bug. Don't hesitate and
|
||||
ask away!
|
||||
|
||||
### Improve the documentation
|
||||
|
||||
The best way to share your knowledge and experience with go-toml is to
|
||||
improve the documentation. Fix a typo, clarify an interface, add an
|
||||
example, anything goes!
|
||||
|
||||
The documentation is present in the [README][readme] and thorough the
|
||||
source code. On release, it gets updated on [GoDoc][godoc]. To make a
|
||||
change to the documentation, create a pull request with your proposed
|
||||
changes. For simple changes like that, the easiest way to go is probably
|
||||
the "Fork this project and edit the file" button on Github, displayed at
|
||||
the top right of the file. Unless it's a trivial change (for example a
|
||||
typo), provide a little bit of context in your pull request description or
|
||||
commit message.
|
||||
|
||||
### Report a bug
|
||||
|
||||
Found a bug! Sorry to hear that :(. Help us and other track them down and
|
||||
fix by reporting it. [File a new bug report][bug-report] on the [issues
|
||||
tracker][issues-tracker]. The template should provide enough guidance on
|
||||
what to include. When in doubt: add more details! By reducing ambiguity and
|
||||
providing more information, it decreases back and forth and saves everyone
|
||||
time.
|
||||
|
||||
### Code changes
|
||||
|
||||
Want to contribute a patch? Very happy to hear that!
|
||||
|
||||
First, some high-level rules:
|
||||
|
||||
* A short proposal with some POC code is better than a lengthy piece of
|
||||
text with no code. Code speaks louder than words.
|
||||
* No backward-incompatible patch will be accepted unless discussed.
|
||||
Sometimes it's hard, and Go's lack of versioning by default does not
|
||||
help, but we try not to break people's programs unless we absolutely have
|
||||
to.
|
||||
* If you are writing a new feature or extending an existing one, make sure
|
||||
to write some documentation.
|
||||
* Bug fixes need to be accompanied with regression tests.
|
||||
* New code needs to be tested.
|
||||
* Your commit messages need to explain why the change is needed, even if
|
||||
already included in the PR description.
|
||||
|
||||
It does sound like a lot, but those best practices are here to save time
|
||||
overall and continuously improve the quality of the project, which is
|
||||
something everyone benefits from.
|
||||
|
||||
#### Get started
|
||||
|
||||
The fairly standard code contribution process looks like that:
|
||||
|
||||
1. [Fork the project][fork].
|
||||
2. Make your changes, commit on any branch you like.
|
||||
3. [Open up a pull request][pull-request]
|
||||
4. Review, potential ask for changes.
|
||||
5. Merge. You're in!
|
||||
|
||||
Feel free to ask for help! You can create draft pull requests to gather
|
||||
some early feedback!
|
||||
|
||||
#### Run the tests
|
||||
|
||||
You can run tests for go-toml using Go's test tool: `go test ./...`.
|
||||
When creating a pull requests, all tests will be ran on Linux on a few Go
|
||||
versions (Travis CI), and on Windows using the latest Go version
|
||||
(AppVeyor).
|
||||
|
||||
#### Style
|
||||
|
||||
Try to look around and follow the same format and structure as the rest of
|
||||
the code. We enforce using `go fmt` on the whole code base.
|
||||
|
||||
---
|
||||
|
||||
### Maintainers-only
|
||||
|
||||
#### Merge pull request
|
||||
|
||||
Checklist:
|
||||
|
||||
* Passing CI.
|
||||
* Does not introduce backward-incompatible changes (unless discussed).
|
||||
* Has relevant doc changes.
|
||||
* Has relevant unit tests.
|
||||
|
||||
1. Merge using "squash and merge".
|
||||
2. Make sure to edit the commit message to keep all the useful information
|
||||
nice and clean.
|
||||
3. Make sure the commit title is clear and contains the PR number (#123).
|
||||
|
||||
#### New release
|
||||
|
||||
1. Go to [releases][releases]. Click on "X commits to master since this
|
||||
release".
|
||||
2. Make note of all the changes. Look for backward incompatible changes,
|
||||
new features, and bug fixes.
|
||||
3. Pick the new version using the above and semver.
|
||||
4. Create a [new release][new-release].
|
||||
5. Follow the same format as [1.1.0][release-110].
|
||||
|
||||
[issues-tracker]: https://github.com/pelletier/go-toml/issues
|
||||
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
|
||||
[godoc]: https://godoc.org/github.com/pelletier/go-toml
|
||||
[readme]: ./README.md
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
|
||||
[releases]: https://github.com/pelletier/go-toml/releases
|
||||
[new-release]: https://github.com/pelletier/go-toml/releases/new
|
||||
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0
|
|
@ -0,0 +1,11 @@
|
|||
FROM golang:1.12-alpine3.9 as builder
|
||||
WORKDIR /go/src/github.com/pelletier/go-toml
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
RUN go install ./...
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/bin/tomll /usr/bin/tomll
|
||||
COPY --from=builder /go/bin/tomljson /usr/bin/tomljson
|
||||
COPY --from=builder /go/bin/jsontoml /usr/bin/jsontoml
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Denis Dyakov
|
||||
Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
|
@ -0,0 +1,29 @@
|
|||
export CGO_ENABLED=0
|
||||
go := go
|
||||
go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1)
|
||||
go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2)
|
||||
|
||||
out.tools := tomll tomljson jsontoml
|
||||
out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz)
|
||||
sources := $(wildcard **/*.go)
|
||||
|
||||
|
||||
.PHONY:
|
||||
tools: $(out.tools)
|
||||
|
||||
$(out.tools): $(sources)
|
||||
GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@
|
||||
|
||||
.PHONY:
|
||||
dist: $(out.dist)
|
||||
|
||||
$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: %
|
||||
if [ "$(go.goos)" = "windows" ]; then \
|
||||
tar -cJf $@ $^.exe; \
|
||||
else \
|
||||
tar -cJf $@ $^; \
|
||||
fi
|
||||
|
||||
.PHONY:
|
||||
clean:
|
||||
rm -rf $(out.tools) $(out.dist)
|
|
@ -0,0 +1,5 @@
|
|||
**Issue:** add link to pelletier/go-toml issue here
|
||||
|
||||
Explanation of what this pull request does.
|
||||
|
||||
More detailed description of the decisions being made and the reasons why (if the patch is non-trivial).
|
|
@ -0,0 +1,151 @@
|
|||
# go-toml
|
||||
|
||||
Go library for the [TOML](https://github.com/mojombo/toml) format.
|
||||
|
||||
This library supports TOML version
|
||||
[v1.0.0-rc.1](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v1.0.0-rc.1.md)
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml)
|
||||
[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE)
|
||||
[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
|
||||
[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml)
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
|
||||
|
||||
## Features
|
||||
|
||||
Go-toml provides the following features for using data parsed from TOML documents:
|
||||
|
||||
* Load TOML documents from files and string data
|
||||
* Easily navigate TOML structure using Tree
|
||||
* Marshaling and unmarshaling to and from data structures
|
||||
* Line & column position data for all parsed elements
|
||||
* [Query support similar to JSON-Path](query/)
|
||||
* Syntax errors contain line and column numbers
|
||||
|
||||
## Import
|
||||
|
||||
```go
|
||||
import "github.com/pelletier/go-toml"
|
||||
```
|
||||
|
||||
## Usage example
|
||||
|
||||
Read a TOML document:
|
||||
|
||||
```go
|
||||
config, _ := toml.Load(`
|
||||
[postgres]
|
||||
user = "pelletier"
|
||||
password = "mypassword"`)
|
||||
// retrieve data directly
|
||||
user := config.Get("postgres.user").(string)
|
||||
|
||||
// or using an intermediate object
|
||||
postgresConfig := config.Get("postgres").(*toml.Tree)
|
||||
password := postgresConfig.Get("password").(string)
|
||||
```
|
||||
|
||||
Or use Unmarshal:
|
||||
|
||||
```go
|
||||
type Postgres struct {
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
type Config struct {
|
||||
Postgres Postgres
|
||||
}
|
||||
|
||||
doc := []byte(`
|
||||
[Postgres]
|
||||
User = "pelletier"
|
||||
Password = "mypassword"`)
|
||||
|
||||
config := Config{}
|
||||
toml.Unmarshal(doc, &config)
|
||||
fmt.Println("user=", config.Postgres.User)
|
||||
```
|
||||
|
||||
Or use a query:
|
||||
|
||||
```go
|
||||
// use a query to gather elements without walking the tree
|
||||
q, _ := query.Compile("$..[user,password]")
|
||||
results := q.Execute(config)
|
||||
for ii, item := range results.Values() {
|
||||
fmt.Printf("Query result %d: %v\n", ii, item)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation and additional examples are available at
|
||||
[godoc.org](http://godoc.org/github.com/pelletier/go-toml).
|
||||
|
||||
## Tools
|
||||
|
||||
Go-toml provides two handy command line tools:
|
||||
|
||||
* `tomll`: Reads TOML files and lints them.
|
||||
|
||||
```
|
||||
go install github.com/pelletier/go-toml/cmd/tomll
|
||||
tomll --help
|
||||
```
|
||||
* `tomljson`: Reads a TOML file and outputs its JSON representation.
|
||||
|
||||
```
|
||||
go install github.com/pelletier/go-toml/cmd/tomljson
|
||||
tomljson --help
|
||||
```
|
||||
|
||||
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
|
||||
|
||||
```
|
||||
go install github.com/pelletier/go-toml/cmd/jsontoml
|
||||
jsontoml --help
|
||||
```
|
||||
|
||||
### Docker image
|
||||
|
||||
Those tools are also availble as a Docker image from
|
||||
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
|
||||
use `tomljson`:
|
||||
|
||||
```
|
||||
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
|
||||
```
|
||||
|
||||
Only master (`latest`) and tagged versions are published to dockerhub. You
|
||||
can build your own image as usual:
|
||||
|
||||
```
|
||||
docker build -t go-toml .
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
Feel free to report bugs and patches using GitHub's pull requests system on
|
||||
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
|
||||
much appreciated!
|
||||
|
||||
### Run tests
|
||||
|
||||
`go test ./...`
|
||||
|
||||
### Fuzzing
|
||||
|
||||
The script `./fuzz.sh` is available to
|
||||
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
|
||||
|
||||
## Versioning
|
||||
|
||||
Go-toml follows [Semantic Versioning](http://semver.org/). The supported version
|
||||
of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of
|
||||
this document. The last two major versions of Go are supported
|
||||
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)).
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Read [LICENSE](LICENSE).
|
|
@ -0,0 +1,230 @@
|
|||
trigger:
|
||||
- master
|
||||
|
||||
stages:
|
||||
- stage: fuzzit
|
||||
displayName: "Run Fuzzit"
|
||||
dependsOn: []
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
|
||||
jobs:
|
||||
- job: submit
|
||||
displayName: "Submit"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go 1.15"
|
||||
inputs:
|
||||
version: "1.15"
|
||||
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
|
||||
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
|
||||
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
filePath: './fuzzit.sh'
|
||||
env:
|
||||
TYPE: fuzzing
|
||||
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
|
||||
|
||||
- stage: run_checks
|
||||
displayName: "Check"
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job: fmt
|
||||
displayName: "fmt"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go 1.15"
|
||||
inputs:
|
||||
version: "1.15"
|
||||
- task: Go@0
|
||||
displayName: "go fmt ./..."
|
||||
inputs:
|
||||
command: 'custom'
|
||||
customCommand: 'fmt'
|
||||
arguments: './...'
|
||||
- job: coverage
|
||||
displayName: "coverage"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go 1.15"
|
||||
inputs:
|
||||
version: "1.15"
|
||||
- task: Go@0
|
||||
displayName: "Generate coverage"
|
||||
inputs:
|
||||
command: 'test'
|
||||
arguments: "-race -coverprofile=coverage.txt -covermode=atomic"
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}'
|
||||
env:
|
||||
CODECOV_TOKEN: $(CODECOV_TOKEN)
|
||||
- job: benchmark
|
||||
displayName: "benchmark"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go 1.15"
|
||||
inputs:
|
||||
version: "1.15"
|
||||
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
filePath: './benchmark.sh'
|
||||
arguments: "master $(Build.Repository.Uri)"
|
||||
|
||||
- job: fuzzing
|
||||
displayName: "fuzzing"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go 1.15"
|
||||
inputs:
|
||||
version: "1.15"
|
||||
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
|
||||
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
|
||||
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
filePath: './fuzzit.sh'
|
||||
env:
|
||||
TYPE: local-regression
|
||||
|
||||
- job: go_unit_tests
|
||||
displayName: "unit tests"
|
||||
strategy:
|
||||
matrix:
|
||||
linux 1.15:
|
||||
goVersion: '1.15'
|
||||
imageName: 'ubuntu-latest'
|
||||
mac 1.15:
|
||||
goVersion: '1.15'
|
||||
imageName: 'macOS-latest'
|
||||
windows 1.15:
|
||||
goVersion: '1.15'
|
||||
imageName: 'windows-latest'
|
||||
linux 1.14:
|
||||
goVersion: '1.14'
|
||||
imageName: 'ubuntu-latest'
|
||||
mac 1.14:
|
||||
goVersion: '1.14'
|
||||
imageName: 'macOS-latest'
|
||||
windows 1.14:
|
||||
goVersion: '1.14'
|
||||
imageName: 'windows-latest'
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go $(goVersion)"
|
||||
inputs:
|
||||
version: $(goVersion)
|
||||
- task: Go@0
|
||||
displayName: "go test ./..."
|
||||
inputs:
|
||||
command: 'test'
|
||||
arguments: './...'
|
||||
- stage: build_binaries
|
||||
displayName: "Build binaries"
|
||||
dependsOn: run_checks
|
||||
jobs:
|
||||
- job: build_binary
|
||||
displayName: "Build binary"
|
||||
strategy:
|
||||
matrix:
|
||||
linux_amd64:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
darwin_amd64:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
windows_amd64:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go"
|
||||
inputs:
|
||||
version: 1.15
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: "make dist"
|
||||
env:
|
||||
go.goos: $(GOOS)
|
||||
go.goarch: $(GOARCH)
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
sourceFolder: '$(Build.SourcesDirectory)'
|
||||
contents: '*.tar.xz'
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
artifactName: binaries
|
||||
- stage: build_binaries_manifest
|
||||
displayName: "Build binaries manifest"
|
||||
dependsOn: build_binaries
|
||||
jobs:
|
||||
- job: build_manifest
|
||||
displayName: "Build binaries manifest"
|
||||
steps:
|
||||
- task: DownloadBuildArtifacts@0
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
downloadType: 'single'
|
||||
artifactName: 'binaries'
|
||||
downloadPath: '$(Build.SourcesDirectory)'
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt"
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
artifactName: manifest
|
||||
|
||||
- stage: build_docker_image
|
||||
displayName: "Build Docker image"
|
||||
dependsOn: run_checks
|
||||
jobs:
|
||||
- job: build
|
||||
displayName: "Build"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
command: 'build'
|
||||
Dockerfile: 'Dockerfile'
|
||||
buildContext: '.'
|
||||
addPipelineData: false
|
||||
|
||||
- stage: publish_docker_image
|
||||
displayName: "Publish Docker image"
|
||||
dependsOn: build_docker_image
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
|
||||
jobs:
|
||||
- job: publish
|
||||
displayName: "Publish"
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'DockerHub'
|
||||
repository: 'pelletier/go-toml'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
buildContext: '.'
|
||||
tags: 'latest'
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
reference_ref=${1:-master}
|
||||
reference_git=${2:-.}
|
||||
|
||||
if ! `hash benchstat 2>/dev/null`; then
|
||||
echo "Installing benchstat"
|
||||
go get golang.org/x/perf/cmd/benchstat
|
||||
fi
|
||||
|
||||
tempdir=`mktemp -d /tmp/go-toml-benchmark-XXXXXX`
|
||||
ref_tempdir="${tempdir}/ref"
|
||||
ref_benchmark="${ref_tempdir}/benchmark-`echo -n ${reference_ref}|tr -s '/' '-'`.txt"
|
||||
local_benchmark="`pwd`/benchmark-local.txt"
|
||||
|
||||
echo "=== ${reference_ref} (${ref_tempdir})"
|
||||
git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null
|
||||
pushd ${ref_tempdir} >/dev/null
|
||||
git checkout ${reference_ref} >/dev/null 2>/dev/null
|
||||
go test -bench=. -benchmem | tee ${ref_benchmark}
|
||||
cd benchmark
|
||||
go test -bench=. -benchmem | tee -a ${ref_benchmark}
|
||||
popd >/dev/null
|
||||
|
||||
echo ""
|
||||
echo "=== local"
|
||||
go test -bench=. -benchmem | tee ${local_benchmark}
|
||||
cd benchmark
|
||||
go test -bench=. -benchmem | tee -a ${local_benchmark}
|
||||
|
||||
echo ""
|
||||
echo "=== diff"
|
||||
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
|
|
@ -0,0 +1,23 @@
|
|||
// Package toml is a TOML parser and manipulation library.
|
||||
//
|
||||
// This version supports the specification as described in
|
||||
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
|
||||
//
|
||||
// Marshaling
|
||||
//
|
||||
// Go-toml can marshal and unmarshal TOML documents from and to data
|
||||
// structures.
|
||||
//
|
||||
// TOML document as a tree
|
||||
//
|
||||
// Go-toml can operate on a TOML document as a tree. Use one of the Load*
|
||||
// functions to parse TOML data and obtain a Tree instance, then one of its
|
||||
// methods to manipulate the tree.
|
||||
//
|
||||
// JSONPath-like queries
|
||||
//
|
||||
// The package github.com/pelletier/go-toml/query implements a system
|
||||
// similar to JSONPath to quickly retrieve elements of a TOML document using a
|
||||
// single expression. See the package documentation for more information.
|
||||
//
|
||||
package toml
|
|
@ -0,0 +1,30 @@
|
|||
# This is a TOML document. Boom.
|
||||
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
organization = "GitHub"
|
||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [ 8001, 8001, 8002 ]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
|
||||
[servers]
|
||||
|
||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
dc = "eqdc10"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
dc = "eqdc10"
|
||||
|
||||
[clients]
|
||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
||||
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
|
|
@ -0,0 +1,30 @@
|
|||
# This is a TOML document. Boom.
|
||||
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
organization = "GitHub"
|
||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [ 8001, 8001, 8002 ]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
|
||||
[servers]
|
||||
|
||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
dc = "eqdc10"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
dc = "eqdc10"
|
||||
|
||||
[clients]
|
||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
||||
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
|
|
@ -0,0 +1,31 @@
|
|||
// +build gofuzz
|
||||
|
||||
package toml
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
tree, err := LoadBytes(data)
|
||||
if err != nil {
|
||||
if tree != nil {
|
||||
panic("tree must be nil if there is an error")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
str, err := tree.ToTomlString()
|
||||
if err != nil {
|
||||
if str != "" {
|
||||
panic(`str must be "" if there is an error`)
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tree, err = Load(str)
|
||||
if err != nil {
|
||||
if tree != nil {
|
||||
panic("tree must be nil if there is an error")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#! /bin/sh
|
||||
set -eu
|
||||
|
||||
go get github.com/dvyukov/go-fuzz/go-fuzz
|
||||
go get github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
|
||||
if [ ! -e toml-fuzz.zip ]; then
|
||||
go-fuzz-build github.com/pelletier/go-toml
|
||||
fi
|
||||
|
||||
rm -fr fuzz
|
||||
mkdir -p fuzz/corpus
|
||||
cp *.toml fuzz/corpus
|
||||
|
||||
go-fuzz -bin=toml-fuzz.zip -workdir=fuzz
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue