From 30c6473e568c23d6bf2d962363131334c6ea8271 Mon Sep 17 00:00:00 2001 From: quest Date: Thu, 24 Oct 2024 22:28:18 -0300 Subject: [PATCH] geoclue-tz initial release --- .gitignore | 3 + .travis.yml | 20 ++++ CODE_OF_CONDUCT.md | 134 +++++++++++++++++++++++++ CONTRIBUTING.md | 9 ++ Makefile | 25 +++++ README.md | 115 +++++++++++++++++++++- cmd/config.go | 73 ++++++++++++++ cmd/generate.go | 205 +++++++++++++++++++++++++++++++++++++++ geoclue-tz.go | 17 ++++ go.mod | 29 ++++++ go.sum | 57 +++++++++++ pull_request_template.md | 13 +++ signals.go | 18 ++++ tz/location.go | 73 ++++++++++++++ tz/tz.go | 53 ++++++++++ tz/zone.go | 81 ++++++++++++++++ 16 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 cmd/config.go create mode 100644 cmd/generate.go create mode 100644 geoclue-tz.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pull_request_template.md create mode 100644 signals.go create mode 100644 tz/location.go create mode 100644 tz/tz.go create mode 100644 tz/zone.go diff --git a/.gitignore b/.gitignore index 6f72f89..b60fa96 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # env file .env + +# binary +geoclue-tz \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cd0a331 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: go +go: + - 1.23.2 + - 1.22.2 + - 1.21.9 +os: + - linux +install: + - go get -v github.com/zquestz/geoclue-tz +script: + - go build + - go fmt ./... + - go vet ./... + - go test -i -race ./... + - go test -v -race ./... +after_script: + - if [ "$TRAVIS_GO_VERSION" = "1.23.2" ] && [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$TRAVIS_TAG" != "" ]; then go get github.com/inconshreveable/mousetrap; fi + - if [ "$TRAVIS_GO_VERSION" = "1.23.2" ] && [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$TRAVIS_TAG" != "" ]; then go install github.com/mitchellh/gox; fi + - if [ "$TRAVIS_GO_VERSION" = "1.23.2" ] && [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$TRAVIS_TAG" != "" ]; then go install github.com/tcnksm/ghr; fi + - if [ "$TRAVIS_GO_VERSION" = "1.23.2" ] && [ "$TRAVIS_OS_NAME" = "linux" ] && [ "$TRAVIS_TAG" != "" ]; then make compile; ghr --username zquestz --token $GITHUB_TOKEN --replace $TRAVIS_TAG pkg/; fi diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a54c2fd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,134 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[quest@mac.com](mailto:quest@mac.com). + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9d41a31 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# geoclue-tz Contribution Guidelines + +## Rules + +There are a few basic ground-rules for contributors to `geoclue-tz`: + +1. When doing refactors to the base code, make sure to discuss it in an issue prior to PR. +2. Use gofmt on all files. +3. Profit! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3781006 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +APPNAME = geoclue-tz +OUTDIR = pkg + +# Allow user to override cross compilation scope +OSARCH ?= dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 +DIRS ?= dragonfly_amd64 freebsd_386 freebsd_amd64 freebsd_arm linux_386 linux_amd64 linux_arm netbsd_386 netbsd_amd64 netbsd_arm openbsd_386 openbsd_amd64 + +all: + go build . + +compile: + gox -osarch="$(OSARCH)" -output "$(OUTDIR)/$(APPNAME)-{{.OS}}_{{.Arch}}/$(APPNAME)" + @for dir in $(DIRS) ; do \ + (cp README.md $(OUTDIR)/$(APPNAME)-$$dir/README.md) ;\ + (cp LICENSE $(OUTDIR)/$(APPNAME)-$$dir/LICENSE) ;\ + (cd $(OUTDIR) && zip -q $(APPNAME)-$$dir.zip -r $(APPNAME)-$$dir) ;\ + echo "make $(OUTDIR)/$(APPNAME)-$$dir.zip" ;\ + done + +install: + go install . + +uninstall: + go clean -i + diff --git a/README.md b/README.md index a2ac6c3..b405962 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,115 @@ # geoclue-tz -Write geoclue /etc/geolocation based on tz zone info. + +[![License][License-Image]][License-URL] [![ReportCard][ReportCard-Image]][ReportCard-URL] [![Build][Build-Status-Image]][Build-Status-URL] [![Release][Release-Image]][Release-URL] + +Generate geoclue /etc/geolocation based on the current time zone. + +```text +Usage: + geoclue-tz [flags] + +Flags: + -c, --completion string completion script for bash, zsh or fish + -d, --dry-run dry run debug mode + -h, --help help for geoclue-tz + -l, --location string enable custom location + --version display version +``` + +## Usage + +Update `/etc/geolocation` based on the current time zone. + +```zsh +geoclue-tz +``` + +Update `/etc/geolocation` based on your custom home location. + +```zsh +geoclue-tz -l home +``` + +## Install + +Make sure that `GOPATH` and `GOBIN` env vars are set. Then run: + +```zsh +go install github.com/zquestz/geoclue-tz@latest +``` + +Arch Linux users can install from the AUR: + +```zsh +yay -S geoclue-tz +``` + +## Configuration + +To setup your own configuration just create `/etc/geoclue-tz.conf`. The configuration file is in UCL format. This makes it super easy to set the values for your custom locations, and restore them whenever you want. + +For more information about UCL visit: +[https://github.com/vstakhov/libucl](https://github.com/vstakhov/libucl) + +The following keys are supported: + +* locations - array (custom locations) +* verbose - bool (verbose mode) +* dryRun - bool (dry run mode) + +Here is a sample configuration, with a single custom location. The only required keys are `latitude`, `longitude`, and `name`. + +```text +locations [ + { + latitude = 19.520960 + longitude = 155.920517 + altitude = 0 + accuracy = 1000 + name = home + } +] +``` + +## Shell Autocompletion + +To set up autocompletion: + +### Bash Linux + +```zsh +geoclue-tz --completion bash > /etc/bash_completion.d/geoclue-tz +``` + +### Bash MacOS + +```zsh +geoclue-tz --completion bash > /usr/local/etc/bash_completion.d/geoclue-tz +``` + +### Zsh + +Generate a `_geoclue-tz` completion script and put it somewhere in your `$fpath`: + +```zsh +geoclue-tz --completion zsh > /usr/local/share/zsh/site-functions/_geoclue-tz +``` + +### Fish + +```zsh +geoclue-tz --completion fish > ~/.config/fish/completions/geoclue-tz.fish +``` + +## License + +geoclue-tz is released under the MIT license. + +[License-URL]: http://opensource.org/licenses/MIT +[License-Image]: https://img.shields.io/npm/l/express.svg +[ReportCard-URL]: http://goreportcard.com/report/zquestz/geoclue-tz +[ReportCard-Image]: https://goreportcard.com/badge/github.com/zquestz/geoclue-tz +[Build-Status-URL]: https://app.travis-ci.com/github/zquestz/geoclue-tz +[Build-Status-Image]: https://app.travis-ci.com/zquestz/geoclue-tz.svg?branch=main +[Release-URL]: https://github.com/zquestz/geoclue-tz/releases/tag/v1.0.0 +[Release-Image]: http://img.shields.io/badge/geoclue-tz-v1.0.0-1eb0fc.svg diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..7b07492 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/zquestz/geoclue-tz/tz" + "github.com/zquestz/go-ucl" +) + +// Config stores all the application configuration. +type Config struct { + Locations []*tz.Location `json:"locations"` + DisplayVersion bool `json:"-"` + Location string `json:"location"` + DryRun bool `json:"dryRun,string"` + Completion string `json:"completion"` +} + +// Load reads the configuration from /etc/geoclue-tz.conf +// and loads it into the Config struct. +// The config is in UCL format. +func (c *Config) Load() error { + conf, err := c.loadConfig() + if err != nil { + return err + } + + // Conf is not required. + if conf != nil { + err = c.applyConf(conf) + if err != nil { + return err + } + } + + return nil +} + +func (c *Config) loadConfig() ([]byte, error) { + f, err := os.Open(filepath.Join("/etc", "geoclue-tz.conf")) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } else { + return nil, err + } + } + defer f.Close() + + ucl.Ucldebug = false + data, err := ucl.NewParser(f).Ucl() + if err != nil { + return nil, err + } + + conf, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return conf, nil +} + +func (c *Config) applyConf(conf []byte) error { + err := json.Unmarshal(conf, c) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 0000000..d928df8 --- /dev/null +++ b/cmd/generate.go @@ -0,0 +1,205 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + "unicode" + + "github.com/spf13/cobra" + "github.com/zquestz/geoclue-tz/tz" +) + +const ( + appName = "geoclue-tz" + version = "1.0.0" +) + +// Stores configuration data. +var config Config + +// GenerateCmd is the main command for the application. +var GenerateCmd = &cobra.Command{ + Use: "geoclue-tz", + Short: "Generate geoclue /etc/geolocation based on the current time zone.", + Long: `Generate geoclue /etc/geolocation based on the current time zone.`, + Run: func(cmd *cobra.Command, args []string) { + err := performCommand(cmd, args) + if err != nil { + bail(err) + } + }, +} + +func init() { + err := config.Load() + if err != nil { + bail(fmt.Errorf("failed to load configuration: %w", err)) + } + + prepareFlags() +} + +func bail(err error) { + fmt.Fprintf(os.Stderr, "[Error] %s\n", capitalize(err.Error())) + os.Exit(1) +} + +func capitalize(str string) string { + if len(str) == 0 { + return "" + } + tmp := []rune(str) + tmp[0] = unicode.ToUpper(tmp[0]) + return string(tmp) +} + +func completion(cmd *cobra.Command, c string) { + switch c { + case "bash": + err := cmd.GenBashCompletion(os.Stdout) + if err != nil { + bail(fmt.Errorf("failed to generate bash completion: %w", err)) + } + case "zsh": + if err := cmd.GenZshCompletion(os.Stdout); err != nil { + bail(fmt.Errorf("failed to generate zsh completion: %w", err)) + } + case "fish": + if err := cmd.GenFishCompletion(os.Stdout, true); err != nil { + bail(fmt.Errorf("failed to generate fish completion: %w", err)) + } + default: + bail(fmt.Errorf("completion not supported: %q", c)) + } +} + +func prepareFlags() { + GenerateCmd.PersistentFlags().BoolVar( + &config.DisplayVersion, "version", false, "display version") + GenerateCmd.PersistentFlags().BoolVarP( + &config.DryRun, "dry-run", "d", config.DryRun, "dry run debug mode") + GenerateCmd.PersistentFlags().StringVarP( + &config.Location, "location", "l", config.Location, "enable custom location") + GenerateCmd.PersistentFlags().StringVarP( + &config.Completion, "completion", "c", config.Completion, "completion script for bash, zsh or fish") + + err := GenerateCmd.RegisterFlagCompletionFunc("completion", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"bash", "fish", "zsh"}, cobra.ShellCompDirectiveNoFileComp + }) + if err != nil { + bail(err) + } + + err = GenerateCmd.RegisterFlagCompletionFunc("location", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return listLocations(), cobra.ShellCompDirectiveNoFileComp + }) + if err != nil { + bail(err) + } +} + +// Where all the work happens. +func performCommand(cmd *cobra.Command, args []string) error { + if config.Completion != "" { + completion(cmd, config.Completion) + return nil + } + + if config.DisplayVersion { + fmt.Printf("%s %s\n", appName, version) + return nil + } + + if len(args) != 0 { + help := cmd.HelpFunc() + help(cmd, args) + } + + if config.Location != "" { + location, err := buildLocation(config.Location) + if err != nil { + return err + } + + err = location.WriteGeolocation(config.DryRun) + if err != nil { + return fmt.Errorf("unable to write %q: %w", tz.EtcGeolocation, err) + } + + return nil + } + + location, err := Location() + if err != nil { + return fmt.Errorf("unable to find location: %w", err) + } + + err = location.WriteGeolocation(config.DryRun) + if err != nil { + return fmt.Errorf("unable to write %q: %w", tz.EtcGeolocation, err) + } + + return nil +} + +// Find the lat/long entry for the current time zone +// in /usr/share/zoneinfo/zone.tab. +func Location() (*tz.Location, error) { + tzName, err := tz.LocalTZ() + if err != nil { + return nil, err + } + + if config.DryRun { + fmt.Printf("Time Zone: %q\n", tzName) + } + + entry, err := tz.ZoneEntry(tzName, config.DryRun) + if err != nil { + return nil, err + } + + if config.DryRun { + fmt.Printf("Location: %#v\n", *entry) + } + + return entry, nil +} + +// listLocations returns a list of custom locations. +func listLocations() []string { + locations := []string{} + for _, loc := range config.Locations { + locations = append(locations, loc.Name) + } + + return locations +} + +// Returns the custom location, +func buildLocation(location string) (*tz.Location, error) { + for _, loc := range config.Locations { + if strings.ToLower(loc.Name) == strings.ToLower(location) { + if loc.Latitude != 0 && loc.Longitude != 0 { + l := &tz.Location{ + Latitude: loc.Latitude, + Longitude: loc.Longitude, + Altitude: loc.Altitude, + Accuracy: loc.Accuracy, + Name: loc.Name, + } + + if config.DryRun { + fmt.Printf("Location: %#v\n", *l) + } + + return l, nil + } + + return nil, fmt.Errorf("location lat/long not provided: %q", location) + } + } + + return nil, fmt.Errorf("location not found: %q", location) +} diff --git a/geoclue-tz.go b/geoclue-tz.go new file mode 100644 index 0000000..ebdf067 --- /dev/null +++ b/geoclue-tz.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + + "github.com/zquestz/geoclue-tz/cmd" +) + +func main() { + setupSignalHandlers() + + if err := cmd.GenerateCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d70d28d --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/zquestz/geoclue-tz + +go 1.23.2 + +require ( + github.com/Songmu/retry v0.1.0 // indirect + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-github/v66 v66.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/gox v1.0.1 // indirect + github.com/mitchellh/iochan v1.0.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tcnksm/ghr v0.17.0 // indirect + github.com/tcnksm/go-gitconfig v0.1.2 // indirect + github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect + github.com/thediveo/enumflag/v2 v2.0.5 // indirect + github.com/zquestz/go-ucl v0.0.0-20220615095619-8a3686d7543a // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2bea888 --- /dev/null +++ b/go.sum @@ -0,0 +1,57 @@ +github.com/Songmu/retry v0.1.0 h1:hPA5xybQsksLR/ry/+t/7cFajPW+dqjmjhzZhioBILA= +github.com/Songmu/retry v0.1.0/go.mod h1:7sXIW7eseB9fq0FUvigRcQMVLR9tuHI0Scok+rkpAuA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= +github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +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/tcnksm/ghr v0.17.0 h1:tunAIxrmGg6mJmTzejxf/f7W3d+Ag8tYapKo0x/jCEM= +github.com/tcnksm/ghr v0.17.0/go.mod h1:2NBhDz6Y7S+EjIS00MufyUcsz15JJXuwmhEsdWp+d4I= +github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= +github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= +github.com/thediveo/enumflag/v2 v2.0.5 h1:VJjvlAqUb6m6mxOrB/0tfBJI0Kvi9wJ8ulh38xK87i8= +github.com/thediveo/enumflag/v2 v2.0.5/go.mod h1:0NcG67nYgwwFsAvoQCmezG0J0KaIxZ0f7skg9eLq1DA= +github.com/zquestz/go-ucl v0.0.0-20220615095619-8a3686d7543a h1:Dvd4T0NxSAwRRMQ+dN/t3UIWSedKxAwJzDNdz8RtQ4c= +github.com/zquestz/go-ucl v0.0.0-20220615095619-8a3686d7543a/go.mod h1:M54hiL2fkAYVcY+k9MgIhTeEHwCfzFK6jRN8aBuPug4= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..6ca1798 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,13 @@ +# Pull Request Template + +## Description + +A short summary of your changes. + +Fixes # (issue) + +## Checklist + +- [ ] All files have been linted with gofmt. +- [ ] Documentation has been updated. +- [ ] All tests pass. diff --git a/signals.go b/signals.go new file mode 100644 index 0000000..3f82d94 --- /dev/null +++ b/signals.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + "os/signal" +) + +func setupSignalHandlers() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + go interuptSignal(c) +} + +func interuptSignal(c <-chan os.Signal) { + <-c + os.Exit(0) +} diff --git a/tz/location.go b/tz/location.go new file mode 100644 index 0000000..1ed286b --- /dev/null +++ b/tz/location.go @@ -0,0 +1,73 @@ +package tz + +import ( + "errors" + "fmt" + "os" + "os/user" + "strconv" +) + +const ( + EtcGeolocation = "/etc/geolocation" +) + +// Location stores location information. +type Location struct { + Latitude float32 `json:"latitude,string"` + Longitude float32 `json:"longitude,string"` + Altitude float32 `json:"altitude,string"` + Accuracy float32 `json:"accuracy,string"` + Name string `json:"name"` +} + +func (l *Location) WriteGeolocation(dryRun bool) error { + if dryRun { + return nil + } + + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("unable to get current user: %w", err) + } + + if currentUser.Uid != "0" { + return errors.New("root access required") + } + + geoclueUser, err := user.Lookup("geoclue") + if err != nil { + return err + } + + geoclueUserId, err := strconv.ParseInt(geoclueUser.Uid, 10, 0) + if err != nil { + return err + } + + err = os.WriteFile(EtcGeolocation, []byte(l.Output()), 0600) + if err != nil { + return err + } + + err = os.Chown(EtcGeolocation, int(geoclueUserId), 0) + if err != nil { + return err + } + + fmt.Printf("Successfully updated %q with %q location\n", EtcGeolocation, l.Name) + + return nil +} + +// Output formats the Location +// for /etc/geolocation. +func (l *Location) Output() string { + return fmt.Sprintf( + "%v\n%v\n%v\n%v", + l.Latitude, + l.Longitude, + l.Altitude, + l.Accuracy, + ) +} diff --git a/tz/tz.go b/tz/tz.go new file mode 100644 index 0000000..47b39d9 --- /dev/null +++ b/tz/tz.go @@ -0,0 +1,53 @@ +package tz + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +const localtime = "/etc/localtime" + +// LocalTZ returns the current IANA time zone of the system. +// Only works on Unix systems. +func LocalTZ() (string, error) { + fi, err := os.Lstat(localtime) + if err != nil { + err = fmt.Errorf("failed to stat %q: %w", localtime, err) + return "", err + } + + if (fi.Mode() & os.ModeSymlink) == 0 { + err = fmt.Errorf("%q is not a symlink", localtime) + return "", err + } + + p, err := os.Readlink(localtime) + if err != nil { + return "", err + } + + name, err := inferTZFromPath(p) + if err != nil { + return "", err + } + + return name, nil +} + +func inferTZFromPath(p string) (string, error) { + parts := strings.Split(p, string(filepath.Separator)) + for i := range parts { + if parts[i] == "zoneinfo" { + parts = parts[i+1:] + break + } + } + + if len(parts) < 1 { + return "", fmt.Errorf("unable to infer time zone from path: %q", p) + } + + return filepath.Join(parts...), nil +} diff --git a/tz/zone.go b/tz/zone.go new file mode 100644 index 0000000..584e755 --- /dev/null +++ b/tz/zone.go @@ -0,0 +1,81 @@ +package tz + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strconv" + "strings" +) + +const zonetab = "/usr/share/zoneinfo/zone.tab" + +func ZoneEntry(name string, dryRun bool) (*Location, error) { + file, err := os.Open(zonetab) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + row := strings.Split(scanner.Text(), "\t") + + if len(row) >= 2 && row[2] == name { + if dryRun { + fmt.Printf("Zone Entry: %#v\n", row) + } + + r, err := regexp.Compile(`([+|-]\d+)([+|-]\d+)`) + if err != nil { + return nil, err + } + + matches := r.FindStringSubmatch(row[1]) + if len(matches) < 3 { + return nil, fmt.Errorf("failed to parse coordinates: %q", row[1]) + } + + lat, err := convertCoordinates(matches[1], 2) + if err != nil { + return nil, err + } + + long, err := convertCoordinates(matches[2], 3) + if err != nil { + return nil, err + } + + return &Location{ + Latitude: lat, + Longitude: long, + Altitude: 0, + Accuracy: 1000, + Name: name, + }, nil + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return nil, fmt.Errorf("time zone entry not found in %q", zonetab) +} + +func convertCoordinates(coordinate string, insertIndex int) (float32, error) { + coordBytes := []byte(strings.Trim(coordinate, "+")) + if coordBytes[0] == '-' { + insertIndex += 1 + } + + coordStr := fmt.Sprintf("%s%s%s", coordBytes[:insertIndex], ".", coordBytes[insertIndex:]) + + coord, err := strconv.ParseFloat(coordStr, 32) + if err != nil { + return 0, err + } + + return float32(coord), nil +}