From c4e2db32b58a3a40f67f0066374935e8df8265d5 Mon Sep 17 00:00:00 2001 From: crapStone Date: Tue, 6 Oct 2020 13:06:47 +0000 Subject: [PATCH] rewrote config file path search (#219) added comment to clarify coding choices added package xdg to vendor folder rewrote config file path search Co-authored-by: crapStone Reviewed-on: https://gitea.com/gitea/tea/pulls/219 Reviewed-by: 6543 <6543@noreply.gitea.io> Reviewed-by: Norwin --- go.mod | 1 + go.sum | 2 + modules/config/config.go | 49 ++--- modules/config/login.go | 6 +- modules/utils/home.go | 95 --------- vendor/github.com/adrg/xdg/.travis.yml | 19 ++ vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md | 77 ++++++++ vendor/github.com/adrg/xdg/CONTRIBUTING.md | 135 +++++++++++++ vendor/github.com/adrg/xdg/LICENSE | 21 ++ vendor/github.com/adrg/xdg/README.md | 187 ++++++++++++++++++ vendor/github.com/adrg/xdg/base_dirs.go | 78 ++++++++ vendor/github.com/adrg/xdg/go.mod | 1 + vendor/github.com/adrg/xdg/paths_darwin.go | 37 ++++ vendor/github.com/adrg/xdg/paths_unix.go | 54 +++++ vendor/github.com/adrg/xdg/paths_windows.go | 69 +++++++ vendor/github.com/adrg/xdg/user_dirs.go | 40 ++++ vendor/github.com/adrg/xdg/utils.go | 126 ++++++++++++ vendor/github.com/adrg/xdg/xdg.go | 180 +++++++++++++++++ vendor/modules.txt | 2 + 19 files changed, 1059 insertions(+), 120 deletions(-) delete mode 100644 modules/utils/home.go create mode 100644 vendor/github.com/adrg/xdg/.travis.yml create mode 100644 vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/adrg/xdg/CONTRIBUTING.md create mode 100644 vendor/github.com/adrg/xdg/LICENSE create mode 100644 vendor/github.com/adrg/xdg/README.md create mode 100644 vendor/github.com/adrg/xdg/base_dirs.go create mode 100644 vendor/github.com/adrg/xdg/go.mod create mode 100644 vendor/github.com/adrg/xdg/paths_darwin.go create mode 100644 vendor/github.com/adrg/xdg/paths_unix.go create mode 100644 vendor/github.com/adrg/xdg/paths_windows.go create mode 100644 vendor/github.com/adrg/xdg/user_dirs.go create mode 100644 vendor/github.com/adrg/xdg/utils.go create mode 100644 vendor/github.com/adrg/xdg/xdg.go diff --git a/go.mod b/go.mod index 2e036e3..1094212 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( code.gitea.io/gitea-vet v0.2.0 code.gitea.io/sdk/gitea v0.13.0 github.com/AlecAivazis/survey/v2 v2.1.1 + github.com/adrg/xdg v0.2.1 github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 github.com/charmbracelet/glamour v0.2.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 70b4b51..62a9b37 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0T github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic= +github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= diff --git a/modules/config/config.go b/modules/config/config.go index e5a5cf6..34913af 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -9,13 +9,13 @@ import ( "fmt" "io/ioutil" "log" - "os" "path/filepath" "strings" "code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/utils" + "github.com/adrg/xdg" "gopkg.in/yaml.v2" ) @@ -26,29 +26,34 @@ type LocalConfig struct { var ( // Config contain if loaded local tea config - Config LocalConfig - yamlConfigPath string + Config LocalConfig ) -// TODO: do not use init function to detect the tea configuration, use GetConfigPath() -func init() { - homeDir, err := utils.Home() - if err != nil { - log.Fatal("Retrieve home dir failed") - } - - dir := filepath.Join(homeDir, ".tea") - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - log.Fatal("Init tea config dir " + dir + " failed") - } - - yamlConfigPath = filepath.Join(dir, "tea.yml") -} - // GetConfigPath return path to tea config file func GetConfigPath() string { - return yamlConfigPath + configFilePath, err := xdg.ConfigFile("tea/config.yml") + + var exists bool + if err != nil { + exists = false + } else { + exists, _ = utils.PathExists(configFilePath) + } + + // fallback to old config if no new one exists + if !exists { + file := filepath.Join(xdg.Home, ".tea", "tea.yml") + exists, _ = utils.PathExists(file) + if exists { + return file + } + } + + if err != nil { + log.Fatal("unable to get or create config file") + } + + return configFilePath } // LoadConfig load config into global Config var @@ -58,12 +63,12 @@ func LoadConfig() error { if exist { bs, err := ioutil.ReadFile(ymlPath) if err != nil { - return err + return fmt.Errorf("Failed to read config file: %s", ymlPath) } err = yaml.Unmarshal(bs, &Config) if err != nil { - return err + return fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) } } diff --git a/modules/config/login.go b/modules/config/login.go index 9d37510..e4488a9 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -131,7 +131,7 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) err := LoadConfig() if err != nil { - log.Fatal("Unable to load config file " + yamlConfigPath) + log.Fatal(err) } for _, l := range Config.Logins { @@ -253,7 +253,7 @@ func InitCommand(repoValue, loginValue, remoteValue string) (*Login, string, str err := LoadConfig() if err != nil { - log.Fatal("load config file failed ", yamlConfigPath) + log.Fatal(err) } if login, err = GetDefaultLogin(); err != nil { @@ -287,7 +287,7 @@ func InitCommand(repoValue, loginValue, remoteValue string) (*Login, string, str func InitCommandLoginOnly(loginValue string) *Login { err := LoadConfig() if err != nil { - log.Fatal("load config file failed ", yamlConfigPath) + log.Fatal(err) } var login *Login diff --git a/modules/utils/home.go b/modules/utils/home.go deleted file mode 100644 index 47361c9..0000000 --- a/modules/utils/home.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package utils - -import ( - "bytes" - "errors" - "os" - "os/exec" - "os/user" - "runtime" - "strconv" - "strings" -) - -// Home returns the home directory for the executing user. -// -// This uses an OS-specific method for discovering the home directory. -// An error is returned if a home directory cannot be detected. -func Home() (string, error) { - user, err := user.Current() - if nil == err { - return user.HomeDir, nil - } - - // cross compile support - if "windows" == runtime.GOOS { - return homeWindows() - } - - // Unix-like system, so just assume Unix - return homeUnix() -} - -func homeUnix() (string, error) { - // First prefer the HOME environmental variable - if home := os.Getenv("HOME"); home != "" { - return home, nil - } - - // If that fails, try getent - var stdout bytes.Buffer - cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) - cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - // If the error is ErrNotFound, we ignore it. Otherwise, return it. - if err != exec.ErrNotFound { - return "", err - } - } else { - if passwd := strings.TrimSpace(stdout.String()); passwd != "" { - // username:password:uid:gid:gecos:home:shell - passwdParts := strings.SplitN(passwd, ":", 7) - if len(passwdParts) > 5 { - return passwdParts[5], nil - } - } - } - - // If all else fails, try the shell - stdout.Reset() - cmd = exec.Command("sh", "-c", "cd && pwd") - cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - return "", err - } - - result := strings.TrimSpace(stdout.String()) - if result == "" { - return "", errors.New("blank output when reading home directory") - } - - return result, nil -} - -func homeWindows() (string, error) { - // First prefer the HOME environmental variable - if home := os.Getenv("HOME"); home != "" { - return home, nil - } - - drive := os.Getenv("HOMEDRIVE") - path := os.Getenv("HOMEPATH") - home := drive + path - if drive == "" || path == "" { - home = os.Getenv("USERPROFILE") - } - if home == "" { - return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") - } - - return home, nil -} diff --git a/vendor/github.com/adrg/xdg/.travis.yml b/vendor/github.com/adrg/xdg/.travis.yml new file mode 100644 index 0000000..7b333cc --- /dev/null +++ b/vendor/github.com/adrg/xdg/.travis.yml @@ -0,0 +1,19 @@ +language: go +go: + - 1.11.x + - 1.12.x + - 1.13.x +os: + - linux + - osx + - windows +env: + - GO111MODULE=on +git: + autocrlf: false +before_install: + - go get -t -v ./... + - go install github.com/golangci/golangci-lint/cmd/golangci-lint +script: + - golangci-lint run --enable-all -D wsl -D gochecknoinits -D gochecknoglobals -D prealloc + - go test -v -race ./... diff --git a/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md b/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..75349e5 --- /dev/null +++ b/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, +body size, disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socio-economic status, nationality, +personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behaviour by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behaviour and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behaviour. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviour that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported by contacting the project team at adrg@epistack.com. All complaints +will be reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/github.com/adrg/xdg/CONTRIBUTING.md b/vendor/github.com/adrg/xdg/CONTRIBUTING.md new file mode 100644 index 0000000..006f146 --- /dev/null +++ b/vendor/github.com/adrg/xdg/CONTRIBUTING.md @@ -0,0 +1,135 @@ +# Contributing to this project + +Contributions in the form of pull requests, issues or just general feedback, +are always welcome. Please take a moment to review this document in order to +make the contribution process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + +## Using the issue tracker + +The issue tracker is the preferred channel for [bug reports](#bugs), +[features requests](#features) and [submitting pull +requests](#pull-requests), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests (use + [Stack Overflow](http://stackoverflow.com) or IRC). +* Please **do not** derail or troll issues. Keep the discussion on topic and + respect the opinions of others. + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. +3. **Isolate the problem** — create a reduced test case. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? What would you expect to be the outcome? All these +details will help people to fix any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the browser/OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + +Please adhere to the coding conventions used throughout a project (indentation, +accurate comments, etc.) and any other requirements (such as test coverage). + +Follow this process if you'd like your work considered for inclusion in the +project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, + and configure the remotes: + + ```bash + # Clone your fork of the repo into the current directory + git clone https://github.com// + # Navigate to the newly cloned directory + cd + # Assign the original repo to a remote called "upstream" + git remote add upstream https://github.com// + ``` + +2. If you cloned a while ago, get the latest changes from upstream: + + ```bash + git checkout + git pull upstream + ``` + +3. Create a new topic branch (off the main project development branch) to + contain your feature, change, or fix: + + ```bash + git checkout -b + ``` + +4. Commit your changes in logical chunks and use descriptive commit messages. + Use [interactive rebase](https://help.github.com/articles/interactive-rebase) + to tidy up your commits before making them public. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + + ```bash + git pull [--rebase] upstream + ``` + +6. Push your topic branch up to your fork: + + ```bash + git push origin + ``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) + with a clear title and description. + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to +license your work under the same license as that used by the project. diff --git a/vendor/github.com/adrg/xdg/LICENSE b/vendor/github.com/adrg/xdg/LICENSE new file mode 100644 index 0000000..7307e1b --- /dev/null +++ b/vendor/github.com/adrg/xdg/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Adrian-George Bostan + +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. diff --git a/vendor/github.com/adrg/xdg/README.md b/vendor/github.com/adrg/xdg/README.md new file mode 100644 index 0000000..a9ad34e --- /dev/null +++ b/vendor/github.com/adrg/xdg/README.md @@ -0,0 +1,187 @@ +xdg +=== +[![Build Status](https://travis-ci.org/adrg/xdg.svg?branch=master)](https://travis-ci.org/adrg/xdg) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/adrg/xdg) +[![License: MIT](https://img.shields.io/badge/license-MIT-red.svg?style=flat-square)](https://opensource.org/licenses/MIT) +[![Go Report Card](https://goreportcard.com/badge/github.com/adrg/xdg)](https://goreportcard.com/report/github.com/adrg/xdg) + +Provides an implementation of the [XDG Base Directory Specification](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html). +The specification defines a set of standard paths for storing application files, +including data and configuration files. For portability and flexibility reasons, +applications should use the XDG defined locations instead of hardcoding paths. +The package also includes the locations of well known user directories. +The current implementation supports Windows, Mac OS and most flavors of Unix. + +Full documentation can be found at: https://godoc.org/github.com/adrg/xdg + +## Installation + go get github.com/adrg/xdg + +## Default locations + +The package defines sensible defaults for XDG variables which are empty or not +present in the environment. + +#### XDG Base Directory + +| | Unix | Mac OS | Windows | +| :--- | :--- | :----- | :--- | +| XDG_DATA_HOME | `~/.local/share` | `~/Library/Application Support` | `%LOCALAPPDATA%` | +| XDG_DATA_DIRS | `/usr/local/share`
`/usr/share` | `/Library/Application Support` | `%APPDATA%\Roaming`
`%PROGRAMDATA%` | +| XDG_CONFIG_HOME | `~/.config` | `~/Library/Preferences` | `%LOCALAPPDATA%` | +| XDG_CONFIG_DIRS | `/etc/xdg` | `/Library/Preferences` | `%PROGRAMDATA%` | +| XDG_CACHE_HOME | `~/.cache` | `~/Library/Caches` | `%LOCALAPPDATA%\cache` | +| XDG_RUNTIME_DIR | `/run/user/UID` | `~/Library/Application Support` | `%LOCALAPPDATA%` | + +#### XDG user directories + +| | Unix | Mac OS | Windows | +| :--- | :--- | :----- | :--- | +| XDG_DESKTOP_DIR | `~/Desktop` | `~/Desktop` | `%USERPROFILE%/Desktop` | +| XDG_DOWNLOAD_DIR | `~/Downloads` | `~/Downloads` | `%USERPROFILE%/Downloads` | +| XDG_DOCUMENTS_DIR | `~/Documents` | `~/Documents` | `%USERPROFILE%/Documents` | +| XDG_MUSIC_DIR | `~/Music` | `~/Music` | `%USERPROFILE%/Music` | +| XDG_PICTURES_DIR | `~/Pictures` | `~/Pictures` | `%USERPROFILE%/Pictures` | +| XDG_VIDEOS_DIR | `~/Videos` | `~/Movies` | `%USERPROFILE%/Videos` | +| XDG_TEMPLATES_DIR | `~/Templates` | `~/Templates` | `%USERPROFILE%/Templates` | +| XDG_PUBLICSHARE_DIR | `~/Public` | `~/Public` | `%PUBLIC%` | + +#### Non-standard directories + +Application directories + +``` +Unix: +- $XDG_DATA_HOME/applications +- ~/.local/share/applications +- /usr/local/share/applications +- /usr/share/applications +- $XDG_DATA_DIRS/applications + +Mac OS: +- /Applications + +Windows: +- %APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs +``` + +Font Directories + +``` +Unix: +- $XDG_DATA_HOME/fonts +- ~/.fonts +- ~/.local/share/fonts +- /usr/local/share/fonts +- /usr/share/fonts +- $XDG_DATA_DIRS/fonts + +Mac OS: +- ~/Library/Fonts +- /Library/Fonts +- /System/Library/Fonts +- /Network/Library/Fonts + +Windows: +- %windir%\Fonts +- %LOCALAPPDATA%\Microsoft\Windows\Fonts +``` + +## Usage + +#### XDG Base Directory + +```go +package main + +import ( + "log" + + "github.com/adrg/xdg" +) + +func main() { + // XDG Base Directory paths. + log.Println("Home config directory:", xdg.DataHome) + log.Println("Data directories:", xdg.DataDirs) + log.Println("Home config directory:", xdg.ConfigHome) + log.Println("Config directories:", xdg.ConfigDirs) + log.Println("Cache directory:", xdg.CacheHome) + log.Println("Runtime directory:", xdg.RuntimeDir) + + // Non-standard directories. + log.Println("Application directories:", xdg.ApplicationDirs) + log.Println("Font directories:", xdg.FontDirs) + + // Obtain a suitable location for application config files. + // ConfigFile takes one parameter which must contain the name of the file, + // but it can also contain a set of parent directories. If the directories + // don't exists, they will be created relative to the base config directory. + configFilePath, err := xdg.ConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Save the config file at:", configFilePath) + + // For other types of application files use: + // xdg.DataFile() + // xdg.CacheFile() + // xdg.RuntimeFile() + + // Finding application config files. + // SearchConfigFile takes one parameter which must contain the name of + // the file, but it can also contain a set of parent directories relative + // to the config search paths (xdg.ConfigHome and xdg.ConfigDirs). + configFilePath, err = xdg.SearchConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Config file was found at:", configFilePath) + + // For other types of application files use: + // xdg.SearchDataFile() + // xdg.SearchCacheFile() + // xdg.SearchRuntimeFile() +} +``` + +#### XDG user directories + +```go +package main + +import ( + "log" + + "github.com/adrg/xdg" +) + +func main() { + // XDG user directories. + log.Println("Desktop directory:", xdg.UserDirs.Desktop) + log.Println("Download directory:", xdg.UserDirs.Download) + log.Println("Documents directory:", xdg.UserDirs.Documents) + log.Println("Music directory:", xdg.UserDirs.Music) + log.Println("Pictures directory:", xdg.UserDirs.Pictures) + log.Println("Videos directory:", xdg.UserDirs.Videos) + log.Println("Templates directory:", xdg.UserDirs.Templates) + log.Println("Public directory:", xdg.UserDirs.PublicShare) +} +``` + +## References +For more information see the +[XDG Base Directory Specification](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html) and +[XDG user directories](https://wiki.archlinux.org/index.php/XDG_user_directories). + +## Contributing + +Contributions in the form of pull requests, issues or just general feedback, +are always welcome. +See [CONTRIBUTING.MD](https://github.com/adrg/xdg/blob/master/CONTRIBUTING.md). + +## License +Copyright (c) 2014 Adrian-George Bostan. + +This project is licensed under the [MIT license](https://opensource.org/licenses/MIT). +See [LICENSE](https://github.com/adrg/xdg/blob/master/LICENSE) for more details. diff --git a/vendor/github.com/adrg/xdg/base_dirs.go b/vendor/github.com/adrg/xdg/base_dirs.go new file mode 100644 index 0000000..0b2054c --- /dev/null +++ b/vendor/github.com/adrg/xdg/base_dirs.go @@ -0,0 +1,78 @@ +package xdg + +import "os" + +// XDG Base Directory environment variables. +var ( + envDataHome = "XDG_DATA_HOME" + envDataDirs = "XDG_DATA_DIRS" + envConfigHome = "XDG_CONFIG_HOME" + envConfigDirs = "XDG_CONFIG_DIRS" + envCacheHome = "XDG_CACHE_HOME" + envRuntimeDir = "XDG_RUNTIME_DIR" +) + +type baseDirectories struct { + dataHome string + data []string + configHome string + config []string + cacheHome string + runtime string + + // Non-standard directories. + fonts []string + applications []string +} + +func (bd baseDirectories) dataFile(relPath string) (string, error) { + return createPath(relPath, append([]string{bd.dataHome}, bd.data...)) +} + +func (bd baseDirectories) configFile(relPath string) (string, error) { + return createPath(relPath, append([]string{bd.configHome}, bd.config...)) +} + +func (bd baseDirectories) cacheFile(relPath string) (string, error) { + return createPath(relPath, []string{bd.cacheHome}) +} + +func (bd baseDirectories) runtimeFile(relPath string) (string, error) { + fi, err := os.Lstat(bd.runtime) + if err != nil { + if os.IsNotExist(err) { + return createPath(relPath, []string{bd.runtime}) + } + return "", err + } + + if fi.IsDir() { + // The runtime directory must be owned by the user. + if err = os.Chown(bd.runtime, os.Getuid(), os.Getgid()); err != nil { + return "", err + } + } else { + // For security reasons, the runtime directory cannot be a symlink. + if err = os.Remove(bd.runtime); err != nil { + return "", err + } + } + + return createPath(relPath, []string{bd.runtime}) +} + +func (bd baseDirectories) searchDataFile(relPath string) (string, error) { + return searchFile(relPath, append([]string{bd.dataHome}, bd.data...)) +} + +func (bd baseDirectories) searchConfigFile(relPath string) (string, error) { + return searchFile(relPath, append([]string{bd.configHome}, bd.config...)) +} + +func (bd baseDirectories) searchCacheFile(relPath string) (string, error) { + return searchFile(relPath, []string{bd.cacheHome}) +} + +func (bd baseDirectories) searchRuntimeFile(relPath string) (string, error) { + return searchFile(relPath, []string{bd.runtime}) +} diff --git a/vendor/github.com/adrg/xdg/go.mod b/vendor/github.com/adrg/xdg/go.mod new file mode 100644 index 0000000..87b1ff6 --- /dev/null +++ b/vendor/github.com/adrg/xdg/go.mod @@ -0,0 +1 @@ +module github.com/adrg/xdg diff --git a/vendor/github.com/adrg/xdg/paths_darwin.go b/vendor/github.com/adrg/xdg/paths_darwin.go new file mode 100644 index 0000000..d74447b --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_darwin.go @@ -0,0 +1,37 @@ +package xdg + +import ( + "path/filepath" +) + +func initBaseDirs(home string) { + // Initialize base directories. + baseDirs.dataHome = xdgPath(envDataHome, filepath.Join(home, "Library", "Application Support")) + baseDirs.data = xdgPaths(envDataDirs, "/Library/Application Support") + baseDirs.configHome = xdgPath(envConfigHome, filepath.Join(home, "Library", "Preferences")) + baseDirs.config = xdgPaths(envConfigDirs, "/Library/Preferences") + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, "Library", "Caches")) + baseDirs.runtime = xdgPath(envRuntimeDir, filepath.Join(home, "Library", "Application Support")) + + // Initialize non-standard directories. + baseDirs.applications = []string{ + "/Applications", + } + baseDirs.fonts = []string{ + filepath.Join(home, "Library/Fonts"), + "/Library/Fonts", + "/System/Library/Fonts", + "/Network/Library/Fonts", + } +} + +func initUserDirs(home string) { + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Movies")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) +} diff --git a/vendor/github.com/adrg/xdg/paths_unix.go b/vendor/github.com/adrg/xdg/paths_unix.go new file mode 100644 index 0000000..b48a2f9 --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_unix.go @@ -0,0 +1,54 @@ +// +build aix dragonfly freebsd linux nacl netbsd openbsd solaris + +package xdg + +import ( + "os" + "path/filepath" + "strconv" +) + +func initBaseDirs(home string) { + // Initialize base directories. + baseDirs.dataHome = xdgPath(envDataHome, filepath.Join(home, ".local", "share")) + baseDirs.data = xdgPaths(envDataDirs, "/usr/local/share", "/usr/share") + baseDirs.configHome = xdgPath(envConfigHome, filepath.Join(home, ".config")) + baseDirs.config = xdgPaths(envConfigDirs, "/etc/xdg") + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, ".cache")) + baseDirs.runtime = xdgPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid()))) + + // Initialize non-standard directories. + appDirs := []string{ + filepath.Join(baseDirs.dataHome, "applications"), + filepath.Join(home, ".local/share/applications"), + "/usr/local/share/applications", + "/usr/share/applications", + } + + fontDirs := []string{ + filepath.Join(baseDirs.dataHome, "fonts"), + filepath.Join(home, ".fonts"), + filepath.Join(home, ".local/share/fonts"), + "/usr/local/share/fonts", + "/usr/share/fonts", + } + + for _, dir := range baseDirs.data { + appDirs = append(appDirs, filepath.Join(dir, "applications")) + fontDirs = append(fontDirs, filepath.Join(dir, "fonts")) + } + + baseDirs.applications = uniquePaths(appDirs) + baseDirs.fonts = uniquePaths(fontDirs) +} + +func initUserDirs(home string) { + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Videos")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) +} diff --git a/vendor/github.com/adrg/xdg/paths_windows.go b/vendor/github.com/adrg/xdg/paths_windows.go new file mode 100644 index 0000000..24194f0 --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_windows.go @@ -0,0 +1,69 @@ +package xdg + +import ( + "os" + "path/filepath" +) + +func initBaseDirs(home string) { + appDataDir := os.Getenv("APPDATA") + if appDataDir == "" { + appDataDir = filepath.Join(home, "AppData") + } + roamingAppDataDir := filepath.Join(appDataDir, "Roaming") + + localAppDataDir := os.Getenv("LOCALAPPDATA") + if localAppDataDir == "" { + localAppDataDir = filepath.Join(appDataDir, "Local") + } + + programDataDir := os.Getenv("PROGRAMDATA") + if programDataDir == "" { + if systemDrive := os.Getenv("SystemDrive"); systemDrive != "" { + programDataDir = filepath.Join(systemDrive, "ProgramData") + } else { + programDataDir = home + } + } + + winDir := os.Getenv("windir") + if winDir == "" { + winDir = os.Getenv("SystemRoot") + if winDir == "" { + winDir = home + } + } + + // Initialize base directories. + baseDirs.dataHome = xdgPath(envDataHome, localAppDataDir) + baseDirs.data = xdgPaths(envDataDirs, roamingAppDataDir, programDataDir) + baseDirs.configHome = xdgPath(envConfigHome, localAppDataDir) + baseDirs.config = xdgPaths(envConfigDirs, programDataDir) + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(localAppDataDir, "cache")) + baseDirs.runtime = xdgPath(envRuntimeDir, localAppDataDir) + + // Initialize non-standard directories. + baseDirs.applications = []string{ + filepath.Join(roamingAppDataDir, "Microsoft", "Windows", "Start Menu", "Programs"), + } + baseDirs.fonts = []string{ + filepath.Join(winDir, "Fonts"), + filepath.Join(localAppDataDir, "Microsoft", "Windows", "Fonts"), + } +} + +func initUserDirs(home string) { + publicDir := os.Getenv("PUBLIC") + if publicDir == "" { + publicDir = filepath.Join(home, "Public") + } + + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Videos")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, publicDir) +} diff --git a/vendor/github.com/adrg/xdg/user_dirs.go b/vendor/github.com/adrg/xdg/user_dirs.go new file mode 100644 index 0000000..dbfd268 --- /dev/null +++ b/vendor/github.com/adrg/xdg/user_dirs.go @@ -0,0 +1,40 @@ +package xdg + +// XDG user directories environment variables. +var ( + envDesktopDir = "XDG_DESKTOP_DIR" + envDownloadDir = "XDG_DOWNLOAD_DIR" + envDocumentsDir = "XDG_DOCUMENTS_DIR" + envMusicDir = "XDG_MUSIC_DIR" + envPicturesDir = "XDG_PICTURES_DIR" + envVideosDir = "XDG_VIDEOS_DIR" + envTemplatesDir = "XDG_TEMPLATES_DIR" + envPublicShareDir = "XDG_PUBLICSHARE_DIR" +) + +// UserDirectories defines the locations of well known user directories. +type UserDirectories struct { + // Desktop defines the location of the user's desktop directory. + Desktop string + + // Download defines a suitable location for user downloaded files. + Download string + + // Documents defines a suitable location for user document files. + Documents string + + // Music defines a suitable location for user audio files. + Music string + + // Pictures defines a suitable location for user image files. + Pictures string + + // VideosDir defines a suitable location for user video files. + Videos string + + // Templates defines a suitable location for user template files. + Templates string + + // PublicShare defines a suitable location for user shared files. + PublicShare string +} diff --git a/vendor/github.com/adrg/xdg/utils.go b/vendor/github.com/adrg/xdg/utils.go new file mode 100644 index 0000000..8d5b196 --- /dev/null +++ b/vendor/github.com/adrg/xdg/utils.go @@ -0,0 +1,126 @@ +package xdg + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +func homeDir() string { + homeEnv := "HOME" + switch runtime.GOOS { + case "windows": + homeEnv = "USERPROFILE" + case "plan9": + homeEnv = "home" + } + + if home := os.Getenv(homeEnv); home != "" { + return home + } + + switch runtime.GOOS { + case "nacl": + return "/" + case "darwin": + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + return "/" + } + } + + return "" +} + +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil || os.IsExist(err) +} + +func expandPath(path, homeDir string) string { + if path == "" || homeDir == "" { + return path + } + if path[0] == '~' { + return filepath.Join(homeDir, path[1:]) + } + if strings.HasPrefix(path, "$HOME") { + return filepath.Join(homeDir, path[5:]) + } + + return path +} + +func createPath(name string, paths []string) (string, error) { + var searchedPaths []string + for _, p := range paths { + path := filepath.Join(p, name) + dir := filepath.Dir(path) + + if exists(dir) { + return path, nil + } + if err := os.MkdirAll(dir, os.ModeDir|0700); err == nil { + return path, nil + } + + searchedPaths = append(searchedPaths, dir) + } + + return "", fmt.Errorf("could not create any of the following paths: %s", + strings.Join(searchedPaths, ", ")) +} + +func searchFile(name string, paths []string) (string, error) { + var searchedPaths []string + for _, p := range paths { + path := filepath.Join(p, name) + if exists(path) { + return path, nil + } + + searchedPaths = append(searchedPaths, filepath.Dir(path)) + } + + return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s", + filepath.Base(name), strings.Join(searchedPaths, ", ")) +} + +func xdgPath(name, defaultPath string) string { + dir := expandPath(os.Getenv(name), Home) + if dir != "" && filepath.IsAbs(dir) { + return dir + } + + return defaultPath +} + +func xdgPaths(name string, defaultPaths ...string) []string { + dirs := uniquePaths(filepath.SplitList(os.Getenv(name))) + if len(dirs) != 0 { + return dirs + } + + return uniquePaths(defaultPaths) +} + +func uniquePaths(paths []string) []string { + var uniq []string + registry := map[string]struct{}{} + + for _, p := range paths { + dir := expandPath(p, Home) + if dir == "" || !filepath.IsAbs(dir) { + continue + } + if _, ok := registry[dir]; ok { + continue + } + + registry[dir] = struct{}{} + uniq = append(uniq, dir) + } + + return uniq +} diff --git a/vendor/github.com/adrg/xdg/xdg.go b/vendor/github.com/adrg/xdg/xdg.go new file mode 100644 index 0000000..826c0f8 --- /dev/null +++ b/vendor/github.com/adrg/xdg/xdg.go @@ -0,0 +1,180 @@ +/* +Package xdg provides an implementation of the XDG Base Directory +Specification. The specification defines a set of standard paths for storing +application files including data and configuration files. For portability and +flexibility reasons, applications should use the XDG defined locations instead +of hardcoding paths. The package also includes the locations of well known user +directories. The current implementation supports Windows, Mac OS and most +flavors of Unix. + + For more information regarding the XDG Base Directory Specification see: + https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + + For more information regarding the XDG user directories see: + https://wiki.archlinux.org/index.php/XDG_user_directories +*/ +package xdg + +var ( + // Home contains the path of the user's home directory. + Home string + + // DataHome defines the base directory relative to which user-specific + // data files should be stored. This directory is defined by the + // environment variable $XDG_DATA_HOME. If this variable is not set, + // a default equal to $HOME/.local/share should be used. + DataHome string + + // DataDirs defines the preference-ordered set of base directories to + // search for data files in addition to the DataHome base directory. + // This set of directories is defined by the environment variable + // $XDG_DATA_DIRS. If this variable is not set, the default directories + // to be used are /usr/local/share and /usr/share, in that order. The + // DataHome directory is considered more important than any of the + // directories defined by DataDirs. Therefore, user data files should be + // written relative to the DataHome directory, if possible. + DataDirs []string + + // ConfigHome defines the base directory relative to which user-specific + // configuration files should be written. This directory is defined by + // the environment variable $XDG_CONFIG_HOME. If this variable is not + // not set, a default equal to $HOME/.config should be used. + ConfigHome string + + // ConfigDirs defines the preference-ordered set of base directories to + // search for configuration files in addition to the ConfigHome base + // directory. This set of directories is defined by the environment + // variable $XDG_CONFIG_DIRS. If this variable is not set, a default + // equal to /etc/xdg should be used. The ConfigHome directory is + // considered more important than any of the directories defined by + // ConfigDirs. Therefore, user config files should be written + // relative to the ConfigHome directory, if possible. + ConfigDirs []string + + // CacheHome defines the base directory relative to which user-specific + // non-essential (cached) data should be written. This directory is + // defined by the environment variable $XDG_CACHE_HOME. If this variable + // is not set, a default equal to $HOME/.cache should be used. + CacheHome string + + // RuntimeDir defines the base directory relative to which user-specific + // non-essential runtime files and other file objects (such as sockets, + // named pipes, etc.) should be stored. This directory is defined by the + // environment variable $XDG_RUNTIME_DIR. If this variable is not set, + // applications should fall back to a replacement directory with similar + // capabilities. Applications should use this directory for communication + // and synchronization purposes and should not place larger files in it, + // since it might reside in runtime memory and cannot necessarily be + // swapped out to disk. + RuntimeDir string + + // UserDirs defines the locations of well known user directories. + UserDirs UserDirectories + + // FontDirs defines the common locations where font files are stored. + FontDirs []string + + // ApplicationDirs defines the common locations of applications. + ApplicationDirs []string + + // baseDirs defines the locations of base directories. + baseDirs baseDirectories +) + +// Reload refreshes base and user directories by reading the environment. +// Defaults are applied for XDG variables which are empty or not present +// in the environment. +func Reload() { + // Initialize home directory. + Home = homeDir() + + // Initialize base directories. + initBaseDirs(Home) + DataHome = baseDirs.dataHome + DataDirs = baseDirs.data + ConfigHome = baseDirs.configHome + ConfigDirs = baseDirs.config + CacheHome = baseDirs.cacheHome + RuntimeDir = baseDirs.runtime + FontDirs = baseDirs.fonts + ApplicationDirs = baseDirs.applications + + // Initialize user directories. + initUserDirs(Home) +} + +// DataFile returns a suitable location for the specified data file. +// The relPath parameter must contain the name of the data file, and +// optionally, a set of parent directories (e.g. appname/app.data). +// If the specified directories do not exist, they will be created relative +// to the base data directory. On failure, an error containing the +// attempted paths is returned. +func DataFile(relPath string) (string, error) { + return baseDirs.dataFile(relPath) +} + +// ConfigFile returns a suitable location for the specified config file. +// The relPath parameter must contain the name of the config file, and +// optionally, a set of parent directories (e.g. appname/app.yaml). +// If the specified directories do not exist, they will be created relative +// to the base config directory. On failure, an error containing the +// attempted paths is returned. +func ConfigFile(relPath string) (string, error) { + return baseDirs.configFile(relPath) +} + +// CacheFile returns a suitable location for the specified cache file. +// The relPath parameter must contain the name of the cache file, and +// optionally, a set of parent directories (e.g. appname/app.cache). +// If the specified directories do not exist, they will be created relative +// to the base cache directory. On failure, an error containing the +// attempted paths is returned. +func CacheFile(relPath string) (string, error) { + return baseDirs.cacheFile(relPath) +} + +// RuntimeFile returns a suitable location for the specified runtime file. +// The relPath parameter must contain the name of the runtime file, and +// optionally, a set of parent directories (e.g. appname/app.pid). +// If the specified directories do not exist, they will be created relative +// to the base runtime directory. On failure, an error containing the +// attempted paths is returned. +func RuntimeFile(relPath string) (string, error) { + return baseDirs.runtimeFile(relPath) +} + +// SearchDataFile searches for specified file in the data search paths. +// The relPath parameter must contain the name of the data file, and +// optionally, a set of parent directories (e.g. appname/app.data). If the +// file cannot be found, an error specifying the searched paths is returned. +func SearchDataFile(relPath string) (string, error) { + return baseDirs.searchDataFile(relPath) +} + +// SearchConfigFile searches for the specified file in config search paths. +// The relPath parameter must contain the name of the config file, and +// optionally, a set of parent directories (e.g. appname/app.yaml). If the +// file cannot be found, an error specifying the searched paths is returned. +func SearchConfigFile(relPath string) (string, error) { + return baseDirs.searchConfigFile(relPath) +} + +// SearchCacheFile searches for the specified file in the cache search path. +// The relPath parameter must contain the name of the cache file, and +// optionally, a set of parent directories (e.g. appname/app.cache). If the +// file cannot be found, an error specifying the searched path is returned. +func SearchCacheFile(relPath string) (string, error) { + return baseDirs.searchCacheFile(relPath) +} + +// SearchRuntimeFile searches for the specified file in the runtime search path. +// The relPath parameter must contain the name of the runtime file, and +// optionally, a set of parent directories (e.g. appname/app.pid). If the +// file cannot be found, an error specifying the searched path is returned. +func SearchRuntimeFile(relPath string) (string, error) { + return baseDirs.searchRuntimeFile(relPath) +} + +func init() { + Reload() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 584189c..3465970 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,6 +7,8 @@ code.gitea.io/sdk/gitea github.com/AlecAivazis/survey/v2 github.com/AlecAivazis/survey/v2/core github.com/AlecAivazis/survey/v2/terminal +# github.com/adrg/xdg v0.2.1 +github.com/adrg/xdg # github.com/alecthomas/chroma v0.7.3 github.com/alecthomas/chroma github.com/alecthomas/chroma/formatters