diff options
Diffstat (limited to 'internal/prompt')
| -rw-r--r-- | internal/prompt/cloud.go | 83 | ||||
| -rw-r--r-- | internal/prompt/git.go | 138 | ||||
| -rw-r--r-- | internal/prompt/login.go | 66 | ||||
| -rw-r--r-- | internal/prompt/main.go | 123 | ||||
| -rw-r--r-- | internal/prompt/misc.go | 43 | ||||
| -rw-r--r-- | internal/prompt/pwd.go | 157 |
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") +} |
