summaryrefslogtreecommitdiff
path: root/internal/prompt
diff options
context:
space:
mode:
Diffstat (limited to 'internal/prompt')
-rw-r--r--internal/prompt/cloud.go83
-rw-r--r--internal/prompt/git.go138
-rw-r--r--internal/prompt/login.go66
-rw-r--r--internal/prompt/main.go123
-rw-r--r--internal/prompt/misc.go43
-rw-r--r--internal/prompt/pwd.go157
6 files changed, 610 insertions, 0 deletions
diff --git a/internal/prompt/cloud.go b/internal/prompt/cloud.go
new file mode 100644
index 0000000..72bd6f9
--- /dev/null
+++ b/internal/prompt/cloud.go
@@ -0,0 +1,83 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ yaml "gopkg.in/yaml.v2"
+)
+
+func getKubernetesField() string {
+ configPaths := filepath.SplitList(os.Getenv("KUBECONFIG"))
+
+ var context string
+ for _, configPath := range configPaths {
+ context = getKubernetesContext(configPath)
+ if context != "" {
+ break
+ }
+ }
+ if context == "" {
+ return ""
+ }
+
+ namespaceBytes, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".kubectl-namespace"))
+ namespace := strings.TrimSpace(string(namespaceBytes))
+ if err != nil {
+ if !os.IsNotExist(err) {
+ handleError(err)
+ }
+ namespace = ""
+ }
+ if namespace != "" {
+ context += "/" + namespace
+ }
+
+ return withType("kube", context)
+}
+
+func getKubernetesContext(configPath string) string {
+ buf, err := ioutil.ReadFile(configPath)
+ if err != nil {
+ //non-existence is acceptable, just make the caller continue with the next configPath
+ if !os.IsNotExist(err) {
+ handleError(err)
+ }
+ return ""
+ }
+
+ var data struct {
+ CurrentContext string `yaml:"current-context"`
+ }
+ err = yaml.Unmarshal(buf, &data)
+ handleError(err)
+ return strings.TrimSpace(data.CurrentContext)
+}
+
+func getOpenstackField() string {
+ cloudName := os.Getenv("CURRENT_OS_CLOUD")
+ if cloudName == "" {
+ return ""
+ }
+ return withType("cloud", cloudName)
+}
diff --git a/internal/prompt/git.go b/internal/prompt/git.go
new file mode 100644
index 0000000..8393c3e
--- /dev/null
+++ b/internal/prompt/git.go
@@ -0,0 +1,138 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type gitRepo struct {
+ RootPath string
+ GitDir string
+}
+
+//Returns two empty strings if `path` is not inside a Git repo.
+func findRepo(path string) (*gitRepo, error) {
+ //find .git directory or file
+ gitEntry := filepath.Join(path, ".git")
+ fi, err := os.Stat(gitEntry)
+ switch {
+ case err == nil:
+ //found - continue below with further checks
+ case !os.IsNotExist(err):
+ return nil, err
+ case path == "/":
+ return nil, nil
+ default:
+ return findRepo(filepath.Dir(path))
+ }
+
+ //found .git - what is it?
+ if fi.Mode().IsDir() {
+ //normal case - .git is a directory
+ return &gitRepo{RootPath: path, GitDir: gitEntry}, nil
+ }
+
+ //.git is a file (e.g. for submodules) - it contains a line like "gitdir: path/to/gitdir"
+ bytes, err := ioutil.ReadFile(gitEntry)
+ if err != nil {
+ return nil, err
+ }
+ for _, line := range strings.Split(string(bytes), "\n") {
+ line = strings.TrimSpace(line)
+ if strings.HasPrefix(line, "gitdir:") {
+ return &gitRepo{
+ RootPath: path,
+ GitDir: filepath.Join(path, strings.TrimSpace(strings.TrimPrefix(line, "gitdir:"))),
+ }, nil
+ }
+ }
+
+ return nil, fmt.Errorf("read %s: missing gitdir directive", gitEntry)
+}
+
+func getRepoStatusField(repo *gitRepo) string {
+ if repo == nil {
+ return ""
+ }
+
+ bytes, err := ioutil.ReadFile(filepath.Join(repo.GitDir, "HEAD"))
+ if err != nil {
+ handleError(err)
+ return withType("git", withColor("1;41", "unknown"))
+ }
+ refSpec := strings.TrimSpace(string(bytes))
+
+ //is current HEAD detached?
+ if !strings.HasPrefix(refSpec, "ref: refs/") {
+ return formatRepoStatusField(withColor("1;41", "detached"), refSpec)
+ }
+
+ //current HEAD is a ref
+ refSpec = strings.TrimPrefix(refSpec, "ref: ")
+ refSpecDisplay := strings.TrimPrefix(refSpec, "refs/")
+ refSpecDisplay = strings.TrimPrefix(refSpecDisplay, "heads/")
+
+ //read file corresponding to refspec to find commit ID
+ bytes, err = ioutil.ReadFile(filepath.Join(repo.GitDir, refSpec))
+ commitID := strings.TrimSpace(string(bytes))
+ if err != nil {
+ if os.IsNotExist(err) {
+ commitID = tryReadFromPackedRefs(repo, refSpec)
+ if commitID == "" {
+ commitID = withColor("37", "blank")
+ }
+ } else {
+ handleError(err)
+ commitID = withColor("1;41", "unknown")
+ }
+ }
+
+ return formatRepoStatusField(refSpecDisplay, commitID)
+}
+
+func tryReadFromPackedRefs(repo *gitRepo, refSpec string) string {
+ bytes, err := ioutil.ReadFile(filepath.Join(repo.GitDir, "packed-refs"))
+ if err != nil {
+ return ""
+ }
+ for _, line := range strings.Split(string(bytes), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ fields := strings.Fields(line)
+ if len(fields) == 2 && fields[1] == refSpec {
+ return fields[0]
+ }
+ }
+ return ""
+}
+
+func formatRepoStatusField(refSpec, commitID string) string {
+ //shorten plain commit IDs from 40 to 10 bytes
+ if len(commitID) == 40 && !strings.Contains(commitID, "\x1B") {
+ commitID = commitID[0:10]
+ }
+ return withType("git", refSpec+"/"+commitID)
+}
diff --git a/internal/prompt/login.go b/internal/prompt/login.go
new file mode 100644
index 0000000..92cc270
--- /dev/null
+++ b/internal/prompt/login.go
@@ -0,0 +1,66 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import "os"
+
+//#include <sys/types.h>
+//#include <pwd.h>
+//#include <unistd.h>
+import "C"
+
+func getLoginField() string {
+ var result string
+
+ //show user name
+ userName := getUserName()
+ commonUser := getenvOrDefault("PRETTYPROMPT_COMMONUSER", "stefan")
+ if commonUser != userName {
+ color := "0"
+ if userName == "root" {
+ color = "1;41"
+ }
+ result = withColor(color, userName) + "@"
+ }
+
+ //show hostname
+ hostname, err := os.Hostname()
+ if err != nil {
+ handleError(err)
+ hostname = "<unknown>"
+ }
+ return result + withColor(
+ getenvOrDefault("PRETTYPROMPT_HOSTCOLOR", "0;33"),
+ hostname,
+ )
+}
+
+func getUserName() string {
+ //try to find username via getpwuid(getuid())
+ pw, err := C.getpwuid(C.getuid())
+ if err == nil {
+ return C.GoString(pw.pw_name)
+ }
+ //fallback to $USER, if set
+ if name := os.Getenv("USER"); name != "" {
+ return name
+ }
+ handleError(err)
+ return "<unknown>"
+}
diff --git a/internal/prompt/main.go b/internal/prompt/main.go
new file mode 100644
index 0000000..dcef513
--- /dev/null
+++ b/internal/prompt/main.go
@@ -0,0 +1,123 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/majewsky/gofu/internal/cli"
+)
+
+//Exec executes the prettyprompt applet and returns an exit code (0 for
+//success, >0 for error).
+func Exec(args []string) int {
+ fields := []string{
+ getLoginField(),
+ }
+ cwd := CurrentDirectory()
+ fields = appendUnlessEmpty(fields, getDirectoryField(cwd))
+ fields = appendUnlessEmpty(fields, getDeletedMessageField(cwd))
+ fields = appendUnlessEmpty(fields, getRepoStatusField(cwd.Repo))
+ fields = appendUnlessEmpty(fields, getTerminalField())
+ fields = appendUnlessEmpty(fields, getOpenstackField())
+ fields = appendUnlessEmpty(fields, getKubernetesField())
+ //this field should always be last
+ if len(args) > 0 {
+ fields = appendUnlessEmpty(fields, getExitCodeField(args[0]))
+ }
+
+ line := strings.Join(fields, " ")
+ lineWidth := getPrintableLength(line)
+
+ //add dashes to expand `line` to fill the terminal's width
+ termWidth, _, err := terminal.GetSize(0)
+ if err != nil {
+ termWidth = 80
+ }
+ if termWidth > lineWidth {
+ line += " "
+ lineWidth++
+ }
+ if termWidth > lineWidth {
+ dashes := make([]byte, termWidth-lineWidth)
+ for idx := range dashes {
+ dashes[idx] = '-'
+ }
+ line += withColor("1", string(dashes))
+ }
+
+ os.Stdout.Write([]byte(line + "\n"))
+
+ //print second line: a letter identifying the shell, and the final "$ ")
+ shellIdent := ""
+ switch os.Getenv("PRETTYPROMPT_SHELL") {
+ case "zsh":
+ shellIdent = "Z"
+ case "bash":
+ shellIdent = "B"
+ }
+ os.Stdout.Write([]byte(shellIdent + "$ "))
+
+ return 0
+}
+
+func getenvOrDefault(key, defaultValue string) (value string) {
+ value = os.Getenv(key)
+ if value == "" {
+ value = defaultValue
+ }
+ return
+}
+
+func appendUnlessEmpty(list []string, val string) []string {
+ if val == "" {
+ return list
+ }
+ return append(list, val)
+}
+
+func handleError(err error) {
+ if err != nil {
+ os.Stderr.Write([]byte("\x1B[1;31mPrompt error: " + err.Error() + "\x1B[0m\n"))
+ }
+}
+
+func getPrintableLength(text string) int {
+ return len(cli.AnsiColorCodeRx.ReplaceAllString(text, ""))
+}
+
+//withColor adds ANSI escape sequences to the string to display it with a
+//certain color. The color is given as the semicolon-separated list of
+//arguments to the ANSI escape sequence SGR, e.g. "1;41" for bold with red
+//background.
+func withColor(color, text string) string {
+ if color == "0" {
+ return text
+ }
+ return fmt.Sprintf("\x1B[%sm%s\x1B[0m", color, text)
+}
+
+//withType adds a type annotation with a standardized format to the text.
+func withType(typeStr, text string) string {
+ return fmt.Sprintf("\x1B[37m%s:\x1B[0m%s", typeStr, text)
+}
diff --git a/internal/prompt/misc.go b/internal/prompt/misc.go
new file mode 100644
index 0000000..124015a
--- /dev/null
+++ b/internal/prompt/misc.go
@@ -0,0 +1,43 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import (
+ "os"
+ "strconv"
+)
+
+func getTerminalField() string {
+ termName := os.Getenv("TERM")
+ if termName == "xterm-256color" {
+ return ""
+ }
+ if termName == "" {
+ termName = withColor("1;41", "not set")
+ }
+ return withType("term", termName)
+}
+
+func getExitCodeField(arg string) string {
+ exitCode, err := strconv.Atoi(arg)
+ if err == nil && exitCode > 0 {
+ return withColor("1;31", "exit:"+arg)
+ }
+ return ""
+}
diff --git a/internal/prompt/pwd.go b/internal/prompt/pwd.go
new file mode 100644
index 0000000..1b93987
--- /dev/null
+++ b/internal/prompt/pwd.go
@@ -0,0 +1,157 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package prompt
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+//Directory contains all data about a directory that the prompt needs.
+type Directory struct {
+ Path string
+ DisplayPath string
+ InBuildTree bool
+ InRepoTree bool
+ Repo *gitRepo
+ NearestAccessiblePath string
+}
+
+//CurrentDirectory prepares a Directory struct for the current working
+//directory.
+func CurrentDirectory() Directory {
+ cwd, err := os.Getwd()
+ if err != nil {
+ cwd = filepath.Clean(os.Getenv("PWD"))
+ }
+ return NewDirectory(cwd)
+}
+
+//NewDirectory prepares a Directory struct for the given path.
+func NewDirectory(path string) (dir Directory) {
+ dir.Path = path
+ dir.DisplayPath = path
+
+ //check if path actually exists
+ dir.NearestAccessiblePath = findNearestAccessiblePath(dir.Path)
+ if dir.NearestAccessiblePath == dir.Path {
+ //marks that everything is okay existence-wise
+ dir.NearestAccessiblePath = ""
+
+ //display tag if below /x/build
+ if buildPath := os.Getenv("BUILD_ROOT"); buildPath != "" {
+ rel, _ := filepath.Rel(buildPath, dir.DisplayPath)
+ if !strings.HasPrefix(rel, "..") && rel != "." {
+ dir.InBuildTree = true
+ dir.DisplayPath = filepath.Join("/", rel)
+ }
+ }
+
+ //display tag if below /x/src
+ if gopath := os.Getenv("GOPATH"); gopath != "" {
+ repoPath := filepath.Join(gopath, "src")
+ rel, _ := filepath.Rel(repoPath, dir.DisplayPath)
+ if !strings.HasPrefix(rel, "..") && rel != "." {
+ dir.InRepoTree = true
+ dir.DisplayPath = rel
+ }
+ }
+
+ //strip $HOME prefix if applicable and desirable
+ dir.stripHomeDirFromDisplay()
+
+ //check if we are inside a Git repository
+ var err error
+ dir.Repo, err = findRepo(dir.Path)
+ handleError(err)
+ }
+
+ return
+}
+
+//This part can benefit from a "return" in the middle, so it's in a separate function.
+func (dir *Directory) stripHomeDirFromDisplay() {
+ if !strings.HasPrefix(dir.DisplayPath, "/") {
+ return
+ }
+
+ homePath := os.Getenv("HOME")
+ if homePath == "" {
+ return
+ }
+
+ rel, _ := filepath.Rel(homePath, dir.DisplayPath)
+ if rel == "." {
+ //do not display an empty DisplayPath if tags are displayed
+ if dir.InBuildTree || dir.InRepoTree {
+ return
+ }
+ rel = ""
+ }
+ if !strings.HasPrefix(rel, "..") {
+ dir.DisplayPath = rel
+ }
+}
+
+func findNearestAccessiblePath(path string) string {
+ _, err := os.Stat(path)
+ if err == nil {
+ return path
+ }
+ return findNearestAccessiblePath(filepath.Dir(path))
+}
+
+func getDirectoryField(dir Directory) string {
+ if dir.DisplayPath == "" {
+ return ""
+ }
+
+ txt := withColor("1;36", dir.DisplayPath)
+ if dir.NearestAccessiblePath == "" {
+ //cwd accessible -> highlight path elements inside the repo (if any)
+ if dir.Repo != nil && dir.Repo.RootPath != dir.Path {
+ rel, _ := filepath.Rel(dir.Repo.RootPath, dir.Path)
+ if strings.HasSuffix(dir.DisplayPath, rel) {
+ base := strings.TrimSuffix(dir.DisplayPath, rel)
+ txt = withColor("0;36", base) + withColor("1;36", rel)
+ }
+ }
+ } else {
+ //cwd inaccessible -> highlight inaccessible path elements
+ rel, _ := filepath.Rel(dir.NearestAccessiblePath, dir.Path)
+ txt = withColor("1;36", dir.NearestAccessiblePath+"/") + withColor("1;31", rel)
+ }
+
+ //apply tags
+ if dir.InRepoTree {
+ txt = withType("repo", txt)
+ }
+ if dir.InBuildTree {
+ txt = withType("build", txt)
+ }
+ return txt
+}
+
+func getDeletedMessageField(dir Directory) string {
+ if dir.NearestAccessiblePath == "" {
+ return ""
+ }
+ return withColor("1;41", "cannot stat cwd")
+}