├── preview.png ├── exitcode ├── exitcode_windows.go ├── exitcode_darwin.go ├── exitcode_linux.go ├── exitcode_netbsd.go ├── exitcode_dragonfly.go ├── exitcode_openbsd.go ├── exitcode_freebsd.go ├── exitcode_zos.go └── exitcode_solaris.go ├── user-is-admin.go ├── .editorconfig ├── segment-newline.go ├── environment.go ├── go.mod ├── segment-time.go ├── .gitignore ├── segment-jobs.go ├── segment-plenv.go ├── segment-shenv.go ├── segment-vgo.go ├── segment-perlbrew.go ├── segment-readonly.go ├── segment-nix-shell.go ├── segment-plugin.go ├── segment-root.go ├── segment-ssh.go ├── segment-readonly_windows.go ├── segment-direnv.go ├── segment-dotenv.go ├── user-is-admin_windows.go ├── segment-username.go ├── .github ├── workflows │ ├── release.yml │ └── build.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── powerline_test.go ├── segment-terraform_workspace.go ├── segment-wsl.go ├── segment-vimode.go ├── segment-docker.go ├── segment-load.go ├── segment-shellvar.go ├── .goreleaser.yaml ├── segment-aws.go ├── segment-termtitle.go ├── segment-gitlite.go ├── generatePreview.sh ├── segment-docker_context.go ├── segment-exitcode.go ├── powerline └── powerline.go ├── segment-node.go ├── segment-virtualenv.go ├── segment-hg.go ├── segment-bzr.go ├── segment-hostname.go ├── segment-fossil.go ├── segment-rvm.go ├── segment-rbenv.go ├── segment-goenv.go ├── segment-duration.go ├── segment-gcp.go ├── themes.go ├── segment-kube.go ├── config.go ├── segment-subversion.go ├── go.sum ├── themes └── default.json ├── segment-cwd.go ├── main.go ├── segment-git.go ├── args.go ├── powerline.go ├── README.md └── LICENSE.md /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjanne/powerline-go/HEAD/preview.png -------------------------------------------------------------------------------- /exitcode/exitcode_windows.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | // We don’t support named errors on Windows 5 | } 6 | -------------------------------------------------------------------------------- /user-is-admin.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func userIsAdmin() bool { 10 | return os.Getuid() == 0 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See http://editorconfig.org 2 | 3 | # In Go files we indent with tabs but still 4 | # set indent_size to control the GitHub web viewer. 5 | [*.go] 6 | indent_size = 4 7 | 8 | -------------------------------------------------------------------------------- /segment-newline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import pwl "github.com/justjanne/powerline-go/powerline" 4 | 5 | func segmentNewline(p *powerline) []pwl.Segment { 6 | return []pwl.Segment{{NewLine: true}} 7 | } 8 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "runtime" 4 | 5 | func homeEnvName() string { 6 | env := "HOME" 7 | if runtime.GOOS == "windows" { 8 | env = "USERPROFILE" 9 | } 10 | return env 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/justjanne/powerline-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/mattn/go-runewidth v0.0.9 7 | github.com/shirou/gopsutil/v3 v3.22.3 8 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f 9 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 10 | golang.org/x/text v0.3.8 11 | gopkg.in/ini.v1 v1.66.4 12 | gopkg.in/yaml.v2 v2.4.0 13 | ) 14 | -------------------------------------------------------------------------------- /segment-time.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "time" 7 | ) 8 | 9 | func segmentTime(p *powerline) []pwl.Segment { 10 | return []pwl.Segment{{ 11 | Name: "time", 12 | Content: time.Now().Format(strings.TrimSpace(p.cfg.Time)), 13 | Foreground: p.theme.TimeFg, 14 | Background: p.theme.TimeBg, 15 | }} 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | powerline-go 7 | /dist/ 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | /.idea/ 19 | 20 | .vscode/ 21 | -------------------------------------------------------------------------------- /segment-jobs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentJobs(p *powerline) []pwl.Segment { 10 | if p.cfg.Jobs <= 0 { 11 | return []pwl.Segment{} 12 | } 13 | return []pwl.Segment{{ 14 | Name: "jobs", 15 | Content: strconv.Itoa(p.cfg.Jobs), 16 | Foreground: p.theme.JobsFg, 17 | Background: p.theme.JobsBg, 18 | }} 19 | } 20 | -------------------------------------------------------------------------------- /segment-plenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentPlEnv(p *powerline) []pwl.Segment { 10 | env, _ := os.LookupEnv("PLENV_VERSION") 11 | if env == "" { 12 | return []pwl.Segment{} 13 | } 14 | return []pwl.Segment{{ 15 | Name: "plenv", 16 | Content: env, 17 | Foreground: p.theme.PlEnvFg, 18 | Background: p.theme.PlEnvBg, 19 | }} 20 | } 21 | -------------------------------------------------------------------------------- /segment-shenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentShEnv(p *powerline) []pwl.Segment { 10 | env, _ := os.LookupEnv("SHENV_VERSION") 11 | if env == "" { 12 | return []pwl.Segment{} 13 | } 14 | return []pwl.Segment{{ 15 | Name: "shenv", 16 | Content: env, 17 | Foreground: p.theme.ShEnvFg, 18 | Background: p.theme.ShEnvBg, 19 | }} 20 | } 21 | -------------------------------------------------------------------------------- /segment-vgo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentVirtualGo(p *powerline) []pwl.Segment { 10 | env, _ := os.LookupEnv("VIRTUALGO") 11 | if env == "" { 12 | return []pwl.Segment{} 13 | } 14 | 15 | return []pwl.Segment{{ 16 | Name: "vgo", 17 | Content: env, 18 | Foreground: p.theme.VirtualGoFg, 19 | Background: p.theme.VirtualGoBg, 20 | }} 21 | } 22 | -------------------------------------------------------------------------------- /segment-perlbrew.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pwl "github.com/justjanne/powerline-go/powerline" 5 | "os" 6 | "path" 7 | ) 8 | 9 | func segmentPerlbrew(p *powerline) []pwl.Segment { 10 | env, _ := os.LookupEnv("PERLBREW_PERL") 11 | if env == "" { 12 | return []pwl.Segment{} 13 | } 14 | 15 | envName := path.Base(env) 16 | return []pwl.Segment{{ 17 | Name: "perlbrew", 18 | Content: envName, 19 | Foreground: p.theme.PerlbrewFg, 20 | Background: p.theme.PerlbrewBg, 21 | }} 22 | } 23 | -------------------------------------------------------------------------------- /segment-readonly.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func segmentPerms(p *powerline) []pwl.Segment { 11 | cwd := p.cwd 12 | if unix.Access(cwd, unix.W_OK) == nil { 13 | return []pwl.Segment{} 14 | } 15 | return []pwl.Segment{{ 16 | Name: "perms", 17 | Content: p.symbols.Lock, 18 | Foreground: p.theme.ReadonlyFg, 19 | Background: p.theme.ReadonlyBg, 20 | }} 21 | } 22 | -------------------------------------------------------------------------------- /segment-nix-shell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pwl "github.com/justjanne/powerline-go/powerline" 5 | "os" 6 | ) 7 | 8 | func segmentNixShell(p *powerline) []pwl.Segment { 9 | var nixShell string 10 | nixShell, _ = os.LookupEnv("IN_NIX_SHELL") 11 | if nixShell == "" { 12 | return []pwl.Segment{} 13 | } 14 | return []pwl.Segment{{ 15 | Name: "nix-shell", 16 | Content: p.symbols.NixShellIndicator, 17 | Foreground: p.theme.NixShellFg, 18 | Background: p.theme.NixShellBg, 19 | }} 20 | } 21 | -------------------------------------------------------------------------------- /segment-plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os/exec" 6 | 7 | pwl "github.com/justjanne/powerline-go/powerline" 8 | ) 9 | 10 | func segmentPlugin(p *powerline, plugin string) ([]pwl.Segment, bool) { 11 | output, err := exec.Command("powerline-go-" + plugin).Output() 12 | if err != nil { 13 | return nil, false 14 | } 15 | segments := []pwl.Segment{} 16 | err = json.Unmarshal(output, &segments) 17 | if err != nil { 18 | // The plugin was found but no valid data was returned. Ignore it 19 | return []pwl.Segment{}, true 20 | } 21 | return segments, true 22 | } 23 | -------------------------------------------------------------------------------- /segment-root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import pwl "github.com/justjanne/powerline-go/powerline" 4 | 5 | func segmentRoot(p *powerline) []pwl.Segment { 6 | var foreground, background uint8 7 | if p.cfg.PrevError == 0 || p.cfg.StaticPromptIndicator { 8 | foreground = p.theme.CmdPassedFg 9 | background = p.theme.CmdPassedBg 10 | } else { 11 | foreground = p.theme.CmdFailedFg 12 | background = p.theme.CmdFailedBg 13 | } 14 | 15 | return []pwl.Segment{{ 16 | Name: "root", 17 | Content: p.shell.RootIndicator, 18 | Foreground: foreground, 19 | Background: background, 20 | }} 21 | } 22 | -------------------------------------------------------------------------------- /segment-ssh.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentSSH(p *powerline) []pwl.Segment { 10 | sshClient, _ := os.LookupEnv("SSH_CLIENT") 11 | if sshClient == "" { 12 | return []pwl.Segment{} 13 | } 14 | var networkIcon string 15 | if p.cfg.SshAlternateIcon { 16 | networkIcon = p.symbols.NetworkAlternate 17 | } else { 18 | networkIcon = p.symbols.Network 19 | } 20 | 21 | return []pwl.Segment{{ 22 | Name: "ssh", 23 | Content: networkIcon, 24 | Foreground: p.theme.SSHFg, 25 | Background: p.theme.SSHBg, 26 | }} 27 | } 28 | -------------------------------------------------------------------------------- /segment-readonly_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | func segmentPerms(p *powerline) []pwl.Segment { 12 | cwd := p.cwd 13 | const W_USR = 0002 14 | // Check user's permissions on directory in a portable but probably slower way 15 | fileInfo, _ := os.Stat(cwd) 16 | if fileInfo.Mode()&W_USR == W_USR { 17 | return []pwl.Segment{} 18 | } 19 | return []pwl.Segment{{ 20 | Name: "perms", 21 | Content: p.symbols.Lock, 22 | Foreground: p.theme.ReadonlyFg, 23 | Background: p.theme.ReadonlyBg, 24 | }} 25 | } 26 | -------------------------------------------------------------------------------- /segment-direnv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | func segmentDirenv(p *powerline) []pwl.Segment { 12 | content := os.Getenv("DIRENV_DIR") 13 | if content == "" { 14 | return []pwl.Segment{} 15 | } 16 | if strings.TrimPrefix(content, "-") == p.userInfo.HomeDir { 17 | content = "~" 18 | } else { 19 | content = filepath.Base(content) 20 | } 21 | 22 | return []pwl.Segment{{ 23 | Name: "direnv", 24 | Content: content, 25 | Foreground: p.theme.DotEnvFg, 26 | Background: p.theme.DotEnvBg, 27 | }} 28 | } 29 | -------------------------------------------------------------------------------- /segment-dotenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentDotEnv(p *powerline) []pwl.Segment { 10 | files := []string{".env", ".envrc"} 11 | dotEnv := false 12 | for _, file := range files { 13 | stat, err := os.Stat(file) 14 | if err == nil && !stat.IsDir() { 15 | dotEnv = true 16 | break 17 | } 18 | } 19 | if !dotEnv { 20 | return []pwl.Segment{} 21 | } 22 | return []pwl.Segment{{ 23 | Name: "dotenv", 24 | Content: p.symbols.DotEnvIndicator, 25 | Foreground: p.theme.DotEnvFg, 26 | Background: p.theme.DotEnvBg, 27 | }} 28 | } 29 | -------------------------------------------------------------------------------- /user-is-admin_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | ) 8 | 9 | func userIsAdmin() bool { 10 | var sid *windows.SID 11 | 12 | err := windows.AllocateAndInitializeSid( 13 | &windows.SECURITY_NT_AUTHORITY, 14 | 2, 15 | windows.SECURITY_BUILTIN_DOMAIN_RID, 16 | windows.DOMAIN_ALIAS_RID_ADMINS, 17 | 0, 0, 0, 0, 0, 0, 18 | &sid, 19 | ) 20 | if err != nil { 21 | return false 22 | } 23 | defer windows.FreeSid(sid) 24 | 25 | t := windows.Token(0) 26 | 27 | member, err := t.IsMember(sid) 28 | if err != nil { 29 | return false 30 | } 31 | 32 | return member 33 | } 34 | -------------------------------------------------------------------------------- /segment-username.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pwl "github.com/justjanne/powerline-go/powerline" 5 | ) 6 | 7 | func segmentUser(p *powerline) []pwl.Segment { 8 | var userPrompt string 9 | switch p.cfg.Shell { 10 | case "bash": 11 | userPrompt = "\\u" 12 | case "zsh": 13 | userPrompt = "%n" 14 | default: 15 | userPrompt = p.username 16 | } 17 | 18 | var background uint8 19 | if p.userIsAdmin { 20 | background = p.theme.UsernameRootBg 21 | } else { 22 | background = p.theme.UsernameBg 23 | } 24 | 25 | return []pwl.Segment{{ 26 | Name: "user", 27 | Content: userPrompt, 28 | Foreground: p.theme.UsernameFg, 29 | Background: background, 30 | }} 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | permissions: 7 | contents: write 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | name: Release binaries 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - run: git fetch --force --tags 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version: "1.20" 20 | - uses: goreleaser/goreleaser-action@v4 21 | with: 22 | distribution: goreleaser 23 | version: latest 24 | args: release --clean 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: {} 4 | merge_group: 5 | types: [checks_requested] 6 | push: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Build binaries 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - run: git fetch --force --tags 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.20" 19 | - uses: goreleaser/goreleaser-action@v4 20 | with: 21 | distribution: goreleaser 22 | version: latest 23 | args: build --clean --snapshot 24 | - run: dist/powerline-go_linux_amd64_v1/powerline-go --help 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /powerline_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_detectShell(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | want string 9 | }{ 10 | { 11 | name: "", 12 | want: "bare", 13 | }, 14 | { 15 | name: "/bin/sh", 16 | want: "bare", 17 | }, { 18 | name: "/bin/bash", 19 | want: "bash", 20 | }, { 21 | name: "/usr/local/bin/bash5", 22 | want: "bash", 23 | }, { 24 | name: "/usr/bin/zsh", 25 | want: "zsh", 26 | }} 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := detectShell(tt.name); got != tt.want { 30 | t.Errorf("detectShell(%q) = %q, want %q", tt.name, got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /segment-terraform_workspace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | pwl "github.com/justjanne/powerline-go/powerline" 8 | ) 9 | 10 | const wsFile = "./.terraform/environment" 11 | 12 | func segmentTerraformWorkspace(p *powerline) []pwl.Segment { 13 | stat, err := os.Stat(wsFile) 14 | if err != nil { 15 | return []pwl.Segment{} 16 | } 17 | if stat.IsDir() { 18 | return []pwl.Segment{} 19 | } 20 | workspace, err := ioutil.ReadFile(wsFile) 21 | if err != nil { 22 | return []pwl.Segment{} 23 | } 24 | return []pwl.Segment{{ 25 | Name: "terraform-workspace", 26 | Content: string(workspace), 27 | Foreground: p.theme.TFWsFg, 28 | Background: p.theme.TFWsBg, 29 | }} 30 | } 31 | -------------------------------------------------------------------------------- /segment-wsl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | pwl "github.com/justjanne/powerline-go/powerline" 8 | ) 9 | 10 | func segmentWSL(p *powerline) []pwl.Segment { 11 | var WSL string 12 | WSLMachineName, _ := os.LookupEnv("WSL_DISTRO_NAME") 13 | WSLHost, _ := os.LookupEnv("NAME") 14 | 15 | if WSLMachineName != "" { 16 | WSL = WSLMachineName 17 | } else if WSLHost != " " { 18 | u, err := url.Parse(WSLHost) 19 | if err == nil { 20 | WSL = u.Host 21 | } 22 | } 23 | 24 | if WSL != "" { 25 | return []pwl.Segment{{ 26 | Name: "WSL", 27 | Content: WSL, 28 | Foreground: p.theme.WSLMachineFg, 29 | Background: p.theme.WSLMachineBg, 30 | }} 31 | } 32 | return []pwl.Segment{} 33 | } 34 | -------------------------------------------------------------------------------- /segment-vimode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pwl "github.com/justjanne/powerline-go/powerline" 5 | ) 6 | 7 | func segmentViMode(p *powerline) []pwl.Segment { 8 | mode := p.cfg.ViMode 9 | if mode == "" { 10 | warn("'--vi-mode' is not set.") 11 | return []pwl.Segment{} 12 | } 13 | 14 | switch mode { 15 | case "vicmd": 16 | return []pwl.Segment{{ 17 | Name: "vi-mode", 18 | Content: "C", 19 | Foreground: p.theme.ViModeCommandFg, 20 | Background: p.theme.ViModeCommandBg, 21 | }} 22 | default: // usually "viins" or "main" 23 | return []pwl.Segment{{ 24 | Name: "vi-mode", 25 | Content: "I", 26 | Foreground: p.theme.ViModeInsertFg, 27 | Background: p.theme.ViModeInsertBg, 28 | }} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /exitcode/exitcode_darwin.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGURG", 20 | 0x11: "SIGSTOP", 21 | 0x12: "SIGTSTP", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1a: "SIGVTALRM", 30 | 0x1b: "SIGPROF", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGINFO", 33 | 0x1e: "SIGUSR1", 34 | 0x1f: "SIGUSR2", 35 | } 36 | -------------------------------------------------------------------------------- /exitcode/exitcode_linux.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGBUS", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGUSR1", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGUSR2", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGSTKFLT", 20 | 0x11: "SIGCHLD", 21 | 0x12: "SIGCONT", 22 | 0x13: "SIGSTOP", 23 | 0x14: "SIGTSTP", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGURG", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1a: "SIGVTALRM", 30 | 0x1b: "SIGPROF", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGIO", 33 | 0x1e: "SIGPWR", 34 | 0x1f: "SIGSYS", 35 | } 36 | -------------------------------------------------------------------------------- /exitcode/exitcode_netbsd.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGURG", 20 | 0x11: "SIGSTOP", 21 | 0x12: "SIGTSTP", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1a: "SIGVTALRM", 30 | 0x1b: "SIGPROF", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGINFO", 33 | 0x1e: "SIGUSR1", 34 | 0x1f: "SIGUSR2", 35 | 0x20: "SIGPWR", 36 | } 37 | -------------------------------------------------------------------------------- /segment-docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | pwl "github.com/justjanne/powerline-go/powerline" 8 | ) 9 | 10 | func segmentDocker(p *powerline) []pwl.Segment { 11 | var docker string 12 | dockerMachineName, _ := os.LookupEnv("DOCKER_MACHINE_NAME") 13 | dockerHost, _ := os.LookupEnv("DOCKER_HOST") 14 | 15 | if dockerMachineName != "" { 16 | docker = dockerMachineName 17 | } else if dockerHost != " " { 18 | u, err := url.Parse(dockerHost) 19 | if err == nil { 20 | docker = u.Host 21 | } 22 | } 23 | 24 | if docker == "" { 25 | return []pwl.Segment{} 26 | } 27 | return []pwl.Segment{{ 28 | Name: "docker", 29 | Content: docker, 30 | Foreground: p.theme.DockerMachineFg, 31 | Background: p.theme.DockerMachineBg, 32 | }} 33 | } 34 | -------------------------------------------------------------------------------- /segment-load.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "runtime" 7 | 8 | "github.com/shirou/gopsutil/v3/load" 9 | ) 10 | 11 | func segmentLoad(p *powerline) []pwl.Segment { 12 | c := runtime.NumCPU() 13 | a, err := load.Avg() 14 | if err != nil { 15 | return []pwl.Segment{} 16 | } 17 | bg := p.theme.LoadBg 18 | 19 | load := a.Load5 20 | switch p.theme.LoadAvgValue { 21 | case 1: 22 | load = a.Load1 23 | case 15: 24 | load = a.Load15 25 | } 26 | 27 | if load > float64(c)*p.theme.LoadThresholdBad { 28 | bg = p.theme.LoadHighBg 29 | } 30 | 31 | return []pwl.Segment{{ 32 | Name: "load", 33 | Content: fmt.Sprintf("%.2f", a.Load5), 34 | Foreground: p.theme.LoadFg, 35 | Background: bg, 36 | }} 37 | } 38 | -------------------------------------------------------------------------------- /exitcode/exitcode_dragonfly.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0A: "SIGBUS", 14 | 0x0B: "SIGSEGV", 15 | 0x0C: "SIGSYS", 16 | 0x0D: "SIGPIPE", 17 | 0x0E: "SIGALRM", 18 | 0x0F: "SIGTERM", 19 | 0x10: "SIGURG", 20 | 0x11: "SIGSTOP", 21 | 0x12: "SIGTSTP", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1A: "SIGVTALRM", 30 | 0x1B: "SIGPROF", 31 | 0x1C: "SIGWINCH", 32 | 0x1D: "SIGINFO", 33 | 0x20: "SIGTHR", 34 | 0x21: "SIGCKPT", 35 | 0x22: "SIGCKPTEXIT", 36 | } 37 | -------------------------------------------------------------------------------- /exitcode/exitcode_openbsd.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGURG", 20 | 0x11: "SIGSTOP", 21 | 0x12: "SIGTSTP", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1a: "SIGVTALRM", 30 | 0x1b: "SIGPROF", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGINFO", 33 | 0x1e: "SIGUSR1", 34 | 0x1f: "SIGUSR2", 35 | 0x20: "SIGTHR", 36 | } 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Environment (please complete the following information):** 24 | - OS: [e.g. linux] 25 | - Architecture: [e.g. intel, arm, if you don’t know this leave it empty] 26 | - Shell: [e.g. zsh, bash, fish, powershell] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /segment-shellvar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentShellVar(p *powerline) []pwl.Segment { 10 | shellVarName := p.cfg.ShellVar 11 | varContent, varExists := os.LookupEnv(shellVarName) 12 | 13 | if !varExists { 14 | if shellVarName != "" { 15 | warn("Shell variable " + shellVarName + " does not exist.") 16 | } 17 | return []pwl.Segment{} 18 | } 19 | 20 | if varContent == "" { 21 | if !p.cfg.ShellVarNoWarnEmpty { 22 | warn("Shell variable " + shellVarName + " is empty.") 23 | } 24 | return []pwl.Segment{} 25 | } 26 | 27 | return []pwl.Segment{{ 28 | Name: "shell-var", 29 | Content: varContent, 30 | Foreground: p.theme.ShellVarFg, 31 | Background: p.theme.ShellVarBg, 32 | }} 33 | } 34 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: powerline-go 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - binary: powerline-go 9 | env: 10 | - CGO_ENABLED=0 11 | goarch: 12 | - amd64 13 | - arm 14 | - arm64 15 | - "386" 16 | goos: 17 | - darwin 18 | - linux 19 | - windows 20 | - freebsd 21 | - netbsd 22 | - openbsd 23 | ignore: 24 | - goos: darwin 25 | goarch: "386" 26 | - goos: openbsd 27 | goarch: arm 28 | flags: 29 | - -trimpath 30 | ldflags: 31 | - -s 32 | - -w 33 | 34 | archives: 35 | - name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" 36 | format: binary 37 | 38 | checksum: 39 | name_template: checksums.txt 40 | 41 | changelog: 42 | use: github 43 | -------------------------------------------------------------------------------- /exitcode/exitcode_freebsd.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGURG", 20 | 0x11: "SIGSTOP", 21 | 0x12: "SIGTSTP", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGXCPU", 28 | 0x19: "SIGXFSZ", 29 | 0x1a: "SIGVTALRM", 30 | 0x1b: "SIGPROF", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGINFO", 33 | 0x1e: "SIGUSR1", 34 | 0x1f: "SIGUSR2", 35 | 0x20: "SIGTHR", 36 | 0x21: "SIGLIBRT", 37 | } 38 | -------------------------------------------------------------------------------- /exitcode/exitcode_zos.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGABRT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGPOLL", 9 | 0x06: "SIGURG", 10 | 0x07: "SIGSTOP", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGUSR1", 20 | 0x11: "SIGUSR2", 21 | 0x12: "SIGABND", 22 | 0x13: "SIGCONT", 23 | 0x14: "SIGCHLD", 24 | 0x15: "SIGTTIN", 25 | 0x16: "SIGTTOU", 26 | 0x17: "SIGIO", 27 | 0x18: "SIGQUIT", 28 | 0x19: "SIGTSTP", 29 | 0x1a: "SIGTRAP", 30 | 0x1b: "SIGIOERR", 31 | 0x1c: "SIGWINCH", 32 | 0x1d: "SIGXCPU", 33 | 0x1e: "SIGXFSZ", 34 | 0x1f: "SIGVTALRM", 35 | 0x20: "SIGPROF", 36 | 0x26: "SIGDCE", 37 | 0x27: "SIGDUMP", 38 | } 39 | -------------------------------------------------------------------------------- /segment-aws.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | ) 8 | 9 | func segmentAWS(p *powerline) []pwl.Segment { 10 | profile := os.Getenv("AWS_PROFILE") 11 | region := os.Getenv("AWS_DEFAULT_REGION") 12 | var content string = "" 13 | 14 | if len(profile) == 0 { 15 | profile = os.Getenv("AWS_VAULT") 16 | } 17 | 18 | if len(region) == 0 { 19 | region = os.Getenv("AWS_REGION") 20 | } 21 | 22 | if len(region) > 0 { 23 | content = "(" + region + ")" 24 | } 25 | 26 | if len(profile) > 0 { 27 | content = profile +" "+ content 28 | } 29 | 30 | if len(content) == 0 { 31 | return []pwl.Segment{} 32 | } 33 | 34 | return []pwl.Segment{{ 35 | Name: "aws", 36 | Content: content, 37 | Foreground: p.theme.AWSFg, 38 | Background: p.theme.AWSBg, 39 | }} 40 | } 41 | -------------------------------------------------------------------------------- /exitcode/exitcode_solaris.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | var Signals = map[int]string{ 4 | 0x01: "SIGHUP", 5 | 0x02: "SIGINT", 6 | 0x03: "SIGQUIT", 7 | 0x04: "SIGILL", 8 | 0x05: "SIGTRAP", 9 | 0x06: "SIGABRT", 10 | 0x07: "SIGEMT", 11 | 0x08: "SIGFPE", 12 | 0x09: "SIGKILL", 13 | 0x0a: "SIGBUS", 14 | 0x0b: "SIGSEGV", 15 | 0x0c: "SIGSYS", 16 | 0x0d: "SIGPIPE", 17 | 0x0e: "SIGALRM", 18 | 0x0f: "SIGTERM", 19 | 0x10: "SIGUSR1", 20 | 0x11: "SIGUSR2", 21 | 0x12: "SIGCHLD", 22 | 0x13: "SIGPWR", 23 | 0x14: "SIGWINCH", 24 | 0x15: "SIGURG", 25 | 0x16: "SIGIO", 26 | 0x17: "SIGSTOP", 27 | 0x18: "SIGTSTP", 28 | 0x19: "SIGCONT", 29 | 0x1a: "SIGTTIN", 30 | 0x1b: "SIGTTOU", 31 | 0x1c: "SIGVTALRM", 32 | 0x1d: "SIGPROF", 33 | 0x1e: "SIGXCPU", 34 | 0x1f: "SIGXFSZ", 35 | 0x20: "SIGWAITING", 36 | 0x21: "SIGLWP", 37 | 0x22: "SIGFREEZE", 38 | 0x23: "SIGTHAW", 39 | 0x24: "SIGCANCEL", 40 | 0x25: "SIGLOST", 41 | 0x26: "SIGXRES", 42 | 0x27: "SIGJVM1", 43 | 0x28: "SIGJVM2", 44 | } 45 | -------------------------------------------------------------------------------- /segment-termtitle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Port of set_term_title segment from powerine-shell: 4 | // https://github.com/b-ryan/powerline-shell/blob/master/powerline_shell/segments/set_term_title.py 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | pwl "github.com/justjanne/powerline-go/powerline" 12 | ) 13 | 14 | func segmentTermTitle(p *powerline) []pwl.Segment { 15 | var title string 16 | 17 | term := os.Getenv("TERM") 18 | if !(strings.Contains(term, "xterm") || strings.Contains(term, "rxvt")) { 19 | return []pwl.Segment{} 20 | } 21 | 22 | if p.cfg.Shell == "bash" { 23 | title = "\\[\\e]0;\\u@\\h: \\w\\a\\]" 24 | } else if p.cfg.Shell == "zsh" { 25 | title = "%{\033]0;%n@%m: %~\007%}" 26 | } else { 27 | cwd := p.cwd 28 | title = fmt.Sprintf("\033]0;%s@%s: %s\007", p.username, p.hostname, cwd) 29 | } 30 | 31 | return []pwl.Segment{{ 32 | Name: "termtitle", 33 | Content: title, 34 | Priority: MaxInteger, // do not truncate 35 | HideSeparators: true, // do not draw separators 36 | }} 37 | } 38 | -------------------------------------------------------------------------------- /segment-gitlite.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | pwl "github.com/justjanne/powerline-go/powerline" 8 | ) 9 | 10 | func segmentGitLite(p *powerline) []pwl.Segment { 11 | if len(p.ignoreRepos) > 0 { 12 | out, err := runGitCommand("git", "--no-optional-locks", "rev-parse", "--show-toplevel") 13 | if err != nil { 14 | return []pwl.Segment{} 15 | } 16 | out = strings.TrimSpace(out) 17 | if p.ignoreRepos[out] { 18 | return []pwl.Segment{} 19 | } 20 | } 21 | 22 | out, err := runGitCommand("git", "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD") 23 | if err != nil { 24 | return []pwl.Segment{} 25 | } 26 | 27 | status := strings.TrimSpace(out) 28 | var branch string 29 | 30 | if status == "HEAD" { 31 | branch = getGitDetachedBranch(p) 32 | } else { 33 | branch = status 34 | } 35 | 36 | if p.cfg.GitMode != "compact" && len(p.symbols.RepoBranch) > 0 { 37 | branch = fmt.Sprintf("%s %s", p.symbols.RepoBranch, branch) 38 | } 39 | 40 | return []pwl.Segment{{ 41 | Name: "git-branch", 42 | Content: branch, 43 | Foreground: p.theme.RepoCleanFg, 44 | Background: p.theme.RepoCleanBg, 45 | }} 46 | } 47 | -------------------------------------------------------------------------------- /generatePreview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FLAGS="-modules cwd,git,root -path-aliases=/tmp/home=~" 4 | 5 | mkdir -p /tmp/home/code/dotfiles; 6 | cd /tmp/home/code/dotfiles/; 7 | git init; 8 | touch file1; 9 | git add .; 10 | git commit -m "commit"; 11 | echo "hI" > file2; 12 | 13 | mkdir -p /tmp/home/deep/down/into/the/abyss/of/directories/where/no/one/ever/comes/; 14 | cd /tmp/home/deep/down/into/the/abyss/of/directories/where/no/one/ever/comes/; 15 | git init; 16 | touch file1; 17 | git add .; 18 | git commit -m "commit"; 19 | echo "test">file1; 20 | git stash; 21 | 22 | export HOME=/tmp/home/ 23 | 24 | clear; 25 | 26 | cd /tmp/home/code/dotfiles/; 27 | powerline-go -shell bare $FLAGS; 28 | echo git branch; 29 | git branch --color=always | cat; 30 | 31 | powerline-go -shell bare $FLAGS; 32 | echo badcmd; 33 | echo "bash: badcmd: command not found"; 34 | 35 | powerline-go -shell bare $FLAGS -error 1; 36 | echo "cd ~/deep/down/into/the/abyss/of/directories/where/no/one/ever/comes/"; 37 | cd /tmp/home/deep/down/into/the/abyss/of/directories/where/no/one/ever/comes/; 38 | 39 | powerline-go -shell bare $FLAGS; 40 | 41 | echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo;echo; 42 | -------------------------------------------------------------------------------- /segment-docker_context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | type DockerContextConfig struct { 12 | CurrentContext string `json:"currentContext"` 13 | } 14 | 15 | func segmentDockerContext(p *powerline) []pwl.Segment { 16 | context := "default" 17 | home, _ := os.LookupEnv("HOME") 18 | 19 | configDir, ok := os.LookupEnv("DOCKER_CONFIG") 20 | if !ok { 21 | configDir = filepath.Join(home, ".docker") 22 | } 23 | 24 | contextFolder := filepath.Join(configDir, "contexts") 25 | configFile := filepath.Join(configDir, "config.json") 26 | contextEnvVar := os.Getenv("DOCKER_CONTEXT") 27 | 28 | if contextEnvVar != "" { 29 | context = contextEnvVar 30 | } else { 31 | stat, err := os.Stat(contextFolder) 32 | if err == nil && stat.IsDir() { 33 | dockerConfigFile, err := os.ReadFile(configFile) 34 | if err == nil { 35 | var dockerConfig DockerContextConfig 36 | err = json.Unmarshal(dockerConfigFile, &dockerConfig) 37 | if err == nil && dockerConfig.CurrentContext != "" { 38 | context = dockerConfig.CurrentContext 39 | } 40 | } 41 | } 42 | } 43 | 44 | // Don’t show the default context 45 | if context == "default" { 46 | return []pwl.Segment{} 47 | } 48 | 49 | return []pwl.Segment{{ 50 | Name: "docker-context", 51 | Content: "🐳" + context, 52 | Foreground: p.theme.PlEnvFg, 53 | Background: p.theme.PlEnvBg, 54 | }} 55 | } 56 | -------------------------------------------------------------------------------- /segment-exitcode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/justjanne/powerline-go/exitcode" 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | var exitCodes = map[int]string{ 12 | // 1: generic 13 | 1: "ERROR", 14 | // 2, 126, 127: common shell conventions, e.g. bash, zsh 15 | 2: "USAGE", 16 | 126: "NOEXEC", 17 | 127: "NOTFOUND", 18 | // 64-78: BSD, sysexits.h 19 | 64: "USAGE", 20 | 65: "DATAERR", 21 | 66: "NOINPUT", 22 | 67: "NOUSER", 23 | 68: "NOHOST", 24 | 69: "UNAVAILABLE", 25 | 70: "SOFTWARE", 26 | 71: "OSERR", 27 | 72: "OSFILE", 28 | 73: "CANTCREAT", 29 | 74: "IOERR", 30 | 75: "TEMPFAIL", 31 | 76: "PROTOCOL", 32 | 77: "NOPERM", 33 | 78: "CONFIG", 34 | } 35 | 36 | func getMeaningFromExitCode(exitCode int) string { 37 | if exitCode < 128 { 38 | name, ok := exitCodes[exitCode] 39 | if ok { 40 | return name 41 | } 42 | } else { 43 | name, ok := exitcode.Signals[exitCode-128] 44 | if ok { 45 | return name 46 | } 47 | } 48 | 49 | return fmt.Sprintf("%d", exitCode) 50 | } 51 | 52 | func segmentExitCode(p *powerline) []pwl.Segment { 53 | var meaning string 54 | if p.cfg.PrevError == 0 { 55 | return []pwl.Segment{} 56 | } 57 | if p.cfg.NumericExitCodes { 58 | meaning = strconv.Itoa(p.cfg.PrevError) 59 | } else { 60 | meaning = getMeaningFromExitCode(p.cfg.PrevError) 61 | } 62 | 63 | return []pwl.Segment{{ 64 | Name: "exit", 65 | Content: meaning, 66 | Foreground: p.theme.CmdFailedFg, 67 | Background: p.theme.CmdFailedBg, 68 | }} 69 | } 70 | -------------------------------------------------------------------------------- /powerline/powerline.go: -------------------------------------------------------------------------------- 1 | package powerline 2 | 3 | import ( 4 | runewidth "github.com/mattn/go-runewidth" 5 | ) 6 | 7 | // Segment describes an information to display on the command line prompt 8 | type Segment struct { 9 | Name string 10 | // Content is the text to be displayed on the command line prompt 11 | Content string 12 | // Foreground is the text color (see https://misc.flogisoft.com/bash/tip_colors_and_formatting#background1) 13 | Foreground uint8 14 | // Background is the color of the filling background (see https://misc.flogisoft.com/bash/tip_colors_and_formatting#background1) 15 | Background uint8 16 | // Separator is the character to be used when generating multiple segments to override the default separator 17 | Separator string 18 | // SeparatorForeground is the character to be used when generating multiple segments to override the default foreground separator 19 | SeparatorForeground uint8 20 | // Priority is the priority of the segment. The higher, the less probable the segment will be dropped if the total length is too long 21 | Priority int 22 | // HideSeparators indicated not to display any separator with next segment. 23 | HideSeparators bool 24 | Width int 25 | // NewLine defines a newline segment to break the powerline in multi lines 26 | NewLine bool 27 | } 28 | 29 | func (s Segment) ComputeWidth(condensed bool) int { 30 | if condensed { 31 | return runewidth.StringWidth(s.Content) + runewidth.StringWidth(s.Separator) 32 | } 33 | return runewidth.StringWidth(s.Content) + runewidth.StringWidth(s.Separator) + 2 34 | } 35 | -------------------------------------------------------------------------------- /segment-node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | pwl "github.com/justjanne/powerline-go/powerline" 11 | ) 12 | 13 | const pkgfile = "./package.json" 14 | 15 | type packageJSON struct { 16 | Version string `json:"version"` 17 | } 18 | 19 | func getNodeVersion() string { 20 | out, err := exec.Command("node", "--version").Output() 21 | if err != nil { 22 | return "" 23 | } 24 | return strings.TrimSuffix(string(out), "\n") 25 | } 26 | 27 | func getPackageVersion() string { 28 | stat, err := os.Stat(pkgfile) 29 | if err != nil { 30 | return "" 31 | } 32 | if stat.IsDir() { 33 | return "" 34 | } 35 | pkg := packageJSON{""} 36 | raw, err := ioutil.ReadFile(pkgfile) 37 | if err != nil { 38 | return "" 39 | } 40 | err = json.Unmarshal(raw, &pkg) 41 | if err != nil { 42 | return "" 43 | } 44 | 45 | return strings.TrimSpace(pkg.Version) 46 | } 47 | 48 | func segmentNode(p *powerline) []pwl.Segment { 49 | nodeVersion := getNodeVersion() 50 | packageVersion := getPackageVersion() 51 | 52 | segments := []pwl.Segment{} 53 | 54 | if nodeVersion != "" { 55 | segments = append(segments, pwl.Segment{ 56 | Name: "node", 57 | Content: p.symbols.NodeIndicator + " " + nodeVersion, 58 | Foreground: p.theme.NodeVersionFg, 59 | Background: p.theme.NodeVersionBg, 60 | }) 61 | } 62 | 63 | if packageVersion != "" { 64 | segments = append(segments, pwl.Segment{ 65 | Name: "node-segment", 66 | Content: packageVersion + " " + p.symbols.NodeIndicator, 67 | Foreground: p.theme.NodeFg, 68 | Background: p.theme.NodeBg, 69 | }) 70 | } 71 | 72 | return segments 73 | } 74 | -------------------------------------------------------------------------------- /segment-virtualenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strings" 7 | 8 | "gopkg.in/ini.v1" 9 | 10 | pwl "github.com/justjanne/powerline-go/powerline" 11 | ) 12 | 13 | func segmentVirtualEnv(p *powerline) []pwl.Segment { 14 | env := os.Getenv("VIRTUAL_ENV_PROMPT") 15 | if strings.HasPrefix(env, "(") && strings.HasSuffix(env, ") ") { 16 | env = strings.TrimPrefix(env, "(") 17 | env = strings.TrimSuffix(env, ") ") 18 | } 19 | if env == "" { 20 | venv, _ := os.LookupEnv("VIRTUAL_ENV") 21 | if venv != "" { 22 | cfg, err := ini.Load(path.Join(venv, "pyvenv.cfg")) 23 | if err == nil { 24 | // python >= 3.6 the venv module will not insert a prompt 25 | // key unless the `--prompt` flag is passed to the module 26 | // or if calling with the prompt arg EnvBuilder 27 | // otherwise env evaluates to an empty string, per return 28 | // of ini.File.Section.Key 29 | if pyEnv := cfg.Section("").Key("prompt").String(); pyEnv != "" { 30 | env = pyEnv 31 | } 32 | } 33 | if env == "" { 34 | env = venv 35 | } 36 | } 37 | } 38 | if env == "" { 39 | env, _ = os.LookupEnv("CONDA_ENV_PATH") 40 | } 41 | if env == "" { 42 | env, _ = os.LookupEnv("CONDA_DEFAULT_ENV") 43 | } 44 | if env == "" { 45 | env, _ = os.LookupEnv("PYENV_VERSION") 46 | } 47 | if env == "" { 48 | return []pwl.Segment{} 49 | } 50 | envName := path.Base(env) 51 | if p.cfg.VenvNameSizeLimit > 0 && len(envName) > p.cfg.VenvNameSizeLimit { 52 | envName = p.symbols.VenvIndicator 53 | } 54 | 55 | return []pwl.Segment{{ 56 | Name: "venv", 57 | Content: escapeVariables(p, envName), 58 | Foreground: p.theme.VirtualEnvFg, 59 | Background: p.theme.VirtualEnvBg, 60 | }} 61 | } 62 | -------------------------------------------------------------------------------- /segment-hg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | func getHgStatus() (bool, bool, bool) { 12 | hasModifiedFiles := false 13 | hasUntrackedFiles := false 14 | hasMissingFiles := false 15 | 16 | out, err := exec.Command("hg", "status").Output() 17 | if err == nil { 18 | output := strings.Split(string(out), "\n") 19 | for _, line := range output { 20 | if line != "" { 21 | if line[0] == '?' { 22 | hasUntrackedFiles = true 23 | } else if line[0] == '!' { 24 | hasMissingFiles = true 25 | } else { 26 | hasModifiedFiles = true 27 | } 28 | } 29 | } 30 | } 31 | return hasModifiedFiles, hasUntrackedFiles, hasMissingFiles 32 | } 33 | 34 | func segmentHg(p *powerline) []pwl.Segment { 35 | out, _ := exec.Command("hg", "branch").Output() 36 | output := strings.SplitN(string(out), "\n", 2) 37 | if !(len(output) > 0 && output[0] != "") { 38 | return []pwl.Segment{} 39 | } 40 | branch := output[0] 41 | hasModifiedFiles, hasUntrackedFiles, hasMissingFiles := getHgStatus() 42 | 43 | var foreground, background uint8 44 | var content string 45 | if hasModifiedFiles || hasUntrackedFiles || hasMissingFiles { 46 | foreground = p.theme.RepoDirtyFg 47 | background = p.theme.RepoDirtyBg 48 | 49 | extra := "" 50 | 51 | if hasUntrackedFiles { 52 | extra += "+" 53 | } 54 | 55 | if hasMissingFiles { 56 | extra += "!" 57 | } 58 | 59 | content = fmt.Sprintf("%s %s", branch, extra) 60 | } else { 61 | foreground = p.theme.RepoCleanFg 62 | background = p.theme.RepoCleanBg 63 | 64 | content = branch 65 | } 66 | 67 | return []pwl.Segment{{ 68 | Name: "hg", 69 | Content: content, 70 | Foreground: foreground, 71 | Background: background, 72 | }} 73 | } 74 | -------------------------------------------------------------------------------- /segment-bzr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func getBzrStatus() (bool, bool, bool) { 11 | hasModifiedFiles := false 12 | hasUntrackedFiles := false 13 | hasMissingFiles := false 14 | 15 | out, err := exec.Command("bzr", "status").Output() 16 | if err == nil { 17 | output := strings.Split(string(out), "\n") 18 | for _, line := range output { 19 | if line != "" { 20 | if line == "unknown:" { 21 | hasUntrackedFiles = true 22 | } else if line == "missing:" { 23 | hasMissingFiles = true 24 | } else { 25 | hasModifiedFiles = true 26 | } 27 | } 28 | } 29 | } 30 | return hasModifiedFiles, hasUntrackedFiles, hasMissingFiles 31 | } 32 | 33 | func segmentBzr(p *powerline) []pwl.Segment { 34 | out, _ := exec.Command("bzr", "nick").Output() 35 | output := strings.SplitN(string(out), "\n", 2) 36 | if len(output) > 0 && output[0] != "" { 37 | branch := output[0] 38 | hasModifiedFiles, hasUntrackedFiles, hasMissingFiles := getBzrStatus() 39 | 40 | var foreground, background uint8 41 | var content string 42 | if hasModifiedFiles || hasUntrackedFiles || hasMissingFiles { 43 | foreground = p.theme.RepoDirtyFg 44 | background = p.theme.RepoDirtyBg 45 | 46 | extra := "" 47 | 48 | if hasUntrackedFiles { 49 | extra += "+" 50 | } 51 | 52 | if hasMissingFiles { 53 | extra += "!" 54 | } 55 | 56 | if hasUntrackedFiles { 57 | extra += "?" 58 | } 59 | 60 | content = fmt.Sprintf("%s %s", branch, extra) 61 | } else { 62 | foreground = p.theme.RepoCleanFg 63 | background = p.theme.RepoCleanBg 64 | 65 | content = branch 66 | } 67 | 68 | return []pwl.Segment{{ 69 | Name: "bzr", 70 | Content: content, 71 | Foreground: foreground, 72 | Background: background, 73 | }} 74 | } 75 | return []pwl.Segment{} 76 | } 77 | -------------------------------------------------------------------------------- /segment-hostname.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func getHostName(fullyQualifiedDomainName string, keepFqdnHostname bool) string { 12 | if keepFqdnHostname { 13 | return fullyQualifiedDomainName 14 | } 15 | return strings.SplitN(fullyQualifiedDomainName, ".", 2)[0] 16 | } 17 | 18 | func getMd5(text string) []byte { 19 | hasher := md5.New() 20 | hasher.Write([]byte(text)) 21 | return hasher.Sum(nil) 22 | } 23 | 24 | func segmentHost(p *powerline) []pwl.Segment { 25 | var hostPrompt string 26 | var foreground, background uint8 27 | 28 | if p.cfg.HostnameOnlyIfSSH { 29 | if os.Getenv("SSH_CLIENT") == "" { 30 | // It's not an ssh connection do nothing 31 | return []pwl.Segment{} 32 | } 33 | } 34 | 35 | if p.cfg.ColorizeHostname { 36 | hostName := getHostName(p.hostname, p.cfg.FqdnHostname) 37 | hostPrompt = hostName 38 | 39 | foregroundEnv, foregroundEnvErr := strconv.ParseUint(os.Getenv("PLGO_HOSTNAMEFG"), 0, 8) 40 | backgroundEnv, backgroundEnvErr := strconv.ParseUint(os.Getenv("PLGO_HOSTNAMEBG"), 0, 8) 41 | if foregroundEnvErr == nil && backgroundEnvErr == nil { 42 | foreground = uint8(foregroundEnv) 43 | background = uint8(backgroundEnv) 44 | } else { 45 | hash := getMd5(hostName) 46 | background = hash[0] % 128 47 | foreground = p.theme.HostnameColorizedFgMap[background] 48 | } 49 | } else { 50 | if p.cfg.Shell == "bash" { 51 | hostPrompt = "\\h" 52 | } else if p.cfg.Shell == "zsh" { 53 | hostPrompt = "%m" 54 | } else { 55 | hostPrompt = getHostName(p.hostname, p.cfg.FqdnHostname) 56 | } 57 | 58 | foreground = p.theme.HostnameFg 59 | background = p.theme.HostnameBg 60 | } 61 | 62 | return []pwl.Segment{{ 63 | Name: "host", 64 | Content: hostPrompt, 65 | Foreground: foreground, 66 | Background: background, 67 | }} 68 | } 69 | -------------------------------------------------------------------------------- /segment-fossil.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func getFossilStatus() (bool, bool, bool) { 11 | hasModifiedFiles := false 12 | hasUntrackedFiles := false 13 | hasMissingFiles := false 14 | 15 | out, err := exec.Command("fossil", "changes", "--differ").Output() 16 | if err == nil { 17 | output := strings.Split(string(out), "\n") 18 | for _, line := range output { 19 | if line != "" { 20 | if strings.HasPrefix(line, "EXTRA") { 21 | hasUntrackedFiles = true 22 | } else if strings.HasPrefix(line, "MISSING") { 23 | hasMissingFiles = true 24 | } else { 25 | hasModifiedFiles = true 26 | } 27 | } 28 | } 29 | } 30 | return hasModifiedFiles, hasUntrackedFiles, hasMissingFiles 31 | } 32 | 33 | func segmentFossil(p *powerline) []pwl.Segment { 34 | out, _ := exec.Command("fossil", "branch", "current").Output() 35 | output := strings.SplitN(string(out), "\n", 2) 36 | if len(output) > 0 && output[0] != "" { 37 | branch := output[0] 38 | hasModifiedFiles, hasUntrackedFiles, hasMissingFiles := getFossilStatus() 39 | 40 | var foreground, background uint8 41 | var content string 42 | if hasModifiedFiles || hasUntrackedFiles || hasMissingFiles { 43 | foreground = p.theme.RepoDirtyFg 44 | background = p.theme.RepoDirtyBg 45 | 46 | extra := "" 47 | 48 | if hasUntrackedFiles { 49 | extra += "+" 50 | } 51 | 52 | if hasMissingFiles { 53 | extra += "!" 54 | } 55 | 56 | if hasUntrackedFiles { 57 | extra += "?" 58 | } 59 | 60 | content = fmt.Sprintf("%s %s", branch, extra) 61 | } else { 62 | foreground = p.theme.RepoCleanFg 63 | background = p.theme.RepoCleanBg 64 | 65 | content = branch 66 | } 67 | 68 | return []pwl.Segment{{ 69 | Name: "fossil", 70 | Content: content, 71 | Foreground: foreground, 72 | Background: background, 73 | }} 74 | } 75 | return []pwl.Segment{} 76 | } 77 | -------------------------------------------------------------------------------- /segment-rvm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | pwl "github.com/justjanne/powerline-go/powerline" 10 | ) 11 | 12 | func runRvmCommand(cmd string, args ...string) (string, error) { 13 | command := exec.Command(cmd, args...) 14 | out, err := command.Output() 15 | return string(out), err 16 | } 17 | 18 | // check RUBY_VERSION variable 19 | func checkEnvForRubyVersion() (string, error) { 20 | rubyVersion := os.Getenv("RUBY_VERSION") 21 | if len(rubyVersion) <= 0 { 22 | return "", errors.New("Not found in RUBY_VERSION") 23 | } 24 | return rubyVersion, nil 25 | } 26 | 27 | // check GEM_HOME variable for gemset information 28 | func checkEnvForRubyGemset() (string, error) { 29 | gemHomeSegments := strings.Split(os.Getenv("GEM_HOME"), "@") 30 | 31 | if len(gemHomeSegments) <= 1 { 32 | return "", errors.New("Gemset not found in GEM_HOME") 33 | } 34 | 35 | return gemHomeSegments[1], nil 36 | } 37 | 38 | // retrieve ruby version from RVM 39 | func checkForRvmOutput() (string, error) { 40 | // ask RVM what the current ruby version is 41 | out, err := runRvmCommand("rvm", "current") 42 | if err != nil { 43 | return "", errors.New("Not found in RVM output") 44 | } 45 | items := strings.Split(out, " ") 46 | if len(items) <= 0 { 47 | return "", errors.New("Not found in RVM output") 48 | } 49 | 50 | return items[0], nil 51 | } 52 | 53 | func segmentRvm(p *powerline) []pwl.Segment { 54 | var ( 55 | segment string 56 | err error 57 | ) 58 | 59 | segment, err = checkEnvForRubyVersion() 60 | if err != nil { 61 | segment, err = checkForRubyVersionFileInTree() 62 | } 63 | if err != nil { 64 | segment, err = checkForRvmOutput() 65 | } 66 | if err != nil { 67 | return []pwl.Segment{} 68 | } 69 | 70 | // Remove explicit "ruby-" prefix from segment because it's superfluous 71 | segment_components := strings.Split(segment, "-") 72 | if len(segment_components) > 1 { 73 | segment = segment_components[1] 74 | } 75 | 76 | // If gemset is missing from segment, get that info from the environment 77 | segment_components = strings.Split(segment, "@") 78 | if len(segment_components) < 2 { 79 | gemset, err := checkEnvForRubyGemset() 80 | if err == nil && gemset != "" { 81 | segment = segment + "@" + gemset 82 | } 83 | } 84 | 85 | return []pwl.Segment{{ 86 | Name: "rvm", 87 | Content: p.symbols.RvmIndicator + " " + segment, 88 | Foreground: p.theme.RvmFg, 89 | Background: p.theme.RvmBg, 90 | }} 91 | } 92 | -------------------------------------------------------------------------------- /segment-rbenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | pwl "github.com/justjanne/powerline-go/powerline" 12 | ) 13 | 14 | const rubyVersionFileSuffix = "/.ruby-version" 15 | const globalVersionFileSuffix = "/.rbenv/version" 16 | 17 | func runRbenvCommand(cmd string, args ...string) (string, error) { 18 | command := exec.Command(cmd, args...) 19 | out, err := command.Output() 20 | return string(out), err 21 | } 22 | 23 | // check RBENV_VERSION variable 24 | func checkEnvForRbenvVersion() (string, error) { 25 | rbenvVersion := os.Getenv("RBENV_VERSION") 26 | if len(rbenvVersion) <= 0 { 27 | return "", errors.New("Not found in RBENV_VERSION") 28 | } 29 | return rbenvVersion, nil 30 | } 31 | 32 | // check existence of .ruby_version in tree until root path 33 | func checkForRubyVersionFileInTree() (string, error) { 34 | var ( 35 | workingDirectory string 36 | err error 37 | ) 38 | 39 | workingDirectory, err = os.Getwd() 40 | if err == nil { 41 | for workingDirectory != "/" { 42 | rubyVersion, rubyVersionErr := ioutil.ReadFile(workingDirectory + rubyVersionFileSuffix) 43 | if rubyVersionErr == nil { 44 | return strings.TrimSpace(string(rubyVersion)), nil 45 | } 46 | 47 | workingDirectory = filepath.Dir(workingDirectory) 48 | } 49 | } 50 | 51 | return "", errors.New("No .ruby_version file found in tree") 52 | } 53 | 54 | // check for global version 55 | func checkForGlobalVersion() (string, error) { 56 | homeDir, _ := os.UserHomeDir() 57 | globalRubyVersion, err := ioutil.ReadFile(homeDir + globalVersionFileSuffix) 58 | if err != nil { 59 | return "", errors.New("No global version file found in tree") 60 | } 61 | return strings.TrimSpace(string(globalRubyVersion)), nil 62 | } 63 | 64 | // retrieve rbenv version output 65 | func checkForRbenvOutput() (string, error) { 66 | // spawn rbenv and print out version 67 | out, err := runRbenvCommand("rbenv", "version") 68 | if err != nil { 69 | return "", errors.New("Not found in rbenv output") 70 | } 71 | items := strings.Split(out, " ") 72 | if len(items) <= 0 { 73 | return "", errors.New("Not found in rbenv output") 74 | } 75 | 76 | return items[0], nil 77 | } 78 | 79 | func segmentRbenv(p *powerline) []pwl.Segment { 80 | var ( 81 | segment string 82 | err error 83 | ) 84 | 85 | segment, err = checkEnvForRbenvVersion() 86 | if err != nil { 87 | segment, err = checkForRubyVersionFileInTree() 88 | } 89 | if err != nil { 90 | segment, err = checkForGlobalVersion() 91 | } 92 | if err != nil { 93 | segment, err = checkForRbenvOutput() 94 | } 95 | if err != nil { 96 | return []pwl.Segment{} 97 | } 98 | return []pwl.Segment{{ 99 | Name: "rbenv", 100 | Content: segment, 101 | Foreground: p.theme.TimeFg, 102 | Background: p.theme.TimeBg, 103 | }} 104 | } 105 | -------------------------------------------------------------------------------- /segment-goenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | pwl "github.com/justjanne/powerline-go/powerline" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | const goenvVersionFileSuffix = "/.go-version" 14 | const goenvVersionEnvVar = "GOENV_VERSION" 15 | const goenvGlobalVersionFileSuffix = "/.goenv/version" 16 | 17 | func runGoenvCommand(cmd string, args ...string) (string, error) { 18 | command := exec.Command(cmd, args...) 19 | out, err := command.Output() 20 | return string(out), err 21 | } 22 | 23 | // check GOENV_VERSION variable 24 | func checkEnvForGoenvVersion() (string, error) { 25 | goenvVersion := os.Getenv(goenvVersionEnvVar) 26 | if len(goenvVersion) > 0 { 27 | return goenvVersion, nil 28 | } else { 29 | return "", fmt.Errorf("Not found in %s", goenvVersionEnvVar) 30 | } 31 | } 32 | 33 | // check existence of .go-version in tree until root path 34 | func checkForGoVersionFileInTree() (string, error) { 35 | var ( 36 | workingDirectory string 37 | err error 38 | ) 39 | 40 | workingDirectory, err = os.Getwd() 41 | if err == nil { 42 | for workingDirectory != "/" { 43 | goVersion, goVersionErr := ioutil.ReadFile(workingDirectory + goenvVersionFileSuffix) 44 | if goVersionErr == nil { 45 | return strings.TrimSpace(string(goVersion)), nil 46 | } 47 | 48 | workingDirectory = filepath.Dir(workingDirectory) 49 | } 50 | } 51 | 52 | return "", fmt.Errorf("No %s file found in tree", goenvVersionFileSuffix) 53 | } 54 | 55 | // check for global version 56 | func checkForGoenvGlobalVersion() (string, error) { 57 | homeDir, _ := os.UserHomeDir() 58 | globalGoVersion, err := ioutil.ReadFile(homeDir + goenvGlobalVersionFileSuffix) 59 | if err == nil { 60 | return strings.TrimSpace(string(globalGoVersion)), nil 61 | } else { 62 | return "", fmt.Errorf("No global go version file found in %s", homeDir+goenvGlobalVersionFileSuffix) 63 | } 64 | } 65 | 66 | // retrieve goenv version output 67 | func checkForGoenvOutput() (string, error) { 68 | // spawn goenv and print out version 69 | out, err := runGoenvCommand("goenv", "version") 70 | if err == nil { 71 | items := strings.Split(out, " ") 72 | if len(items) > 1 { 73 | return items[0], nil 74 | } 75 | } 76 | 77 | return "", fmt.Errorf("Not found in goenv object") 78 | } 79 | 80 | func segmentGoenv(p *powerline) []pwl.Segment { 81 | global, _ := checkForGoenvGlobalVersion() 82 | 83 | segment, err := checkEnvForGoenvVersion() 84 | if err != nil || segment == global { 85 | segment, err = checkForGoVersionFileInTree() 86 | } 87 | if err != nil || segment == global { 88 | segment, err = checkForGoenvOutput() 89 | } 90 | if err != nil || segment == global { 91 | return []pwl.Segment{} 92 | } else { 93 | return []pwl.Segment{{ 94 | Name: "goenv", 95 | Content: segment, 96 | Foreground: p.theme.GoenvFg, 97 | Background: p.theme.GoenvBg, 98 | }} 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /segment-duration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | pwl "github.com/justjanne/powerline-go/powerline" 10 | ) 11 | 12 | const ( 13 | micro rune = '\u00B5' 14 | milli rune = 'm' 15 | second rune = 's' 16 | minute rune = 'm' 17 | hour rune = 'h' 18 | ) 19 | 20 | const ( 21 | nanoseconds int64 = 1 22 | microseconds int64 = nanoseconds * 1000 23 | milliseconds int64 = microseconds * 1000 24 | seconds int64 = milliseconds * 1000 25 | minutes int64 = seconds * 60 26 | hours int64 = minutes * 60 27 | ) 28 | 29 | func segmentDuration(p *powerline) []pwl.Segment { 30 | if p.cfg.Duration == "" { 31 | return []pwl.Segment{{ 32 | Name: "duration", 33 | Content: "No duration", 34 | Foreground: p.theme.DurationFg, 35 | Background: p.theme.DurationBg, 36 | }} 37 | } 38 | 39 | durationValue := strings.Trim(p.cfg.Duration, "'\"") 40 | durationMinValue := strings.Trim(p.cfg.DurationMin, "'\"") 41 | 42 | hasPrecision := strings.Contains(durationValue, ".") 43 | 44 | durationFloat, err := strconv.ParseFloat(durationValue, 64) 45 | durationMinFloat, _ := strconv.ParseFloat(durationMinValue, 64) 46 | if err != nil { 47 | return []pwl.Segment{{ 48 | Name: "duration", 49 | Content: fmt.Sprintf("Failed to convert '%s' to a number", p.cfg.Duration), 50 | Foreground: p.theme.DurationFg, 51 | Background: p.theme.DurationBg, 52 | }} 53 | } 54 | 55 | if durationFloat < durationMinFloat { 56 | return []pwl.Segment{} 57 | } 58 | 59 | duration := time.Duration(durationFloat * float64(time.Second.Nanoseconds())) 60 | 61 | if duration <= 0 { 62 | return []pwl.Segment{} 63 | } 64 | 65 | var content string 66 | ns := duration.Nanoseconds() 67 | if ns > hours { 68 | hrs := ns / hours 69 | ns -= hrs * hours 70 | mins := ns / minutes 71 | content = fmt.Sprintf("%d%c %d%c", hrs, hour, mins, minute) 72 | } else if ns > minutes { 73 | mins := ns / minutes 74 | ns -= mins * minutes 75 | secs := ns / seconds 76 | content = fmt.Sprintf("%d%c %d%c", mins, minute, secs, second) 77 | } else if !hasPrecision { 78 | secs := ns / seconds 79 | content = fmt.Sprintf("%d%c", secs, second) 80 | } else if ns > seconds { 81 | secs := ns / seconds 82 | ns -= secs * seconds 83 | millis := ns / milliseconds 84 | content = fmt.Sprintf("%d%c %d%c%c", secs, second, millis, milli, second) 85 | } else if ns > milliseconds || p.cfg.DurationLowPrecision { 86 | millis := ns / milliseconds 87 | ns -= millis * milliseconds 88 | micros := ns / microseconds 89 | if p.cfg.DurationLowPrecision { 90 | content = fmt.Sprintf("%d%c%c", millis, milli, second) 91 | } else { 92 | content = fmt.Sprintf("%d%c%c %d%c%c", millis, milli, second, micros, micro, second) 93 | } 94 | } else { 95 | content = fmt.Sprintf("%d%c%c", ns/microseconds, micro, second) 96 | } 97 | 98 | return []pwl.Segment{{ 99 | Name: "duration", 100 | Content: content, 101 | Foreground: p.theme.DurationFg, 102 | Background: p.theme.DurationBg, 103 | }} 104 | } 105 | -------------------------------------------------------------------------------- /segment-gcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "strings" 11 | 12 | pwl "github.com/justjanne/powerline-go/powerline" 13 | ) 14 | 15 | const gcloudCoreSectionHeader = "\n[core]\n" 16 | 17 | func getCloudConfigDir() (string, error) { 18 | p, err := os.UserHomeDir() 19 | if err != nil { 20 | return "", err 21 | } 22 | if runtime.GOOS != "windows" { 23 | p += "/.config" 24 | } 25 | p += "/gcloud" 26 | return p, nil 27 | } 28 | 29 | func getActiveGCloudConfig(configDir string) (string, error) { 30 | activeConfigPath := configDir + "/active_config" 31 | 32 | stat, err := os.Stat(activeConfigPath) 33 | if (err == nil && os.IsNotExist(err)) || (err == nil && stat.IsDir()) { 34 | return "default", nil 35 | } else if err != nil { 36 | return "", err 37 | } 38 | 39 | contents, err := os.ReadFile(activeConfigPath) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | config := strings.TrimSpace(string(contents)) 45 | if config == "" { 46 | config = "default" 47 | } 48 | 49 | return config, nil 50 | } 51 | 52 | func getGCPProjectFromGCloud() (string, error) { 53 | out, err := exec.Command("gcloud", "config", "list", "project", "--format", "value(core.project)").Output() 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | return strings.TrimSuffix(string(out), "\n"), nil 59 | } 60 | 61 | func getGCPProjectFromFile() (string, error) { 62 | configDir, err := getCloudConfigDir() 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | activeConfig, err := getActiveGCloudConfig(configDir) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | configPath := configDir + "/configurations/config_" + activeConfig 73 | stat, err := os.Stat(configPath) 74 | if err != nil { 75 | return "", err 76 | } else if stat.IsDir() { 77 | return "", fmt.Errorf("%s is a directory", configPath) 78 | } 79 | 80 | b, err := os.ReadFile(configPath) 81 | if err != nil { 82 | return "", err 83 | } 84 | b = append([]byte("\n"), b...) 85 | 86 | coreStart := bytes.Index(b, []byte(gcloudCoreSectionHeader)) 87 | if coreStart == -1 { 88 | return "", fmt.Errorf("could not find [core] section in %s", configPath) 89 | } 90 | b = b[coreStart+len(gcloudCoreSectionHeader):] 91 | 92 | coreEnd := bytes.Index(b, []byte("\n[")) 93 | if coreEnd != -1 { 94 | b = b[:coreEnd] 95 | } 96 | 97 | lines := bytes.Split(b[coreStart+len(gcloudCoreSectionHeader):], []byte("\n")) 98 | for _, line := range lines { 99 | parts := bytes.Split(line, []byte("=")) 100 | if len(parts) == 2 { 101 | if strings.TrimSpace(string(parts[0])) == "project" { 102 | return strings.TrimSpace(string(parts[1])), nil 103 | } 104 | } 105 | } 106 | 107 | return "", nil 108 | } 109 | 110 | func getGCPProject() (string, error) { 111 | if project, err := getGCPProjectFromFile(); err == nil { 112 | return project, nil 113 | } else { 114 | return getGCPProjectFromGCloud() 115 | } 116 | } 117 | 118 | func segmentGCP(p *powerline) []pwl.Segment { 119 | project, err := getGCPProject() 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | 124 | if project == "" { 125 | return []pwl.Segment{} 126 | } 127 | return []pwl.Segment{{ 128 | Name: "gcp", 129 | Content: project, 130 | Foreground: p.theme.GCPFg, 131 | Background: p.theme.GCPBg, 132 | }} 133 | } 134 | -------------------------------------------------------------------------------- /themes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Symbols of the theme 4 | type SymbolTemplate struct { 5 | Lock string 6 | Network string 7 | NetworkAlternate string 8 | Separator string 9 | SeparatorThin string 10 | SeparatorReverse string 11 | SeparatorReverseThin string 12 | 13 | RepoDetached string 14 | RepoBranch string 15 | RepoAhead string 16 | RepoBehind string 17 | RepoStaged string 18 | RepoNotStaged string 19 | RepoUntracked string 20 | RepoConflicted string 21 | RepoStashed string 22 | 23 | DotEnvIndicator string 24 | KubeIndicator string 25 | NixShellIndicator string 26 | NodeIndicator string 27 | RvmIndicator string 28 | VenvIndicator string 29 | } 30 | 31 | // Theme definitions 32 | type Theme struct { 33 | BoldForeground bool 34 | 35 | Reset uint8 36 | 37 | DefaultFg uint8 38 | DefaultBg uint8 39 | 40 | UsernameFg uint8 41 | UsernameBg uint8 42 | UsernameRootBg uint8 43 | 44 | HostnameFg uint8 45 | HostnameBg uint8 46 | 47 | // The foreground-background mapping is precomputed and stored in a map for improved performance 48 | // The old script used to brute-force this at runtime 49 | HostnameColorizedFgMap map[uint8]uint8 50 | 51 | HomeSpecialDisplay bool 52 | HomeFg uint8 53 | HomeBg uint8 54 | AliasFg uint8 55 | AliasBg uint8 56 | PathFg uint8 57 | PathBg uint8 58 | CwdFg uint8 59 | SeparatorFg uint8 60 | 61 | ReadonlyFg uint8 62 | ReadonlyBg uint8 63 | 64 | SSHFg uint8 65 | SSHBg uint8 66 | 67 | DockerMachineFg uint8 68 | DockerMachineBg uint8 69 | 70 | KubeClusterFg uint8 71 | KubeClusterBg uint8 72 | KubeNamespaceFg uint8 73 | KubeNamespaceBg uint8 74 | 75 | WSLMachineFg uint8 76 | WSLMachineBg uint8 77 | 78 | DotEnvFg uint8 79 | DotEnvBg uint8 80 | 81 | AWSFg uint8 82 | AWSBg uint8 83 | 84 | RepoCleanFg uint8 85 | RepoCleanBg uint8 86 | RepoDirtyFg uint8 87 | RepoDirtyBg uint8 88 | 89 | JobsFg uint8 90 | JobsBg uint8 91 | 92 | CmdPassedFg uint8 93 | CmdPassedBg uint8 94 | CmdFailedFg uint8 95 | CmdFailedBg uint8 96 | 97 | SvnChangesFg uint8 98 | SvnChangesBg uint8 99 | 100 | GCPFg uint8 101 | GCPBg uint8 102 | 103 | GitAheadFg uint8 104 | GitAheadBg uint8 105 | GitBehindFg uint8 106 | GitBehindBg uint8 107 | GitStagedFg uint8 108 | GitStagedBg uint8 109 | GitNotStagedFg uint8 110 | GitNotStagedBg uint8 111 | GitUntrackedFg uint8 112 | GitUntrackedBg uint8 113 | GitConflictedFg uint8 114 | GitConflictedBg uint8 115 | GitStashedFg uint8 116 | GitStashedBg uint8 117 | 118 | GoenvFg uint8 119 | GoenvBg uint8 120 | 121 | VirtualEnvFg uint8 122 | VirtualEnvBg uint8 123 | 124 | VirtualGoFg uint8 125 | VirtualGoBg uint8 126 | 127 | PerlbrewFg uint8 128 | PerlbrewBg uint8 129 | 130 | PlEnvFg uint8 131 | PlEnvBg uint8 132 | 133 | TFWsFg uint8 134 | TFWsBg uint8 135 | 136 | TimeFg uint8 137 | TimeBg uint8 138 | 139 | ShellVarFg uint8 140 | ShellVarBg uint8 141 | 142 | ShEnvFg uint8 143 | ShEnvBg uint8 144 | 145 | NodeFg uint8 146 | NodeBg uint8 147 | NodeVersionFg uint8 148 | NodeVersionBg uint8 149 | 150 | RvmFg uint8 151 | RvmBg uint8 152 | 153 | LoadFg uint8 154 | LoadBg uint8 155 | LoadHighBg uint8 156 | LoadAvgValue byte 157 | LoadThresholdBad float64 158 | 159 | NixShellFg uint8 160 | NixShellBg uint8 161 | 162 | DurationFg uint8 163 | DurationBg uint8 164 | 165 | ViModeCommandFg uint8 166 | ViModeCommandBg uint8 167 | ViModeInsertFg uint8 168 | ViModeInsertBg uint8 169 | } 170 | -------------------------------------------------------------------------------- /segment-kube.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pwl "github.com/justjanne/powerline-go/powerline" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // KubeContext holds the kubernetes context 16 | type KubeContext struct { 17 | Context struct { 18 | Cluster string 19 | Namespace string 20 | User string 21 | } 22 | Name string 23 | } 24 | 25 | // KubeConfig is the kubernetes configuration 26 | type KubeConfig struct { 27 | Contexts []KubeContext `yaml:"contexts"` 28 | CurrentContext string `yaml:"current-context"` 29 | } 30 | 31 | func homePath() string { 32 | return os.Getenv(homeEnvName()) 33 | } 34 | 35 | func readKubeConfig(config *KubeConfig, path string) (err error) { 36 | absolutePath, err := filepath.Abs(path) 37 | if err != nil { 38 | return 39 | } 40 | fileContent, err := ioutil.ReadFile(absolutePath) 41 | if err != nil { 42 | return 43 | } 44 | err = yaml.Unmarshal(fileContent, config) 45 | if err != nil { 46 | return 47 | } 48 | 49 | return 50 | } 51 | 52 | func segmentKube(p *powerline) []pwl.Segment { 53 | paths := append(strings.Split(os.Getenv("KUBECONFIG"), ":"), path.Join(homePath(), ".kube", "config")) 54 | config := &KubeConfig{} 55 | for _, configPath := range paths { 56 | temp := &KubeConfig{} 57 | if readKubeConfig(temp, configPath) == nil { 58 | config.Contexts = append(config.Contexts, temp.Contexts...) 59 | if config.CurrentContext == "" { 60 | config.CurrentContext = temp.CurrentContext 61 | } 62 | } 63 | } 64 | 65 | cluster := "" 66 | namespace := "" 67 | for _, context := range config.Contexts { 68 | if context.Name == config.CurrentContext { 69 | cluster = context.Name 70 | namespace = context.Context.Namespace 71 | break 72 | } 73 | } 74 | 75 | // When you use gke your clusters may look something like gke_projectname_availability-zone_cluster-01 76 | // instead I want it to read as `cluster-01` 77 | // So we remove the first 3 segments of this string, if the flag is set, and there are enough segments 78 | if strings.HasPrefix(cluster, "gke") && p.cfg.ShortenGKENames { 79 | segments := strings.Split(cluster, "_") 80 | if len(segments) > 3 { 81 | cluster = strings.Join(segments[3:], "_") 82 | } 83 | } 84 | 85 | // When you use openshift your clusters may look something like namespace/portal-url:port/user, 86 | // instead I want it to read as `portal-url`. 87 | // So we ensure there are three segments split by / and then choose the middle part, 88 | // we also remove the port number from the result. 89 | if p.cfg.ShortenOpenshiftNames { 90 | segments := strings.Split(cluster, "/") 91 | if len(segments) == 3 { 92 | cluster = segments[1] 93 | idx := strings.IndexByte(cluster, ':') 94 | if idx != -1 { 95 | cluster = cluster[0:idx] 96 | } 97 | } 98 | } 99 | 100 | // With AWS EKS, cluster names are ARNs; it makes more sense to shorten them 101 | // so "eks-infra" instead of "arn:aws:eks:us-east-1:XXXXXXXXXXXX:cluster/eks-infra 102 | const arnRegexString string = "^arn:aws:eks:[[:alnum:]-]+:[[:digit:]]+:cluster/(.*)$" 103 | arnRe := regexp.MustCompile(arnRegexString) 104 | 105 | if arnMatches := arnRe.FindStringSubmatch(cluster); arnMatches != nil && p.cfg.ShortenEKSNames { 106 | cluster = arnMatches[1] 107 | } 108 | segments := []pwl.Segment{} 109 | // Only draw the icon once 110 | kubeIconHasBeenDrawnYet := false 111 | if cluster != "" { 112 | kubeIconHasBeenDrawnYet = true 113 | segments = append(segments, pwl.Segment{ 114 | Name: "kube-cluster", 115 | Content: p.symbols.KubeIndicator + " " + cluster, 116 | Foreground: p.theme.KubeClusterFg, 117 | Background: p.theme.KubeClusterBg, 118 | }) 119 | } 120 | 121 | if namespace != "" { 122 | content := namespace 123 | if !kubeIconHasBeenDrawnYet { 124 | content = p.symbols.KubeIndicator + " " + content 125 | } 126 | segments = append(segments, pwl.Segment{ 127 | Name: "kube-namespace", 128 | Content: content, 129 | Foreground: p.theme.KubeNamespaceFg, 130 | Background: p.theme.KubeNamespaceBg, 131 | }) 132 | } 133 | return segments 134 | } 135 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type SymbolMap map[string]SymbolTemplate 11 | type ShellMap map[string]ShellInfo 12 | type ThemeMap map[string]Theme 13 | type AliasMap map[string]string 14 | 15 | type Config struct { 16 | CwdMode string `json:"cwd-mode"` 17 | CwdMaxDepth int `json:"cwd-max-depth"` 18 | CwdMaxDirSize int `json:"cwd-max-dir-size"` 19 | ColorizeHostname bool `json:"colorize-hostname"` 20 | FqdnHostname bool `json:"fqdn-hostname"` 21 | HostnameOnlyIfSSH bool `json:"hostname-only-if-ssh"` 22 | SshAlternateIcon bool `json:"alternate-ssh-icon"` 23 | EastAsianWidth bool `json:"east-asian-width"` 24 | PromptOnNewLine bool `json:"newline"` 25 | StaticPromptIndicator bool `json:"static-prompt-indicator"` 26 | VenvNameSizeLimit int `json:"venv-name-size-limit"` 27 | Jobs int `json:"-"` 28 | GitAssumeUnchangedSize int64 `json:"git-assume-unchanged-size"` 29 | GitDisableStats []string `json:"git-disable-stats"` 30 | GitMode string `json:"git-mode"` 31 | Mode string `json:"mode"` 32 | Theme string `json:"theme"` 33 | Shell string `json:"shell"` 34 | Modules []string `json:"modules"` 35 | ModulesRight []string `json:"modules-right"` 36 | Priority []string `json:"priority"` 37 | MaxWidthPercentage int `json:"max-width-percentage"` 38 | TruncateSegmentWidth int `json:"truncate-segment-width"` 39 | PrevError int `json:"-"` 40 | NumericExitCodes bool `json:"numeric-exit-codes"` 41 | IgnoreRepos []string `json:"ignore-repos"` 42 | ShortenGKENames bool `json:"shorten-gke-names"` 43 | ShortenEKSNames bool `json:"shorten-eks-names"` 44 | ShortenOpenshiftNames bool `json:"shorten-openshift-names"` 45 | ShellVar string `json:"shell-var"` 46 | ShellVarNoWarnEmpty bool `json:"shell-var-no-warn-empty"` 47 | TrimADDomain bool `json:"trim-ad-domain"` 48 | PathAliases AliasMap `json:"path-aliases"` 49 | Duration string `json:"-"` 50 | DurationMin string `json:"duration-min"` 51 | DurationLowPrecision bool `json:"duration-low-precision"` 52 | Eval bool `json:"eval"` 53 | Condensed bool `json:"condensed"` 54 | IgnoreWarnings bool `json:"ignore-warnings"` 55 | Modes SymbolMap `json:"modes"` 56 | Shells ShellMap `json:"shells"` 57 | Themes ThemeMap `json:"themes"` 58 | Time string `json:"-"` 59 | ViMode string `json:"vi-mode"` 60 | } 61 | 62 | func (mode *SymbolTemplate) UnmarshalJSON(data []byte) error { 63 | type Alias SymbolTemplate 64 | tmp := defaults.Modes[defaults.Mode] 65 | err := json.Unmarshal(data, (*Alias)(&tmp)) 66 | if err == nil { 67 | *mode = tmp 68 | } 69 | return err 70 | } 71 | 72 | func (theme *Theme) UnmarshalJSON(data []byte) error { 73 | type Alias Theme 74 | tmp := defaults.Themes[defaults.Theme] 75 | err := json.Unmarshal(data, (*Alias)(&tmp)) 76 | if err == nil { 77 | *theme = tmp 78 | } 79 | return err 80 | } 81 | 82 | func configPath() string { 83 | home, _ := os.UserHomeDir() 84 | return filepath.Join(home, ".config", "powerline-go", "config.json") 85 | } 86 | 87 | func (cfg *Config) Load() error { 88 | path := configPath() 89 | file, err := ioutil.ReadFile(path) 90 | if err != nil { 91 | return nil // fail silently 92 | } 93 | return json.Unmarshal(file, cfg) 94 | } 95 | 96 | func (cfg *Config) Save() error { 97 | path := configPath() 98 | tmp := cfg 99 | tmp.Themes = map[string]Theme{} 100 | tmp.Modes = map[string]SymbolTemplate{} 101 | tmp.Shells = map[string]ShellInfo{} 102 | data, err := json.MarshalIndent(tmp, "", " ") 103 | if err != nil { 104 | return err 105 | } 106 | return ioutil.WriteFile(path, data, 0644) 107 | } 108 | -------------------------------------------------------------------------------- /segment-subversion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | pwl "github.com/justjanne/powerline-go/powerline" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | var otherModified int 12 | 13 | func addSvnRepoStatsSegment(p *powerline, nChanges int, symbol string, foreground uint8, background uint8) (segments []pwl.Segment) { 14 | if nChanges > 0 { 15 | segments = append(segments, pwl.Segment{ 16 | Name: "svn-status", 17 | Content: fmt.Sprintf("%d%s", nChanges, symbol), 18 | Foreground: foreground, 19 | Background: background, 20 | }) 21 | } 22 | return segments 23 | } 24 | 25 | func (r repoStats) SvnSegments(p *powerline) (segments []pwl.Segment) { 26 | segments = append(segments, addSvnRepoStatsSegment(p, r.ahead, p.symbols.RepoAhead, p.theme.GitAheadFg, p.theme.GitAheadBg)...) 27 | segments = append(segments, addSvnRepoStatsSegment(p, r.behind, p.symbols.RepoBehind, p.theme.GitBehindFg, p.theme.GitBehindBg)...) 28 | segments = append(segments, addSvnRepoStatsSegment(p, r.staged, p.symbols.RepoStaged, p.theme.GitStagedFg, p.theme.GitStagedBg)...) 29 | segments = append(segments, addSvnRepoStatsSegment(p, r.notStaged, p.symbols.RepoNotStaged, p.theme.GitNotStagedFg, p.theme.GitNotStagedBg)...) 30 | segments = append(segments, addSvnRepoStatsSegment(p, r.untracked, p.symbols.RepoUntracked, p.theme.GitUntrackedFg, p.theme.GitUntrackedBg)...) 31 | segments = append(segments, addSvnRepoStatsSegment(p, r.conflicted, p.symbols.RepoConflicted, p.theme.GitConflictedFg, p.theme.GitConflictedBg)...) 32 | segments = append(segments, addSvnRepoStatsSegment(p, r.stashed, p.symbols.RepoStashed, p.theme.GitStashedFg, p.theme.GitStashedBg)...) 33 | return segments 34 | } 35 | 36 | func runSvnCommand(cmd string, args ...string) (string, error) { 37 | command := exec.Command(cmd, args...) 38 | out, err := command.Output() 39 | return string(out), err 40 | } 41 | 42 | func parseSvnURL() (map[string]string, error) { 43 | info, err := runSvnCommand("svn", "info") 44 | if err != nil { 45 | return nil, errors.New("not a working copy") 46 | } 47 | 48 | svnInfo := make(map[string]string, 0) 49 | infos := strings.Split(info, "\n") 50 | if len(infos) > 1 { 51 | for _, line := range infos[:] { 52 | items := strings.Split(line, ": ") 53 | if len(items) >= 2 { 54 | svnInfo[items[0]] = items[1] 55 | } 56 | } 57 | } 58 | 59 | return svnInfo, nil 60 | } 61 | 62 | func ensureUnmodified(code string, stats repoStats) { 63 | if code != " " { 64 | otherModified++ 65 | } 66 | } 67 | 68 | func parseSvnStatus() repoStats { 69 | stats := repoStats{} 70 | info, err := runSvnCommand("svn", "status", "-u") 71 | if err != nil { 72 | return stats 73 | } 74 | infos := strings.Split(info, "\n") 75 | if len(infos) > 1 { 76 | for _, line := range infos[:] { 77 | if len(line) >= 9 { 78 | code := line[0:1] 79 | switch code { 80 | case "?": 81 | stats.untracked++ 82 | case "C": 83 | stats.conflicted++ 84 | case "A", "D", "M": 85 | stats.notStaged++ 86 | default: 87 | ensureUnmodified(code, stats) 88 | } 89 | code = line[1:2] 90 | switch code { 91 | case "C": 92 | stats.conflicted++ 93 | case "M": 94 | stats.notStaged++ 95 | default: 96 | ensureUnmodified(code, stats) 97 | } 98 | ensureUnmodified(line[2:3], stats) 99 | ensureUnmodified(line[3:4], stats) 100 | ensureUnmodified(line[4:5], stats) 101 | ensureUnmodified(line[5:6], stats) 102 | ensureUnmodified(line[6:7], stats) 103 | ensureUnmodified(line[7:8], stats) 104 | code = line[8:9] 105 | switch code { 106 | case "*": 107 | stats.behind++ 108 | default: 109 | ensureUnmodified(code, stats) 110 | } 111 | } 112 | } 113 | } 114 | 115 | return stats 116 | } 117 | 118 | func segmentSubversion(p *powerline) []pwl.Segment { 119 | 120 | svnInfo, err := parseSvnURL() 121 | if err != nil { 122 | return []pwl.Segment{} 123 | } 124 | 125 | if len(p.ignoreRepos) > 0 { 126 | if p.ignoreRepos[svnInfo["URL"]] || p.ignoreRepos[svnInfo["Relative URL"]] { 127 | return []pwl.Segment{} 128 | } 129 | } 130 | 131 | svnStats := parseSvnStatus() 132 | 133 | var foreground, background uint8 134 | if svnStats.dirty() || otherModified > 0 { 135 | foreground = p.theme.RepoDirtyFg 136 | background = p.theme.RepoDirtyBg 137 | } else { 138 | foreground = p.theme.RepoCleanFg 139 | background = p.theme.RepoCleanBg 140 | } 141 | 142 | segments := []pwl.Segment{{ 143 | Name: "svn-branch", 144 | Content: svnInfo["Relative URL"], 145 | Foreground: foreground, 146 | Background: background, 147 | }} 148 | 149 | segments = append(segments, svnStats.SvnSegments(p)...) 150 | return segments 151 | } 152 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 4 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 5 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 7 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 8 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 9 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 10 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 11 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 15 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 16 | github.com/shirou/gopsutil/v3 v3.22.3 h1:UebRzEomgMpv61e3hgD1tGooqX5trFbdU/ehphbHd00= 17 | github.com/shirou/gopsutil/v3 v3.22.3/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 20 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= 22 | github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= 23 | github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= 24 | github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= 25 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 26 | github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= 27 | github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 28 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 29 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 30 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 33 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 44 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 46 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 47 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 50 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 51 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 52 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 53 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 54 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 55 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 56 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 57 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 62 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 63 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 64 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /themes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "BoldForeground": false, 3 | "Reset": 255, 4 | "UsernameFg": 250, 5 | "UsernameBg": 240, 6 | "UsernameRootBg": 124, 7 | "HostnameFg": 250, 8 | "HostnameBg": 238, 9 | "HomeSpecialDisplay": true, 10 | "HomeFg": 15, 11 | "HomeBg": 31, 12 | "AliasFg": 15, 13 | "AliasBg": 31, 14 | "PathFg": 250, 15 | "PathBg": 237, 16 | "CwdFg": 254, 17 | "SeparatorFg": 244, 18 | "ReadonlyFg": 254, 19 | "ReadonlyBg": 124, 20 | "SshFg": 254, 21 | "SshBg": 166, 22 | "DockerMachineFg": 177, 23 | "DockerMachineBg": 55, 24 | "WSLMachineFg": 250, 25 | "WSLMachineBg": 238, 26 | "DotEnvFg": 15, 27 | "DotEnvBg": 55, 28 | "RepoCleanFg": 0, 29 | "RepoCleanBg": 148, 30 | "RepoDirtyFg": 15, 31 | "RepoDirtyBg": 161, 32 | "JobsFg": 39, 33 | "JobsBg": 238, 34 | "CmdPassedFg": 15, 35 | "CmdPassedBg": 236, 36 | "CmdFailedFg": 15, 37 | "CmdFailedBg": 161, 38 | "SvnChangesFg": 22, 39 | "SvnChangesBg": 148, 40 | "GitAheadFg": 250, 41 | "GitAheadBg": 240, 42 | "GitBehindFg": 250, 43 | "GitBehindBg": 240, 44 | "GitStagedFg": 15, 45 | "GitStagedBg": 22, 46 | "GitNotStagedFg": 15, 47 | "GitNotStagedBg": 130, 48 | "GitUntrackedFg": 15, 49 | "GitUntrackedBg": 52, 50 | "GitConflictedFg": 15, 51 | "GitConflictedBg": 9, 52 | "GitStashedFg": 15, 53 | "GitStashedBg": 20, 54 | "VirtualEnvFg": 0, 55 | "VirtualEnvBg": 35, 56 | "PerlbrewFg": 0, 57 | "PerlbrewBg": 20, 58 | "PlEnvFg": 0, 59 | "PlEnvBg": 32, 60 | "TimeFg": 15, 61 | "TimeBg": 236, 62 | "ShellVarFg": 52, 63 | "ShellVarBg": 11, 64 | "ShEnvFg": 15, 65 | "ShEnvBg": 130, 66 | "NodeFg": 15, 67 | "NodeBg": 40, 68 | "NodeVersionFg": 40, 69 | "NodeVersionBg": 15, 70 | "RvmFg": 255, 71 | "RvmBg": 160, 72 | "LoadFg": 15, 73 | "LoadBg": 22, 74 | "LoadHighBg": 161, 75 | "LoadAvgValue": 5, 76 | "LoadThresholdBad": 1.0, 77 | "NixShellFg": 15, 78 | "NixShellBg": 69, 79 | "DurationFg": 250, 80 | "DurationBg": 237, 81 | "HostnameColorizedFgMap": { 82 | "0": 250, 83 | "1": 250, 84 | "2": 120, 85 | "3": 228, 86 | "4": 250, 87 | "5": 250, 88 | "6": 123, 89 | "7": 238, 90 | "8": 0, 91 | "9": 0, 92 | "10": 0, 93 | "11": 0, 94 | "12": 250, 95 | "13": 0, 96 | "14": 0, 97 | "15": 242, 98 | "16": 250, 99 | "17": 250, 100 | "18": 250, 101 | "19": 189, 102 | "20": 254, 103 | "21": 250, 104 | "22": 83, 105 | "23": 87, 106 | "24": 117, 107 | "25": 188, 108 | "26": 254, 109 | "27": 0, 110 | "28": 120, 111 | "29": 122, 112 | "30": 123, 113 | "31": 159, 114 | "32": 255, 115 | "33": 0, 116 | "34": 157, 117 | "35": 158, 118 | "36": 159, 119 | "37": 159, 120 | "38": 195, 121 | "39": 0, 122 | "40": 194, 123 | "41": 194, 124 | "42": 195, 125 | "43": 195, 126 | "44": 195, 127 | "45": 0, 128 | "46": 0, 129 | "47": 0, 130 | "48": 0, 131 | "49": 0, 132 | "50": 0, 133 | "51": 0, 134 | "52": 250, 135 | "53": 250, 136 | "54": 250, 137 | "55": 189, 138 | "56": 254, 139 | "57": 250, 140 | "58": 227, 141 | "59": 253, 142 | "60": 255, 143 | "61": 0, 144 | "62": 233, 145 | "63": 17, 146 | "64": 192, 147 | "65": 255, 148 | "66": 195, 149 | "67": 232, 150 | "68": 233, 151 | "69": 17, 152 | "70": 193, 153 | "71": 232, 154 | "72": 232, 155 | "73": 232, 156 | "74": 234, 157 | "75": 236, 158 | "76": 194, 159 | "77": 235, 160 | "78": 235, 161 | "79": 235, 162 | "80": 235, 163 | "81": 237, 164 | "82": 0, 165 | "83": 237, 166 | "84": 237, 167 | "85": 237, 168 | "86": 237, 169 | "87": 237, 170 | "88": 250, 171 | "89": 250, 172 | "90": 250, 173 | "91": 189, 174 | "92": 254, 175 | "93": 0, 176 | "94": 222, 177 | "95": 255, 178 | "96": 255, 179 | "97": 232, 180 | "98": 233, 181 | "99": 17, 182 | "100": 228, 183 | "101": 15, 184 | "102": 232, 185 | "103": 233, 186 | "104": 17, 187 | "105": 18, 188 | "106": 229, 189 | "107": 232, 190 | "108": 234, 191 | "109": 234, 192 | "110": 236, 193 | "111": 54, 194 | "112": 230, 195 | "113": 235, 196 | "114": 22, 197 | "115": 237, 198 | "116": 238, 199 | "117": 238, 200 | "118": 0, 201 | "119": 237, 202 | "120": 22, 203 | "121": 23, 204 | "122": 23, 205 | "123": 23, 206 | "124": 252, 207 | "125": 252, 208 | "126": 189, 209 | "127": 189, 210 | "128": 254, 211 | "129": 0, 212 | "130": 223, 213 | "131": 232, 214 | "132": 232, 215 | "133": 232, 216 | "134": 233, 217 | "135": 17, 218 | "136": 229, 219 | "137": 232, 220 | "138": 233, 221 | "139": 234, 222 | "140": 53, 223 | "141": 18, 224 | "142": 229, 225 | "143": 232, 226 | "144": 234, 227 | "145": 236, 228 | "146": 17, 229 | "147": 19, 230 | "148": 230, 231 | "149": 235, 232 | "150": 238, 233 | "151": 22, 234 | "152": 23, 235 | "153": 24, 236 | "154": 0, 237 | "155": 237, 238 | "156": 22, 239 | "157": 2, 240 | "158": 29, 241 | "159": 6, 242 | "160": 254, 243 | "161": 254, 244 | "162": 254, 245 | "163": 254, 246 | "164": 254, 247 | "165": 0, 248 | "166": 255, 249 | "167": 233, 250 | "168": 233, 251 | "169": 234, 252 | "170": 234, 253 | "171": 235, 254 | "172": 230, 255 | "173": 234, 256 | "174": 52, 257 | "175": 235, 258 | "176": 53, 259 | "177": 53, 260 | "178": 230, 261 | "179": 235, 262 | "180": 236, 263 | "181": 52, 264 | "182": 53, 265 | "183": 55, 266 | "184": 230, 267 | "185": 235, 268 | "186": 238, 269 | "187": 58, 270 | "188": 240, 271 | "189": 20, 272 | "190": 0, 273 | "191": 238, 274 | "192": 58, 275 | "193": 64, 276 | "194": 35, 277 | "195": 66, 278 | "196": 0, 279 | "197": 0, 280 | "198": 0, 281 | "199": 0, 282 | "200": 0, 283 | "201": 0, 284 | "202": 0, 285 | "203": 235, 286 | "204": 235, 287 | "205": 235, 288 | "206": 235, 289 | "207": 53, 290 | "208": 0, 291 | "209": 236, 292 | "210": 52, 293 | "211": 237, 294 | "212": 53, 295 | "213": 53, 296 | "214": 0, 297 | "215": 236, 298 | "216": 238, 299 | "217": 1, 300 | "218": 89, 301 | "219": 5, 302 | "220": 0, 303 | "221": 237, 304 | "222": 58, 305 | "223": 95, 306 | "224": 131, 307 | "225": 126, 308 | "226": 0, 309 | "227": 238, 310 | "228": 58, 311 | "229": 3, 312 | "230": 143, 313 | "231": 242, 314 | "232": 250, 315 | "233": 250, 316 | "234": 250, 317 | "235": 250, 318 | "236": 250, 319 | "237": 250, 320 | "238": 251, 321 | "239": 252, 322 | "240": 188, 323 | "241": 254, 324 | "242": 254, 325 | "243": 255, 326 | "244": 0, 327 | "245": 232, 328 | "246": 233, 329 | "247": 234, 330 | "248": 235, 331 | "249": 236, 332 | "250": 237, 333 | "251": 238, 334 | "252": 239, 335 | "253": 240, 336 | "254": 242, 337 | "255": 243 338 | }, 339 | "ViModeCommandFg": 0, 340 | "ViModeCommandBg": 250, 341 | "ViModeInsertFg": 22, 342 | "ViModeInsertBg": 70 343 | } 344 | -------------------------------------------------------------------------------- /segment-cwd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sort" 6 | "strings" 7 | 8 | pwl "github.com/justjanne/powerline-go/powerline" 9 | ) 10 | 11 | const ellipsis = "\u2026" 12 | 13 | type pathSegment struct { 14 | path string 15 | home bool 16 | root bool 17 | ellipsis bool 18 | alias bool 19 | } 20 | 21 | type byRevLength []string 22 | 23 | func (s byRevLength) Len() int { 24 | return len(s) 25 | } 26 | func (s byRevLength) Swap(i, j int) { 27 | s[i], s[j] = s[j], s[i] 28 | } 29 | func (s byRevLength) Less(i, j int) bool { 30 | return len(s[i]) > len(s[j]) 31 | } 32 | 33 | func maybeAliasPathSegments(p *powerline, pathSegments []pathSegment) []pathSegment { 34 | pathSeparator := string(os.PathSeparator) 35 | 36 | if p.cfg.PathAliases == nil || len(p.cfg.PathAliases) == 0 { 37 | return pathSegments 38 | } 39 | 40 | keys := make([]string, len(p.cfg.PathAliases)) 41 | for k := range p.cfg.PathAliases { 42 | keys = append(keys, k) 43 | } 44 | sort.Sort(byRevLength(keys)) 45 | 46 | Aliases: 47 | for _, k := range keys { 48 | // This turns a string like "foo/bar/baz" into an array of strings. 49 | path := strings.Split(strings.Trim(k, pathSeparator), pathSeparator) 50 | 51 | // If the path has 3 elements, we know we should look at pathSegments 52 | // in 3-element chunks. 53 | size := len(path) 54 | // If there aren't that many segments in our path we can skip to the 55 | // next alias. 56 | if size > len(pathSegments) { 57 | continue Aliases 58 | } 59 | 60 | alias := p.cfg.PathAliases[k] 61 | 62 | Segments: 63 | // We want to see if that array of strings exists in pathSegments. 64 | for i := range pathSegments { 65 | // This is the upper index that we would look at. So if i is 0, 66 | // then we'd look at pathSegments[0,1,2], then [1,2,3], etc.. If i 67 | // is 2, we'd look at pathSegments[2,3,4] and so on. 68 | max := (i + size) - 1 69 | 70 | // But if the upper index is out of bounds we can short-circuit 71 | // and move on to the next alias. 72 | if max > (len(pathSegments)-i)-1 { 73 | continue Aliases 74 | } 75 | 76 | // Then we loop over the indices in path and compare the 77 | // elements. If any element doesn't match we can move on to the 78 | // next index in pathSegments. 79 | for j := range path { 80 | if path[j] != pathSegments[i+j].path { 81 | continue Segments 82 | } 83 | } 84 | 85 | // They all matched! That means we can replace this slice with our 86 | // alias and skip to the next alias. 87 | pathSegments = append( 88 | pathSegments[:i], 89 | append( 90 | []pathSegment{{ 91 | path: alias, 92 | alias: true, 93 | }}, 94 | pathSegments[max+1:]..., 95 | )..., 96 | ) 97 | continue Aliases 98 | } 99 | } 100 | 101 | return pathSegments 102 | } 103 | 104 | func cwdToPathSegments(p *powerline, cwd string) []pathSegment { 105 | pathSeparator := string(os.PathSeparator) 106 | pathSegments := make([]pathSegment, 0) 107 | 108 | if cwd == p.userInfo.HomeDir { 109 | pathSegments = append(pathSegments, pathSegment{ 110 | path: "~", 111 | home: true, 112 | }) 113 | cwd = "" 114 | } else if strings.HasPrefix(cwd, p.userInfo.HomeDir+pathSeparator) { 115 | pathSegments = append(pathSegments, pathSegment{ 116 | path: "~", 117 | home: true, 118 | }) 119 | cwd = cwd[len(p.userInfo.HomeDir):] 120 | } else if cwd == pathSeparator { 121 | pathSegments = append(pathSegments, pathSegment{ 122 | path: pathSeparator, 123 | root: true, 124 | }) 125 | } 126 | 127 | cwd = strings.Trim(cwd, pathSeparator) 128 | names := strings.Split(cwd, pathSeparator) 129 | if names[0] == "" { 130 | names = names[1:] 131 | } 132 | 133 | for _, name := range names { 134 | pathSegments = append(pathSegments, pathSegment{ 135 | path: name, 136 | }) 137 | } 138 | 139 | return maybeAliasPathSegments(p, pathSegments) 140 | } 141 | 142 | func maybeShortenName(p *powerline, pathSegment string) string { 143 | if p.cfg.CwdMaxDirSize > 0 && len(pathSegment) > p.cfg.CwdMaxDirSize { 144 | return pathSegment[:p.cfg.CwdMaxDirSize] 145 | } 146 | return pathSegment 147 | } 148 | 149 | func escapeVariables(p *powerline, pathSegment string) string { 150 | pathSegment = strings.Replace(pathSegment, `\`, p.shell.EscapedBackslash, -1) 151 | pathSegment = strings.Replace(pathSegment, "`", p.shell.EscapedBacktick, -1) 152 | pathSegment = strings.Replace(pathSegment, `$`, p.shell.EscapedDollar, -1) 153 | return pathSegment 154 | } 155 | 156 | func getColor(p *powerline, pathSegment pathSegment, isLastDir bool) (uint8, uint8, bool) { 157 | if pathSegment.home && p.theme.HomeSpecialDisplay { 158 | return p.theme.HomeFg, p.theme.HomeBg, true 159 | } else if pathSegment.alias { 160 | return p.theme.AliasFg, p.theme.AliasBg, true 161 | } else if isLastDir { 162 | return p.theme.CwdFg, p.theme.PathBg, false 163 | } 164 | return p.theme.PathFg, p.theme.PathBg, false 165 | } 166 | 167 | func segmentCwd(p *powerline) (segments []pwl.Segment) { 168 | cwd := p.cwd 169 | 170 | switch p.cfg.CwdMode { 171 | case "plain": 172 | if strings.HasPrefix(cwd, p.userInfo.HomeDir) { 173 | cwd = "~" + cwd[len(p.userInfo.HomeDir):] 174 | } 175 | 176 | segments = append(segments, pwl.Segment{ 177 | Name: "cwd", 178 | Content: escapeVariables(p, cwd), 179 | Foreground: p.theme.CwdFg, 180 | Background: p.theme.PathBg, 181 | }) 182 | default: 183 | pathSegments := cwdToPathSegments(p, cwd) 184 | 185 | if p.cfg.CwdMode == "dironly" { 186 | pathSegments = pathSegments[len(pathSegments)-1:] 187 | } else { 188 | maxDepth := p.cfg.CwdMaxDepth 189 | if maxDepth <= 0 { 190 | warn("Ignoring -cwd-max-depth argument since it's smaller than or equal to 0") 191 | } else if len(pathSegments) > maxDepth { 192 | var nBefore int 193 | if maxDepth > 2 { 194 | nBefore = 2 195 | } else { 196 | nBefore = maxDepth - 1 197 | } 198 | firstPart := pathSegments[:nBefore] 199 | secondPart := pathSegments[len(pathSegments)+nBefore-maxDepth:] 200 | 201 | pathSegments = make([]pathSegment, 0) 202 | pathSegments = append(pathSegments, firstPart...) 203 | pathSegments = append(pathSegments, pathSegment{ 204 | path: ellipsis, 205 | ellipsis: true, 206 | }) 207 | pathSegments = append(pathSegments, secondPart...) 208 | } 209 | 210 | if p.cfg.CwdMode == "semifancy" && len(pathSegments) > 1 { 211 | var path string 212 | for idx, pathSegment := range pathSegments { 213 | if pathSegment.home || pathSegment.alias { 214 | continue 215 | } 216 | path += pathSegment.path 217 | if idx != len(pathSegments)-1 { 218 | path += string(os.PathSeparator) 219 | } 220 | } 221 | first := pathSegments[0] 222 | pathSegments = make([]pathSegment, 0) 223 | if first.home || first.alias { 224 | pathSegments = append(pathSegments, first) 225 | } 226 | pathSegments = append(pathSegments, pathSegment{ 227 | path: path, 228 | }) 229 | } 230 | } 231 | 232 | for idx, pathSegment := range pathSegments { 233 | isLastDir := idx == len(pathSegments)-1 234 | foreground, background, special := getColor(p, pathSegment, isLastDir) 235 | 236 | segment := pwl.Segment{ 237 | Content: escapeVariables(p, maybeShortenName(p, pathSegment.path)), 238 | Foreground: foreground, 239 | Background: background, 240 | } 241 | 242 | if !special { 243 | if p.align == alignRight && p.supportsRightModules() && idx != 0 { 244 | segment.Separator = p.symbols.SeparatorReverseThin 245 | segment.SeparatorForeground = p.theme.SeparatorFg 246 | } else if (p.align == alignLeft || !p.supportsRightModules()) && !isLastDir { 247 | segment.Separator = p.symbols.SeparatorThin 248 | segment.SeparatorForeground = p.theme.SeparatorFg 249 | } 250 | } 251 | 252 | segment.Name = "cwd-path" 253 | if isLastDir { 254 | segment.Name = "cwd" 255 | } 256 | 257 | segments = append(segments, segment) 258 | } 259 | } 260 | return segments 261 | } 262 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | 11 | pwl "github.com/justjanne/powerline-go/powerline" 12 | ) 13 | 14 | type alignment int 15 | 16 | const ( 17 | alignLeft alignment = iota 18 | alignRight 19 | ) 20 | 21 | const ( 22 | // MinUnsignedInteger minimum unsigned integer 23 | MinUnsignedInteger uint = 0 24 | // MaxUnsignedInteger maximum unsigned integer 25 | MaxUnsignedInteger = ^MinUnsignedInteger 26 | // MaxInteger maximum integer 27 | MaxInteger = int(MaxUnsignedInteger >> 1) 28 | /* MinInteger minimum integer 29 | MinInteger = ^MaxInteger 30 | */ 31 | ) 32 | 33 | func warn(msg string) { 34 | if *args.IgnoreWarnings { 35 | return 36 | } 37 | 38 | print("[powerline-go]", msg) 39 | } 40 | 41 | func pathExists(path string) bool { 42 | if _, err := os.Stat(path); os.IsNotExist(err) { 43 | return false 44 | } 45 | return true 46 | } 47 | 48 | func getValidCwd() string { 49 | cwd, err := os.Getwd() 50 | if err != nil { 51 | var exists bool 52 | cwd, exists = os.LookupEnv("PWD") 53 | if !exists { 54 | warn("Your current directory is invalid.") 55 | print("> ") 56 | os.Exit(1) 57 | } 58 | } 59 | 60 | parts := strings.Split(cwd, string(os.PathSeparator)) 61 | up := cwd 62 | 63 | for len(parts) > 0 && !pathExists(up) { 64 | parts = parts[:len(parts)-1] 65 | up = strings.Join(parts, string(os.PathSeparator)) 66 | } 67 | if cwd != up { 68 | warn("Your current directory is invalid. Lowest valid directory: " + up) 69 | } 70 | return cwd 71 | } 72 | 73 | var modules = map[string]func(*powerline) []pwl.Segment{ 74 | "aws": segmentAWS, 75 | "bzr": segmentBzr, 76 | "cwd": segmentCwd, 77 | "direnv": segmentDirenv, 78 | "docker": segmentDocker, 79 | "docker-context": segmentDockerContext, 80 | "dotenv": segmentDotEnv, 81 | "duration": segmentDuration, 82 | "exit": segmentExitCode, 83 | "fossil": segmentFossil, 84 | "gcp": segmentGCP, 85 | "git": segmentGit, 86 | "gitlite": segmentGitLite, 87 | "goenv": segmentGoenv, 88 | "hg": segmentHg, 89 | "svn": segmentSubversion, 90 | "host": segmentHost, 91 | "jobs": segmentJobs, 92 | "kube": segmentKube, 93 | "load": segmentLoad, 94 | "newline": segmentNewline, 95 | "perlbrew": segmentPerlbrew, 96 | "plenv": segmentPlEnv, 97 | "perms": segmentPerms, 98 | "rbenv": segmentRbenv, 99 | "root": segmentRoot, 100 | "rvm": segmentRvm, 101 | "shell-var": segmentShellVar, 102 | "shenv": segmentShEnv, 103 | "ssh": segmentSSH, 104 | "termtitle": segmentTermTitle, 105 | "terraform-workspace": segmentTerraformWorkspace, 106 | "time": segmentTime, 107 | "node": segmentNode, 108 | "user": segmentUser, 109 | "venv": segmentVirtualEnv, 110 | "vgo": segmentVirtualGo, 111 | "vi-mode": segmentViMode, 112 | "wsl": segmentWSL, 113 | "nix-shell": segmentNixShell, 114 | } 115 | 116 | func comments(lines ...string) string { 117 | return " " + strings.Join(lines, "\n"+" ") 118 | } 119 | 120 | func commentsWithDefaults(lines ...string) string { 121 | return comments(lines...) + "\n" 122 | } 123 | 124 | func main() { 125 | flag.Parse() 126 | 127 | cfg := defaults 128 | err := cfg.Load() 129 | if err != nil { 130 | println("Error loading config") 131 | println(err.Error()) 132 | } 133 | 134 | flag.Visit(func(f *flag.Flag) { 135 | switch f.Name { 136 | case "cwd-mode": 137 | cfg.CwdMode = *args.CwdMode 138 | case "cwd-max-depth": 139 | cfg.CwdMaxDepth = *args.CwdMaxDepth 140 | case "cwd-max-dir-size": 141 | cfg.CwdMaxDirSize = *args.CwdMaxDirSize 142 | case "colorize-hostname": 143 | cfg.ColorizeHostname = *args.ColorizeHostname 144 | case "fqdn-hostname": 145 | cfg.FqdnHostname = *args.FqdnHostname 146 | case "hostname-only-if-ssh": 147 | cfg.HostnameOnlyIfSSH = *args.HostnameOnlyIfSSH 148 | case "alternate-ssh-icon": 149 | cfg.SshAlternateIcon = *args.SshAlternateIcon 150 | case "east-asian-width": 151 | cfg.EastAsianWidth = *args.EastAsianWidth 152 | case "newline": 153 | cfg.PromptOnNewLine = *args.PromptOnNewLine 154 | case "static-prompt-indicator": 155 | cfg.StaticPromptIndicator = *args.StaticPromptIndicator 156 | case "venv-name-size-limit": 157 | cfg.VenvNameSizeLimit = *args.VenvNameSizeLimit 158 | case "jobs": 159 | cfg.Jobs = *args.Jobs 160 | case "git-assume-unchanged-size": 161 | cfg.GitAssumeUnchangedSize = *args.GitAssumeUnchangedSize 162 | case "git-disable-stats": 163 | cfg.GitDisableStats = strings.Split(*args.GitDisableStats, ",") 164 | case "git-mode": 165 | cfg.GitMode = *args.GitMode 166 | case "mode": 167 | cfg.Mode = *args.Mode 168 | case "theme": 169 | cfg.Theme = *args.Theme 170 | case "shell": 171 | cfg.Shell = *args.Shell 172 | case "modules": 173 | cfg.Modules = strings.Split(*args.Modules, ",") 174 | case "modules-right": 175 | cfg.ModulesRight = strings.Split(*args.ModulesRight, ",") 176 | case "priority": 177 | cfg.Priority = strings.Split(*args.Priority, ",") 178 | case "max-width": 179 | cfg.MaxWidthPercentage = *args.MaxWidthPercentage 180 | case "truncate-segment-width": 181 | cfg.TruncateSegmentWidth = *args.TruncateSegmentWidth 182 | case "error": 183 | cfg.PrevError = *args.PrevError 184 | case "numeric-exit-codes": 185 | cfg.NumericExitCodes = *args.NumericExitCodes 186 | case "ignore-repos": 187 | cfg.IgnoreRepos = strings.Split(*args.IgnoreRepos, ",") 188 | case "shorten-gke-names": 189 | cfg.ShortenGKENames = *args.ShortenGKENames 190 | case "shorten-eks-names": 191 | cfg.ShortenEKSNames = *args.ShortenEKSNames 192 | case "shorten-openshift-names": 193 | cfg.ShortenOpenshiftNames = *args.ShortenOpenshiftNames 194 | case "shell-var": 195 | cfg.ShellVar = *args.ShellVar 196 | case "shell-var-no-warn-empty": 197 | cfg.ShellVarNoWarnEmpty = *args.ShellVarNoWarnEmpty 198 | case "trim-ad-domain": 199 | cfg.TrimADDomain = *args.TrimADDomain 200 | case "path-aliases": 201 | for _, pair := range strings.Split(*args.PathAliases, ",") { 202 | kv := strings.SplitN(pair, "=", 2) 203 | cfg.PathAliases[kv[0]] = kv[1] 204 | } 205 | case "duration": 206 | cfg.Duration = *args.Duration 207 | case "duration-min": 208 | cfg.DurationMin = *args.DurationMin 209 | case "duration-low-precision": 210 | cfg.DurationLowPrecision = *args.DurationLowPrecision 211 | case "eval": 212 | cfg.Eval = *args.Eval 213 | case "condensed": 214 | cfg.Condensed = *args.Condensed 215 | case "ignore-warnings": 216 | cfg.IgnoreWarnings = *args.IgnoreWarnings 217 | case "time": 218 | cfg.Time = *args.Time 219 | case "vi-mode": 220 | cfg.ViMode = *args.ViMode 221 | } 222 | }) 223 | 224 | if strings.HasSuffix(cfg.Theme, ".json") { 225 | file, err := ioutil.ReadFile(cfg.Theme) 226 | if err == nil { 227 | theme := cfg.Themes[defaults.Theme] 228 | err = json.Unmarshal(file, &theme) 229 | if err == nil { 230 | cfg.Themes[cfg.Theme] = theme 231 | } else { 232 | println("Error reading theme") 233 | println(err.Error()) 234 | } 235 | } 236 | } 237 | 238 | if strings.HasSuffix(cfg.Mode, ".json") { 239 | file, err := ioutil.ReadFile(cfg.Mode) 240 | if err == nil { 241 | symbols := cfg.Modes[defaults.Mode] 242 | err = json.Unmarshal(file, &symbols) 243 | if err == nil { 244 | cfg.Modes[cfg.Mode] = symbols 245 | } else { 246 | println("Error reading mode") 247 | println(err.Error()) 248 | } 249 | } 250 | } 251 | 252 | p := newPowerline(cfg, getValidCwd(), alignLeft) 253 | if p.supportsRightModules() && p.hasRightModules() && !cfg.Eval { 254 | panic("Flag '-modules-right' requires '-eval' mode.") 255 | } 256 | 257 | fmt.Print(p.draw()) 258 | } 259 | -------------------------------------------------------------------------------- /segment-git.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | pwl "github.com/justjanne/powerline-go/powerline" 13 | ) 14 | 15 | type repoStats struct { 16 | ahead int 17 | behind int 18 | untracked int 19 | notStaged int 20 | staged int 21 | conflicted int 22 | stashed int 23 | } 24 | 25 | func (r repoStats) dirty() bool { 26 | return r.untracked+r.notStaged+r.staged+r.conflicted > 0 27 | } 28 | 29 | func (r repoStats) any() bool { 30 | return r.ahead+r.behind+r.untracked+r.notStaged+r.staged+r.conflicted+r.stashed > 0 31 | } 32 | 33 | func addRepoStatsSegment(nChanges int, symbol string, foreground uint8, background uint8) []pwl.Segment { 34 | if nChanges > 0 { 35 | return []pwl.Segment{{ 36 | Name: "git-status", 37 | Content: fmt.Sprintf("%d%s", nChanges, symbol), 38 | Foreground: foreground, 39 | Background: background, 40 | }} 41 | } 42 | return []pwl.Segment{} 43 | } 44 | 45 | func (r repoStats) GitSegments(p *powerline) (segments []pwl.Segment) { 46 | segments = append(segments, addRepoStatsSegment(r.ahead, p.symbols.RepoAhead, p.theme.GitAheadFg, p.theme.GitAheadBg)...) 47 | segments = append(segments, addRepoStatsSegment(r.behind, p.symbols.RepoBehind, p.theme.GitBehindFg, p.theme.GitBehindBg)...) 48 | segments = append(segments, addRepoStatsSegment(r.staged, p.symbols.RepoStaged, p.theme.GitStagedFg, p.theme.GitStagedBg)...) 49 | segments = append(segments, addRepoStatsSegment(r.notStaged, p.symbols.RepoNotStaged, p.theme.GitNotStagedFg, p.theme.GitNotStagedBg)...) 50 | segments = append(segments, addRepoStatsSegment(r.untracked, p.symbols.RepoUntracked, p.theme.GitUntrackedFg, p.theme.GitUntrackedBg)...) 51 | segments = append(segments, addRepoStatsSegment(r.conflicted, p.symbols.RepoConflicted, p.theme.GitConflictedFg, p.theme.GitConflictedBg)...) 52 | segments = append(segments, addRepoStatsSegment(r.stashed, p.symbols.RepoStashed, p.theme.GitStashedFg, p.theme.GitStashedBg)...) 53 | return 54 | } 55 | 56 | func addRepoStatsSymbol(nChanges int, symbol string, GitMode string) string { 57 | if nChanges > 0 { 58 | if GitMode == "simple" { 59 | return symbol 60 | } else if GitMode == "compact" { 61 | return fmt.Sprintf(" %d%s", nChanges, symbol ) 62 | } else { 63 | return symbol 64 | } 65 | } 66 | return "" 67 | } 68 | 69 | func (r repoStats) GitSymbols(p *powerline) string { 70 | var info string 71 | info += addRepoStatsSymbol(r.ahead, p.symbols.RepoAhead, p.cfg.GitMode) 72 | info += addRepoStatsSymbol(r.behind, p.symbols.RepoBehind, p.cfg.GitMode) 73 | info += addRepoStatsSymbol(r.staged, p.symbols.RepoStaged, p.cfg.GitMode) 74 | info += addRepoStatsSymbol(r.notStaged, p.symbols.RepoNotStaged, p.cfg.GitMode) 75 | info += addRepoStatsSymbol(r.untracked, p.symbols.RepoUntracked, p.cfg.GitMode) 76 | info += addRepoStatsSymbol(r.conflicted, p.symbols.RepoConflicted, p.cfg.GitMode) 77 | info += addRepoStatsSymbol(r.stashed, p.symbols.RepoStashed, p.cfg.GitMode) 78 | return info 79 | } 80 | 81 | var branchRegex = regexp.MustCompile(`^## (?P\S+?)(\.{3}(?P\S+?)( \[(ahead (?P\d+)(, )?)?(behind (?P\d+))?])?)?$`) 82 | 83 | func groupDict(pattern *regexp.Regexp, haystack string) map[string]string { 84 | match := pattern.FindStringSubmatch(haystack) 85 | result := make(map[string]string) 86 | if len(match) > 0 { 87 | for i, name := range pattern.SubexpNames() { 88 | if i != 0 { 89 | result[name] = match[i] 90 | } 91 | } 92 | } 93 | return result 94 | } 95 | 96 | var gitProcessEnv = func() []string { 97 | homeEnv := homeEnvName() 98 | home, _ := os.LookupEnv(homeEnv) 99 | path, _ := os.LookupEnv("PATH") 100 | env := map[string]string{ 101 | "LANG": "C", 102 | homeEnv: home, 103 | "PATH": path, 104 | } 105 | result := make([]string, 0) 106 | for key, value := range env { 107 | result = append(result, fmt.Sprintf("%s=%s", key, value)) 108 | } 109 | return result 110 | }() 111 | 112 | func runGitCommand(cmd string, args ...string) (string, error) { 113 | command := exec.Command(cmd, args...) 114 | command.Env = gitProcessEnv 115 | out, err := command.Output() 116 | return string(out), err 117 | } 118 | 119 | func parseGitBranchInfo(status []string) map[string]string { 120 | return groupDict(branchRegex, status[0]) 121 | } 122 | 123 | func getGitDetachedBranch(p *powerline) string { 124 | out, err := runGitCommand("git", "--no-optional-locks", "rev-parse", "--short", "HEAD") 125 | if err != nil { 126 | out, err := runGitCommand("git", "--no-optional-locks", "symbolic-ref", "--short", "HEAD") 127 | if err != nil { 128 | return "Error" 129 | } 130 | return strings.SplitN(out, "\n", 2)[0] 131 | } 132 | detachedRef := strings.SplitN(out, "\n", 2) 133 | return fmt.Sprintf("%s %s", p.symbols.RepoDetached, detachedRef[0]) 134 | } 135 | 136 | func parseGitStats(status []string) repoStats { 137 | stats := repoStats{} 138 | if len(status) > 1 { 139 | for _, line := range status[1:] { 140 | if len(line) > 2 { 141 | code := line[:2] 142 | switch code { 143 | case "??": 144 | stats.untracked++ 145 | case "DD", "AU", "UD", "UA", "DU", "AA", "UU": 146 | stats.conflicted++ 147 | default: 148 | if code[0] != ' ' { 149 | stats.staged++ 150 | } 151 | 152 | if code[1] != ' ' { 153 | stats.notStaged++ 154 | } 155 | } 156 | } 157 | } 158 | } 159 | return stats 160 | } 161 | 162 | func repoRoot(path string) (string, error) { 163 | out, err := runGitCommand("git", "--no-optional-locks", "rev-parse", "--show-toplevel") 164 | if err != nil { 165 | return "", err 166 | } 167 | return strings.TrimSpace(out), nil 168 | } 169 | 170 | func indexSize(root string) (int64, error) { 171 | fileInfo, err := os.Stat(path.Join(root, ".git", "index")) 172 | if err != nil { 173 | return 0, err 174 | } 175 | 176 | return fileInfo.Size(), nil 177 | } 178 | 179 | func segmentGit(p *powerline) []pwl.Segment { 180 | repoRoot, err := repoRoot(p.cwd) 181 | if err != nil { 182 | return []pwl.Segment{} 183 | } 184 | 185 | if len(p.ignoreRepos) > 0 && p.ignoreRepos[repoRoot] { 186 | return []pwl.Segment{} 187 | } 188 | 189 | args := []string{ 190 | "--no-optional-locks", "status", "--porcelain", "-b", "--ignore-submodules", 191 | } 192 | 193 | if p.cfg.GitAssumeUnchangedSize > 0 { 194 | indexSize, _ := indexSize(p.cwd) 195 | if indexSize > (p.cfg.GitAssumeUnchangedSize * 1024) { 196 | args = append(args, "-uno") 197 | } 198 | } 199 | 200 | out, err := runGitCommand("git", args...) 201 | if err != nil { 202 | return []pwl.Segment{} 203 | } 204 | 205 | status := strings.Split(out, "\n") 206 | stats := parseGitStats(status) 207 | branchInfo := parseGitBranchInfo(status) 208 | var branch string 209 | 210 | if branchInfo["local"] != "" { 211 | ahead, _ := strconv.ParseInt(branchInfo["ahead"], 10, 32) 212 | stats.ahead = int(ahead) 213 | 214 | behind, _ := strconv.ParseInt(branchInfo["behind"], 10, 32) 215 | stats.behind = int(behind) 216 | 217 | branch = branchInfo["local"] 218 | } else { 219 | branch = getGitDetachedBranch(p) 220 | } 221 | 222 | if len(p.symbols.RepoBranch) > 0 { 223 | branch = fmt.Sprintf("%s %s", p.symbols.RepoBranch, branch) 224 | } 225 | 226 | var foreground, background uint8 227 | if stats.dirty() { 228 | foreground = p.theme.RepoDirtyFg 229 | background = p.theme.RepoDirtyBg 230 | } else { 231 | foreground = p.theme.RepoCleanFg 232 | background = p.theme.RepoCleanBg 233 | } 234 | 235 | segments := []pwl.Segment{{ 236 | Name: "git-branch", 237 | Content: branch, 238 | Foreground: foreground, 239 | Background: background, 240 | }} 241 | 242 | stashEnabled := true 243 | for _, stat := range p.cfg.GitDisableStats { 244 | // "ahead, behind, staged, notStaged, untracked, conflicted, stashed" 245 | switch stat { 246 | case "ahead": 247 | stats.ahead = 0 248 | case "behind": 249 | stats.behind = 0 250 | case "staged": 251 | stats.staged = 0 252 | case "notStaged": 253 | stats.notStaged = 0 254 | case "untracked": 255 | stats.untracked = 0 256 | case "conflicted": 257 | stats.conflicted = 0 258 | case "stashed": 259 | stats.stashed = 0 260 | stashEnabled = false 261 | } 262 | } 263 | 264 | if stashEnabled { 265 | out, err = runGitCommand("git", "--no-optional-locks", "rev-list", "-g", "refs/stash") 266 | if err == nil { 267 | stats.stashed = strings.Count(out, "\n") 268 | } 269 | } 270 | 271 | if p.cfg.GitMode == "simple" { 272 | if stats.any() { 273 | segments[0].Content += " " + stats.GitSymbols(p) 274 | } 275 | } else if p.cfg.GitMode == "compact" { 276 | if stats.any() { 277 | segments[0].Content += stats.GitSymbols(p) 278 | } 279 | } else { // fancy 280 | segments = append(segments, stats.GitSegments(p)...) 281 | } 282 | 283 | return segments 284 | } 285 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | ) 7 | 8 | type arguments struct { 9 | CwdMode *string 10 | CwdMaxDepth *int 11 | CwdMaxDirSize *int 12 | ColorizeHostname *bool 13 | FqdnHostname *bool 14 | HostnameOnlyIfSSH *bool 15 | SshAlternateIcon *bool 16 | EastAsianWidth *bool 17 | PromptOnNewLine *bool 18 | StaticPromptIndicator *bool 19 | VenvNameSizeLimit *int 20 | GitAssumeUnchangedSize *int64 21 | GitDisableStats *string 22 | GitMode *string 23 | Jobs *int 24 | Mode *string 25 | Theme *string 26 | Shell *string 27 | Modules *string 28 | ModulesRight *string 29 | Priority *string 30 | MaxWidthPercentage *int 31 | TruncateSegmentWidth *int 32 | PrevError *int 33 | NumericExitCodes *bool 34 | IgnoreRepos *string 35 | ShortenGKENames *bool 36 | ShortenEKSNames *bool 37 | ShortenOpenshiftNames *bool 38 | ShellVar *string 39 | ShellVarNoWarnEmpty *bool 40 | TrimADDomain *bool 41 | PathAliases *string 42 | Duration *string 43 | DurationMin *string 44 | DurationLowPrecision *bool 45 | Eval *bool 46 | Condensed *bool 47 | IgnoreWarnings *bool 48 | Time *string 49 | ViMode *string 50 | } 51 | 52 | var args = arguments{ 53 | CwdMode: flag.String( 54 | "cwd-mode", 55 | defaults.CwdMode, 56 | commentsWithDefaults("How to display the current directory", 57 | "(valid choices: fancy, semifancy, plain, dironly)")), 58 | CwdMaxDepth: flag.Int( 59 | "cwd-max-depth", 60 | defaults.CwdMaxDepth, 61 | commentsWithDefaults("Maximum number of directories to show in path")), 62 | CwdMaxDirSize: flag.Int( 63 | "cwd-max-dir-size", 64 | defaults.CwdMaxDirSize, 65 | commentsWithDefaults("Maximum number of letters displayed for each directory in the path")), 66 | ColorizeHostname: flag.Bool( 67 | "colorize-hostname", 68 | defaults.ColorizeHostname, 69 | comments("Colorize the hostname based on a hash of itself, or use the PLGO_HOSTNAMEFG and PLGO_HOSTNAMEBG env vars (both need to be set).")), 70 | FqdnHostname: flag.Bool( 71 | "fqdn-hostname", 72 | defaults.FqdnHostname, 73 | comments("Use the longer fully qualified domain name as the hostname")), 74 | HostnameOnlyIfSSH: flag.Bool( 75 | "hostname-only-if-ssh", 76 | defaults.HostnameOnlyIfSSH, 77 | comments("Show hostname only for SSH connections")), 78 | SshAlternateIcon: flag.Bool( 79 | "alternate-ssh-icon", 80 | defaults.SshAlternateIcon, 81 | comments("Show the older, original icon for SSH connections")), 82 | EastAsianWidth: flag.Bool( 83 | "east-asian-width", 84 | defaults.EastAsianWidth, 85 | comments("Use East Asian Ambiguous Widths")), 86 | PromptOnNewLine: flag.Bool( 87 | "newline", 88 | defaults.PromptOnNewLine, 89 | comments("Show the prompt on a new line")), 90 | StaticPromptIndicator: flag.Bool( 91 | "static-prompt-indicator", 92 | defaults.StaticPromptIndicator, 93 | comments("Always show the prompt indicator with the default color, never with the error color")), 94 | VenvNameSizeLimit: flag.Int( 95 | "venv-name-size-limit", 96 | defaults.VenvNameSizeLimit, 97 | comments("Show indicator instead of virtualenv name if name is longer than this limit (defaults to 0, which is unlimited)")), 98 | Jobs: flag.Int( 99 | "jobs", 100 | defaults.Jobs, 101 | comments("Number of jobs currently running")), 102 | GitAssumeUnchangedSize: flag.Int64( 103 | "git-assume-unchanged-size", 104 | defaults.GitAssumeUnchangedSize, 105 | comments("Disable checking for changed/edited files in git repositories where the index is larger than this size (in KB), improves performance")), 106 | GitDisableStats: flag.String( 107 | "git-disable-stats", 108 | strings.Join(defaults.GitDisableStats, ","), 109 | commentsWithDefaults("Comma-separated list to disable individual git statuses", 110 | "(valid choices: ahead, behind, staged, notStaged, untracked, conflicted, stashed)")), 111 | GitMode: flag.String( 112 | "git-mode", 113 | defaults.GitMode, 114 | commentsWithDefaults("How to display git status", 115 | "(valid choices: fancy, compact, simple)")), 116 | Mode: flag.String( 117 | "mode", 118 | defaults.Mode, 119 | commentsWithDefaults("The characters used to make separators between segments.", 120 | "(valid choices: patched, compatible, flat)")), 121 | Theme: flag.String( 122 | "theme", 123 | defaults.Theme, 124 | commentsWithDefaults("Set this to the theme you want to use", 125 | "(valid choices: default, low-contrast, gruvbox, solarized-dark16, solarized-light16)")), 126 | Shell: flag.String( 127 | "shell", 128 | defaults.Shell, 129 | commentsWithDefaults("Set this to your shell type", 130 | "(valid choices: autodetect, bare, bash, zsh)")), 131 | Modules: flag.String( 132 | "modules", 133 | strings.Join(defaults.Modules, ","), 134 | commentsWithDefaults("The list of modules to load, separated by ','", 135 | "(valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, vi-mode, wsl)", 136 | "Unrecognized modules will be invoked as 'powerline-go-MODULE' executable plugins and should output a (possibly empty) list of JSON objects that unmarshal to powerline-go's Segment structs.")), 137 | ModulesRight: flag.String( 138 | "modules-right", 139 | strings.Join(defaults.ModulesRight, ","), 140 | comments("The list of modules to load anchored to the right, for shells that support it, separated by ','", 141 | "(valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, wsl)", 142 | "Unrecognized modules will be invoked as 'powerline-go-MODULE' executable plugins and should output a (possibly empty) list of JSON objects that unmarshal to powerline-go's Segment structs.")), 143 | Priority: flag.String( 144 | "priority", 145 | strings.Join(defaults.Priority, ","), 146 | commentsWithDefaults("Segments sorted by priority, if not enough space exists, the least priorized segments are removed first. Separate with ','", 147 | "(valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, vi-mode, wsl)")), 148 | MaxWidthPercentage: flag.Int( 149 | "max-width", 150 | defaults.MaxWidthPercentage, 151 | comments("Maximum width of the shell that the prompt may use, in percent. Setting this to 0 disables the shrinking subsystem.")), 152 | TruncateSegmentWidth: flag.Int( 153 | "truncate-segment-width", 154 | defaults.TruncateSegmentWidth, 155 | commentsWithDefaults("Maximum width of a segment, segments longer than this will be shortened if space is limited. Setting this to 0 disables it.")), 156 | PrevError: flag.Int( 157 | "error", 158 | defaults.PrevError, 159 | comments("Exit code of previously executed command")), 160 | NumericExitCodes: flag.Bool( 161 | "numeric-exit-codes", 162 | defaults.NumericExitCodes, 163 | comments("Shows numeric exit codes for errors.")), 164 | IgnoreRepos: flag.String( 165 | "ignore-repos", 166 | strings.Join(defaults.IgnoreRepos, ","), 167 | comments("A list of git repos to ignore. Separate with ','.", 168 | "Repos are identified by their root directory.")), 169 | ShortenGKENames: flag.Bool( 170 | "shorten-gke-names", 171 | defaults.ShortenGKENames, 172 | comments("Shortens names for GKE Kube clusters.")), 173 | ShortenEKSNames: flag.Bool( 174 | "shorten-eks-names", 175 | defaults.ShortenEKSNames, 176 | comments("Shortens names for EKS Kube clusters.")), 177 | ShortenOpenshiftNames: flag.Bool( 178 | "shorten-openshift-names", 179 | defaults.ShortenOpenshiftNames, 180 | comments("Shortens names for Openshift Kube clusters.")), 181 | ShellVar: flag.String( 182 | "shell-var", 183 | defaults.ShellVar, 184 | comments("A shell variable to add to the segments.")), 185 | ShellVarNoWarnEmpty: flag.Bool( 186 | "shell-var-no-warn-empty", 187 | defaults.ShellVarNoWarnEmpty, 188 | comments("Disables warning for empty shell variable.")), 189 | TrimADDomain: flag.Bool( 190 | "trim-ad-domain", 191 | defaults.TrimADDomain, 192 | comments("Trim the Domainname from the AD username.")), 193 | PathAliases: flag.String( 194 | "path-aliases", 195 | "", 196 | comments("One or more aliases from a path to a short name. Separate with ','.", 197 | "An alias maps a path like foo/bar/baz to a short name like FBB.", 198 | "Specify these as key/value pairs like foo/bar/baz=FBB.", 199 | "Use '~' for your home dir. You may need to escape this character to avoid shell substitution.")), 200 | Duration: flag.String( 201 | "duration", 202 | defaults.Duration, 203 | comments("The elapsed clock-time of the previous command")), 204 | Time: flag.String( 205 | "time", 206 | defaults.Time, 207 | comments("The layout string how a reference time should be represented.", 208 | "The reference time is predefined and not user chosen.", 209 | "Consult the golang documentation for details: https://pkg.go.dev/time#example-Time.Format")), 210 | DurationMin: flag.String( 211 | "duration-min", 212 | defaults.DurationMin, 213 | comments("The minimal time a command has to take before the duration segment is shown")), 214 | DurationLowPrecision: flag.Bool( 215 | "duration-low-precision", 216 | defaults.DurationLowPrecision, 217 | comments("Use low precision timing for duration with milliseconds as maximum resolution")), 218 | Eval: flag.Bool( 219 | "eval", 220 | defaults.Eval, 221 | comments("Output prompt in 'eval' format.")), 222 | Condensed: flag.Bool( 223 | "condensed", 224 | defaults.Condensed, 225 | comments("Remove spacing between segments")), 226 | IgnoreWarnings: flag.Bool( 227 | "ignore-warnings", 228 | defaults.IgnoreWarnings, 229 | comments("Ignores all warnings regarding unset or broken variables")), 230 | ViMode: flag.String( 231 | "vi-mode", 232 | defaults.ViMode, 233 | comments("The current vi-mode (eg. KEYMAP for zsh) for vi-module module")), 234 | } 235 | -------------------------------------------------------------------------------- /powerline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "path" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | 13 | pwl "github.com/justjanne/powerline-go/powerline" 14 | "github.com/mattn/go-runewidth" 15 | "github.com/shirou/gopsutil/v3/process" 16 | "golang.org/x/term" 17 | "golang.org/x/text/width" 18 | ) 19 | 20 | // ShellInfo holds the shell information 21 | type ShellInfo struct { 22 | RootIndicator string 23 | ColorTemplate string 24 | EscapedDollar string 25 | EscapedBacktick string 26 | EscapedBackslash string 27 | EvalPromptPrefix string 28 | EvalPromptSuffix string 29 | EvalPromptRightPrefix string 30 | EvalPromptRightSuffix string 31 | } 32 | 33 | type powerline struct { 34 | cfg Config 35 | cwd string 36 | userInfo user.User 37 | userIsAdmin bool 38 | hostname string 39 | username string 40 | theme Theme 41 | shell ShellInfo 42 | reset string 43 | symbols SymbolTemplate 44 | priorities map[string]int 45 | ignoreRepos map[string]bool 46 | Segments [][]pwl.Segment 47 | curSegment int 48 | align alignment 49 | rightPowerline *powerline 50 | } 51 | 52 | type prioritizedSegments struct { 53 | i int 54 | segs []pwl.Segment 55 | } 56 | 57 | func newPowerline(cfg Config, cwd string, align alignment) *powerline { 58 | p := new(powerline) 59 | p.cfg = cfg 60 | p.cwd = cwd 61 | userInfo, err := user.Current() 62 | if userInfo != nil && err == nil { 63 | p.userInfo = *userInfo 64 | } 65 | p.hostname, _ = os.Hostname() 66 | 67 | hostnamePrefix := fmt.Sprintf("%s%c", p.hostname, os.PathSeparator) 68 | if strings.HasPrefix(p.userInfo.Username, hostnamePrefix) { 69 | p.username = p.userInfo.Username[len(hostnamePrefix):] 70 | } else { 71 | p.username = p.userInfo.Username 72 | } 73 | if cfg.TrimADDomain { 74 | usernameWithAd := strings.SplitN(p.username, `\`, 2) 75 | if len(usernameWithAd) > 1 { 76 | // remove the Domain name from username 77 | p.username = usernameWithAd[1] 78 | } 79 | } 80 | p.userIsAdmin = userIsAdmin() 81 | 82 | p.theme = cfg.Themes[cfg.Theme] 83 | if cfg.Shell == "autodetect" { 84 | var shellExe string 85 | proc, err := process.NewProcess(int32(os.Getppid())) 86 | if err == nil { 87 | shellExe, _ = proc.Exe() 88 | } 89 | if shellExe == "" { 90 | shellExe = os.Getenv("SHELL") 91 | } 92 | cfg.Shell = detectShell(shellExe) 93 | } 94 | p.shell = cfg.Shells[cfg.Shell] 95 | p.reset = fmt.Sprintf(p.shell.ColorTemplate, "[0m") 96 | p.symbols = cfg.Modes[cfg.Mode] 97 | p.priorities = make(map[string]int) 98 | for idx, priority := range cfg.Priority { 99 | p.priorities[priority] = len(cfg.Priority) - idx 100 | } 101 | p.align = align 102 | p.ignoreRepos = make(map[string]bool) 103 | for _, r := range cfg.IgnoreRepos { 104 | if r == "" { 105 | continue 106 | } 107 | p.ignoreRepos[r] = true 108 | } 109 | p.Segments = make([][]pwl.Segment, 1) 110 | var mods []string 111 | if p.align == alignLeft { 112 | mods = cfg.Modules 113 | if len(cfg.ModulesRight) > 0 { 114 | if p.supportsRightModules() { 115 | p.rightPowerline = newPowerline(cfg, cwd, alignRight) 116 | } else { 117 | mods = append(mods, cfg.ModulesRight...) 118 | } 119 | } 120 | } else { 121 | mods = cfg.ModulesRight 122 | } 123 | initSegments(p, mods) 124 | 125 | return p 126 | } 127 | 128 | func detectShell(shellExe string) string { 129 | var shell string 130 | shellExe = path.Base(shellExe) 131 | if strings.Contains(shellExe, "bash") { 132 | shell = "bash" 133 | } else if strings.Contains(shellExe, "zsh") { 134 | shell = "zsh" 135 | } else { 136 | shell = "bare" 137 | } 138 | return shell 139 | } 140 | 141 | func initSegments(p *powerline, mods []string) { 142 | orderedSegments := map[int][]pwl.Segment{} 143 | c := make(chan prioritizedSegments, len(mods)) 144 | wg := sync.WaitGroup{} 145 | for i, module := range mods { 146 | wg.Add(1) 147 | go func(w *sync.WaitGroup, i int, module string, c chan prioritizedSegments) { 148 | elem, ok := modules[module] 149 | if ok { 150 | c <- prioritizedSegments{ 151 | i: i, 152 | segs: elem(p), 153 | } 154 | } else { 155 | s, ok := segmentPlugin(p, module) 156 | if ok { 157 | c <- prioritizedSegments{ 158 | i: i, 159 | segs: s, 160 | } 161 | } else { 162 | println("Module not found: " + module) 163 | } 164 | } 165 | wg.Done() 166 | }(&wg, i, module, c) 167 | } 168 | wg.Wait() 169 | close(c) 170 | for s := range c { 171 | orderedSegments[s.i] = s.segs 172 | } 173 | for i := 0; i < len(mods); i++ { 174 | for _, seg := range orderedSegments[i] { 175 | p.appendSegment(seg.Name, seg) 176 | } 177 | } 178 | } 179 | 180 | func (p *powerline) color(prefix string, code uint8) string { 181 | if code == p.theme.Reset { 182 | return p.reset 183 | } 184 | return fmt.Sprintf(p.shell.ColorTemplate, fmt.Sprintf("[%s;5;%dm", prefix, code)) 185 | } 186 | 187 | func (p *powerline) fgColor(code uint8) string { 188 | if p.theme.BoldForeground { 189 | return p.color("1;38", code) 190 | } else { 191 | return p.color("38", code) 192 | } 193 | } 194 | 195 | func (p *powerline) bgColor(code uint8) string { 196 | return p.color("48", code) 197 | } 198 | 199 | func (p *powerline) appendSegment(origin string, segment pwl.Segment) { 200 | if segment.Foreground == segment.Background && segment.Background == 0 { 201 | segment.Background = p.theme.DefaultBg 202 | segment.Foreground = p.theme.DefaultFg 203 | } 204 | if segment.Separator == "" { 205 | if p.isRightPrompt() { 206 | segment.Separator = p.symbols.SeparatorReverse 207 | } else { 208 | segment.Separator = p.symbols.Separator 209 | } 210 | } 211 | if segment.SeparatorForeground == 0 { 212 | segment.SeparatorForeground = segment.Background 213 | } 214 | segment.Priority += p.priorities[origin] 215 | segment.Width = segment.ComputeWidth(p.cfg.Condensed) 216 | if segment.NewLine { 217 | p.newRow() 218 | } else { 219 | p.Segments[p.curSegment] = append(p.Segments[p.curSegment], segment) 220 | } 221 | } 222 | 223 | func (p *powerline) newRow() { 224 | if len(p.Segments[p.curSegment]) > 0 { 225 | p.Segments = append(p.Segments, make([]pwl.Segment, 0)) 226 | p.curSegment = p.curSegment + 1 227 | } 228 | } 229 | 230 | func termWidth() int { 231 | termWidth, _, err := term.GetSize(int(os.Stdin.Fd())) 232 | if err != nil { 233 | shellMaxLengthStr, found := os.LookupEnv("COLUMNS") 234 | if !found { 235 | return 0 236 | } 237 | 238 | shellMaxLength64, err := strconv.ParseInt(shellMaxLengthStr, 0, 64) 239 | if err != nil { 240 | return 0 241 | } 242 | 243 | termWidth = int(shellMaxLength64) 244 | } 245 | 246 | return termWidth 247 | } 248 | 249 | func (p *powerline) truncateRow(rowNum int) { 250 | 251 | shellMaxLength := termWidth() * p.cfg.MaxWidthPercentage / 100 252 | row := p.Segments[rowNum] 253 | rowLength := 0 254 | 255 | if shellMaxLength > 0 { 256 | for _, segment := range row { 257 | rowLength += segment.Width 258 | } 259 | 260 | if rowLength > shellMaxLength && p.cfg.TruncateSegmentWidth > 0 { 261 | minPriorityNotTruncated := MaxInteger 262 | minPriorityNotTruncatedSegmentID := -1 263 | for idx, segment := range row { 264 | if segment.Width > p.cfg.TruncateSegmentWidth && segment.Priority < minPriorityNotTruncated { 265 | minPriorityNotTruncated = segment.Priority 266 | minPriorityNotTruncatedSegmentID = idx 267 | } 268 | } 269 | for minPriorityNotTruncatedSegmentID != -1 && rowLength > shellMaxLength { 270 | segment := row[minPriorityNotTruncatedSegmentID] 271 | 272 | rowLength -= segment.Width 273 | 274 | segment.Content = runewidth.Truncate(segment.Content, p.cfg.TruncateSegmentWidth-runewidth.StringWidth(segment.Separator)-3, "…") 275 | segment.Width = segment.ComputeWidth(p.cfg.Condensed) 276 | 277 | row = append(append(row[:minPriorityNotTruncatedSegmentID], segment), row[minPriorityNotTruncatedSegmentID+1:]...) 278 | rowLength += segment.Width 279 | 280 | minPriorityNotTruncated = MaxInteger 281 | minPriorityNotTruncatedSegmentID = -1 282 | for idx, segment := range row { 283 | if segment.Width > p.cfg.TruncateSegmentWidth && segment.Priority < minPriorityNotTruncated { 284 | minPriorityNotTruncated = segment.Priority 285 | minPriorityNotTruncatedSegmentID = idx 286 | } 287 | } 288 | } 289 | } 290 | 291 | for rowLength > shellMaxLength { 292 | minPriority := MaxInteger 293 | minPrioritySegmentID := -1 294 | for idx, segment := range row { 295 | if segment.Priority < minPriority { 296 | minPriority = segment.Priority 297 | minPrioritySegmentID = idx 298 | } 299 | } 300 | if minPrioritySegmentID != -1 { 301 | segment := row[minPrioritySegmentID] 302 | row = append(row[:minPrioritySegmentID], row[minPrioritySegmentID+1:]...) 303 | rowLength -= segment.Width 304 | } 305 | } 306 | } 307 | p.Segments[rowNum] = row 308 | } 309 | 310 | func (p *powerline) numEastAsianRunes(segmentContent *string) int { 311 | if !p.cfg.EastAsianWidth { 312 | return 0 313 | } 314 | numEastAsianRunes := 0 315 | for _, r := range *segmentContent { 316 | switch width.LookupRune(r).Kind() { 317 | case width.EastAsianAmbiguous: 318 | numEastAsianRunes++ 319 | case width.Neutral: 320 | case width.EastAsianWide: 321 | case width.EastAsianNarrow: 322 | case width.EastAsianFullwidth: 323 | case width.EastAsianHalfwidth: 324 | } 325 | } 326 | return numEastAsianRunes 327 | } 328 | 329 | func (p *powerline) drawRow(rowNum int, buffer *bytes.Buffer) { 330 | row := p.Segments[rowNum] 331 | numEastAsianRunes := 0 332 | 333 | // Prepend padding 334 | if p.isRightPrompt() { 335 | buffer.WriteRune(' ') 336 | } 337 | for idx, segment := range row { 338 | if segment.HideSeparators { 339 | buffer.WriteString(segment.Content) 340 | continue 341 | } 342 | var separatorBackground string 343 | if p.isRightPrompt() { 344 | if idx == 0 { 345 | separatorBackground = p.reset 346 | } else { 347 | prevSegment := row[idx-1] 348 | separatorBackground = p.bgColor(prevSegment.Background) 349 | } 350 | buffer.WriteString(separatorBackground) 351 | buffer.WriteString(p.fgColor(segment.SeparatorForeground)) 352 | buffer.WriteString(segment.Separator) 353 | } else { 354 | if idx >= len(row)-1 { 355 | if !p.hasRightModules() || p.supportsRightModules() { 356 | separatorBackground = p.reset 357 | } else if p.hasRightModules() && rowNum >= len(p.Segments)-1 { 358 | nextSegment := p.rightPowerline.Segments[0][0] 359 | separatorBackground = p.bgColor(nextSegment.Background) 360 | } 361 | } else { 362 | nextSegment := row[idx+1] 363 | separatorBackground = p.bgColor(nextSegment.Background) 364 | } 365 | } 366 | buffer.WriteString(p.fgColor(segment.Foreground)) 367 | buffer.WriteString(p.bgColor(segment.Background)) 368 | if !p.cfg.Condensed { 369 | buffer.WriteRune(' ') 370 | } 371 | buffer.WriteString(segment.Content) 372 | numEastAsianRunes += p.numEastAsianRunes(&segment.Content) 373 | if !p.cfg.Condensed { 374 | buffer.WriteRune(' ') 375 | } 376 | if !p.isRightPrompt() { 377 | buffer.WriteString(separatorBackground) 378 | buffer.WriteString(p.fgColor(segment.SeparatorForeground)) 379 | buffer.WriteString(segment.Separator) 380 | } 381 | buffer.WriteString(p.reset) 382 | } 383 | 384 | // Append padding before cursor for left-aligned prompts 385 | if !p.isRightPrompt() || !p.hasRightModules() { 386 | buffer.WriteRune(' ') 387 | } 388 | 389 | // Don't append padding for right-aligned modules 390 | if !p.isRightPrompt() { 391 | for i := 0; i < numEastAsianRunes; i++ { 392 | buffer.WriteRune(' ') 393 | } 394 | } 395 | } 396 | 397 | func (p *powerline) draw() string { 398 | 399 | var buffer bytes.Buffer 400 | 401 | if p.cfg.Eval { 402 | if p.align == alignLeft { 403 | buffer.WriteString(p.shell.EvalPromptPrefix) 404 | } else if p.supportsRightModules() { 405 | buffer.WriteString(p.shell.EvalPromptRightPrefix) 406 | } 407 | } 408 | 409 | for rowNum := range p.Segments { 410 | p.truncateRow(rowNum) 411 | p.drawRow(rowNum, &buffer) 412 | if rowNum < len(p.Segments)-1 { 413 | buffer.WriteRune('\n') 414 | } 415 | } 416 | 417 | if p.cfg.PromptOnNewLine { 418 | buffer.WriteRune('\n') 419 | 420 | var foreground, background uint8 421 | if p.cfg.PrevError == 0 || p.cfg.StaticPromptIndicator { 422 | foreground = p.theme.CmdPassedFg 423 | background = p.theme.CmdPassedBg 424 | } else { 425 | foreground = p.theme.CmdFailedFg 426 | background = p.theme.CmdFailedBg 427 | } 428 | 429 | buffer.WriteString(p.fgColor(foreground)) 430 | buffer.WriteString(p.bgColor(background)) 431 | buffer.WriteString(p.shell.RootIndicator) 432 | buffer.WriteString(p.reset) 433 | buffer.WriteString(p.fgColor(background)) 434 | buffer.WriteString(p.symbols.Separator) 435 | buffer.WriteString(p.reset) 436 | buffer.WriteRune(' ') 437 | } 438 | 439 | if p.cfg.Eval { 440 | switch p.align { 441 | case alignLeft: 442 | buffer.WriteString(p.shell.EvalPromptSuffix) 443 | if p.supportsRightModules() { 444 | buffer.WriteRune('\n') 445 | if !p.hasRightModules() { 446 | buffer.WriteString(p.shell.EvalPromptRightPrefix + p.shell.EvalPromptRightSuffix) 447 | } 448 | } 449 | case alignRight: 450 | if p.supportsRightModules() { 451 | buffer.Truncate(buffer.Len() - 1) 452 | buffer.WriteString(p.shell.EvalPromptRightSuffix) 453 | } 454 | } 455 | if p.hasRightModules() { 456 | buffer.WriteString(p.rightPowerline.draw()) 457 | } 458 | } 459 | 460 | return buffer.String() 461 | } 462 | 463 | func (p *powerline) hasRightModules() bool { 464 | return p.rightPowerline != nil && len(p.rightPowerline.Segments[0]) > 0 465 | } 466 | 467 | func (p *powerline) supportsRightModules() bool { 468 | return p.shell.EvalPromptRightPrefix != "" || p.shell.EvalPromptRightSuffix != "" 469 | } 470 | 471 | func (p *powerline) isRightPrompt() bool { 472 | return p.align == alignRight && p.supportsRightModules() 473 | } 474 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Powerline style prompt for your shell 2 | 3 | A [Powerline](https://github.com/Lokaltog/vim-powerline) like prompt for Bash, 4 | ZSH and Fish. Based on [Powerline-Shell](https://github.com/banga/powerline-shell) by @banga. 5 | Ported to golang by @justjanne. 6 | 7 | ![Solarized+Powerline](https://raw.github.com/justjanne/powerline-go/main/preview.png) 8 | 9 | - Shows some important details about the git/hg branch (see below) 10 | - Changes color if the last command exited with a failure code 11 | - If you're too deep into a directory tree, shortens the displayed path with an ellipsis 12 | - Shows the current Python [virtualenv](http://www.virtualenv.org/) environment 13 | - Shows the current Ruby version using [rbenv](https://github.com/rbenv/rbenv) or [rvm](https://rvm.io/) 14 | - Shows if you are in a [nix](https://nixos.org/) shell 15 | - It's easy to customize and extend. See below for details. 16 | 17 | **Table of Contents** 18 | 19 | - [Version Control](#version-control) 20 | - [Installation](#installation) 21 | - [Precompiled Binaries](#precompiled-binaries) 22 | - [Other Platforms](#other-platforms) 23 | - [Bash](#bash) 24 | - [ZSH](#zsh) 25 | - [Fish](#fish) 26 | - [Nix](#nix) 27 | - [PowerShell](#powershell) 28 | - [Customization](#customization) 29 | - [License](#license) 30 | 31 | ## Version Control 32 | 33 | All of the version control systems supported by powerline shell give you a 34 | quick look into the state of your repo: 35 | 36 | - The current branch is displayed and changes background color when the 37 | branch is dirty. 38 | - When the local branch differs from the remote, the difference in number 39 | of commits is shown along with `⇡` or `⇣` indicating whether a git push 40 | or pull is pending 41 | 42 | In addition, git has a few extra symbols: 43 | 44 | - `✎` -- a file has been modified, but not staged for commit 45 | - `✔` -- a file is staged for commit 46 | - `✼` -- a file has conflicts 47 | - `+` -- untracked files are present 48 | - `⚑` -- stash is present 49 | 50 | Each of these will have a number next to it if more than one file matches. 51 | 52 | ## Installation 53 | 54 | Requires Go 1.15+ 55 | 56 | `powerline-go` uses ANSI color codes, these should nowadays work everywhere, 57 | but you may have to set your $TERM to `xterm-256color` for it to work. 58 | 59 | If you want to use the "patched" mode (which is the default, and provides 60 | improved UI), you'll need to install a powerline font, either as fallback, 61 | or by patching the font you use for your terminal: see 62 | [powerline-fonts](https://github.com/Lokaltog/powerline-fonts). 63 | Alternatively you can use "compatible" or "flat" mode. 64 | 65 | ### Precompiled Binaries 66 | 67 | I provide precompiled binaries for x64 Linux and macOS in the 68 | [releases tab](https://github.com/justjanne/powerline-go/releases) 69 | 70 | ### Other Platforms 71 | 72 | - Install (and update) the package with 73 | 74 | ```bash 75 | go install github.com/justjanne/powerline-go@latest 76 | ``` 77 | 78 | - By default it will be in `$GOPATH/bin`, if you want to change that, you can set 79 | your `$GOPATH` and/or `$GOBIN`, but will need to change the path in the 80 | following scripts, too. 81 | 82 | ### Bash 83 | 84 | Add the following to your `.bashrc`: 85 | 86 | ```bash 87 | function _update_ps1() { 88 | PS1="$($GOPATH/bin/powerline-go -error $? -jobs $(jobs -p | wc -l))" 89 | 90 | # Uncomment the following line to automatically clear errors after showing 91 | # them once. This not only clears the error for powerline-go, but also for 92 | # everything else you run in that shell. Don't enable this if you're not 93 | # sure this is what you want. 94 | 95 | #set "?" 96 | } 97 | 98 | if [ "$TERM" != "linux" ] && [ -f "$GOPATH/bin/powerline-go" ]; then 99 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" 100 | fi 101 | ``` 102 | 103 | Currently, right prompt support is not available when using bash. 104 | 105 | ### ZSH 106 | 107 | Add the following to your `.zshrc`: 108 | 109 | ```bash 110 | function powerline_precmd() { 111 | PS1="$($GOPATH/bin/powerline-go -error $? -jobs ${${(%):%j}:-0})" 112 | 113 | # Uncomment the following line to automatically clear errors after showing 114 | # them once. This not only clears the error for powerline-go, but also for 115 | # everything else you run in that shell. Don't enable this if you're not 116 | # sure this is what you want. 117 | 118 | #set "?" 119 | } 120 | 121 | function install_powerline_precmd() { 122 | for s in "${precmd_functions[@]}"; do 123 | if [ "$s" = "powerline_precmd" ]; then 124 | return 125 | fi 126 | done 127 | precmd_functions+=(powerline_precmd) 128 | } 129 | 130 | if [ "$TERM" != "linux" ] && [ -f "$GOPATH/bin/powerline-go" ]; then 131 | install_powerline_precmd 132 | fi 133 | ``` 134 | 135 | ### Fish 136 | 137 | Redefine `fish_prompt` in `~/.config/fish/config.fish`: 138 | 139 | ```bash 140 | function fish_prompt 141 | eval $GOPATH/bin/powerline-go -error $status -jobs (count (jobs -p)) 142 | end 143 | ``` 144 | ### Nix 145 | 146 | When using `nix-shell --pure`, `powerline-go` will not be accessible, and 147 | your prompt will disappear. 148 | 149 | To work around this you can add this snippet to your `.bashrc`, 150 | which should re-enable the prompt in most cases: 151 | 152 | ```bash 153 | # Workaround for nix-shell --pure 154 | if [ "$IN_NIX_SHELL" == "pure" ]; then 155 | if [ -x "$HOME/.nix-profile/bin/powerline-go" ]; then 156 | alias powerline-go="$HOME/.nix-profile/bin/powerline-go" 157 | elif [ -x "/run/current-system/sw/bin/powerline-go" ]; then 158 | alias powerline-go="/run/current-system/sw/bin/powerline-go" 159 | fi 160 | fi 161 | ``` 162 | 163 | ### Powershell 164 | 165 | Redefine `prompt` function on your profile: 166 | 167 | ```powershell 168 | # Load powerline-go prompt 169 | function global:prompt { 170 | $pwd = $ExecutionContext.SessionState.Path.CurrentLocation 171 | $startInfo = New-Object System.Diagnostics.ProcessStartInfo 172 | $startInfo.FileName = "powerline-go" 173 | $startInfo.Arguments = "-shell bare" 174 | $startInfo.Environment["TERM"] = "xterm-256color" 175 | $startInfo.CreateNoWindow = $true 176 | $startInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8 177 | $startInfo.RedirectStandardOutput = $true 178 | $startInfo.UseShellExecute = $false 179 | $startInfo.WorkingDirectory = $pwd 180 | $process = New-Object System.Diagnostics.Process 181 | $process.StartInfo = $startInfo 182 | $process.Start() | Out-Null 183 | $standardOut = $process.StandardOutput.ReadToEnd() 184 | $process.WaitForExit() 185 | $standardOut 186 | } 187 | ``` 188 | 189 | Use `ProcessStartInfo` is needed to allow fill the enviromnet variables required by powerline-go. 190 | 191 | ## Customization 192 | 193 | There are a few optional arguments which can be seen by running 194 | `powerline-go -help`. These can be used by changing the command you have set 195 | in your shell’s init file. 196 | 197 | ``` 198 | Usage of powerline-go: 199 | -alternate-ssh-icon 200 | Show the older, original icon for SSH connections 201 | -colorize-hostname 202 | Colorize the hostname based on a hash of itself, or use the PLGO_HOSTNAMEFG and PLGO_HOSTNAMEBG env vars (both need to be set). 203 | -fqdn-hostname 204 | Use the longer fully qualified domain name as the hostname 205 | -condensed 206 | Remove spacing between segments 207 | -cwd-max-depth int 208 | Maximum number of directories to show in path 209 | (default 5) 210 | -cwd-max-dir-size int 211 | Maximum number of letters displayed for each directory in the path 212 | (default -1) 213 | -cwd-mode string 214 | How to display the current directory 215 | (valid choices: fancy, semifancy, plain, dironly) 216 | (default "fancy") 217 | -duration string 218 | The elapsed clock-time of the previous command 219 | -duration-min string 220 | The minimal time a command has to take before the duration segment is shown (default "0") 221 | -east-asian-width 222 | Use East Asian Ambiguous Widths 223 | -error int 224 | Exit code of previously executed command 225 | -eval 226 | Output prompt in 'eval' format. 227 | -git-assume-unchanged-size int 228 | Disable checking for changed/edited files in git repositories where the index is larger than this size (in KB), improves performance (default 2048) 229 | -git-disable-stats string 230 | Comma-separated list to disable individual git statuses 231 | (valid choices: ahead, behind, staged, notStaged, untracked, conflicted, stashed) 232 | -git-mode string 233 | How to display git status 234 | (valid choices: fancy, compact, simple) 235 | (default "fancy") 236 | -hostname-only-if-ssh 237 | Show hostname only for SSH connections 238 | -ignore-repos string 239 | A list of git repos to ignore. Separate with ','. 240 | Repos are identified by their root directory. 241 | -ignore-warnings 242 | Ignores all warnings regarding unset or broken variables 243 | -jobs int 244 | Number of jobs currently running 245 | -max-width int 246 | Maximum width of the shell that the prompt may use, in percent. Setting this to 0 disables the shrinking subsystem. 247 | -mode string 248 | The characters used to make separators between segments. 249 | (valid choices: patched, compatible, flat) 250 | (default "patched") 251 | -modules string 252 | The list of modules to load, separated by ',' 253 | (valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, vi-mode, wsl) 254 | Unrecognized modules will be invoked as 'powerline-go-MODULE' executable plugins and should output a (possibly empty) list of JSON objects that unmarshal to powerline-go's Segment structs. 255 | (default "venv,user,host,ssh,cwd,perms,git,hg,jobs,exit,root") 256 | -modules-right string 257 | The list of modules to load anchored to the right, for shells that support it, separated by ',' 258 | (valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, wsl) 259 | Unrecognized modules will be invoked as 'powerline-go-MODULE' executable plugins and should output a (possibly empty) list of JSON objects that unmarshal to powerline-go's Segment structs. 260 | -newline 261 | Show the prompt on a new line 262 | -numeric-exit-codes 263 | Shows numeric exit codes for errors. 264 | -path-aliases string 265 | One or more aliases from a path to a short name. Separate with ','. 266 | An alias maps a path like foo/bar/baz to a short name like FBB. 267 | Specify these as key/value pairs like foo/bar/baz=FBB. 268 | Use '~' for your home dir. You may need to escape this character to avoid shell substitution. 269 | -priority string 270 | Segments sorted by priority, if not enough space exists, the least priorized segments are removed first. Separate with ',' 271 | (valid choices: aws, bzr, cwd, direnv, docker, docker-context, dotenv, duration, exit, fossil, gcp, git, gitlite, goenv, hg, host, jobs, kube, load, newline, nix-shell, node, perlbrew, perms, plenv, rbenv, root, rvm, shell-var, shenv, ssh, svn, termtitle, terraform-workspace, time, user, venv, vgo, vi-mode, wsl) 272 | (default "root,cwd,user,host,ssh,perms,git-branch,git-status,hg,jobs,exit,cwd-path") 273 | -shell string 274 | Set this to your shell type 275 | (valid choices: autodetect, bare, bash, zsh) 276 | (default "autodetect") 277 | -shell-var string 278 | A shell variable to add to the segments. 279 | -shell-var-no-warn-empty 280 | Disables warning for empty shell variable. 281 | -shorten-eks-names 282 | Shortens names for EKS Kube clusters. 283 | -shorten-gke-names 284 | Shortens names for GKE Kube clusters. 285 | -static-prompt-indicator 286 | Always show the prompt indicator with the default color, never with the error color 287 | -theme string 288 | Set this to the theme you want to use 289 | (valid choices: default, low-contrast, gruvbox, solarized-dark16, solarized-light16) 290 | (default "default") 291 | -trim-ad-domain 292 | Trim the Domainname from the AD username. 293 | -truncate-segment-width int 294 | Maximum width of a segment, segments longer than this will be shortened if space is limited. Setting this to 0 disables it. 295 | (default 16) 296 | -venv-name-size-limit int 297 | Show indicator instead of virtualenv name if name is longer than this limit (defaults to 0, which is unlimited) 298 | -vi-mode string 299 | The current vi-mode (eg. KEYMAP for zsh) for vi-module module 300 | ``` 301 | 302 | ### Eval 303 | 304 | If using `eval` and `-modules-right` is desired, the shell setup must be modified slightly, as shown below: 305 | 306 | ##### Bash 307 | 308 | Add the following to your `.bashrc`: 309 | 310 | ```bash 311 | function _update_ps1() { 312 | eval "$($GOPATH/bin/powerline-go -error $? -shell bash -eval -modules-right git)" 313 | } 314 | 315 | if [ "$TERM" != "linux" ] && [ -f "$GOPATH/bin/powerline-go" ]; then 316 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" 317 | fi 318 | ``` 319 | 320 | ##### ZSH 321 | 322 | Add the following to your `.zshrc`: 323 | 324 | ```bash 325 | function powerline_precmd() { 326 | eval "$($GOPATH/bin/powerline-go -error $? -shell zsh -eval -modules-right git)" 327 | } 328 | 329 | function install_powerline_precmd() { 330 | for s in "${precmd_functions[@]}"; do 331 | if [ "$s" = "powerline_precmd" ]; then 332 | return 333 | fi 334 | done 335 | precmd_functions+=(powerline_precmd) 336 | } 337 | 338 | if [ "$TERM" != "linux" ]; then 339 | install_powerline_precmd 340 | fi 341 | ``` 342 | 343 | ##### Fish 344 | 345 | Eval mode (and `modules-right` support) for Fish is not currently available. 346 | 347 | ### Path Aliases 348 | 349 | The point of the path aliases feature is to allow you to replace long paths 350 | with a shorter string that you can understand more quickly. This is useful if 351 | you're often in deep path hierarchies that end up consuming most of your 352 | terminal width, even when some portions are replaced by an ellipsis. 353 | 354 | For example, you might want to replace the string `$GOPATH/src/github.com` with 355 | `@GOPATH-GH`. When you're in a directory like 356 | `$GOPATH/src/github.com/justjanne/powerline-go`, you'll instead see `@GOPATH-GH > 357 | justjanne > powerline-go` in the shell prompt. 358 | 359 | Aliases are defined as comma-separated key value pairs, like this: 360 | 361 | ```bash 362 | powerline-go ... -path-aliases \$GOPATH/src/github.com=@GOPATH-GH,\~/work/projects/foo=@FOO,\~/work/projects/bar=@BAR 363 | ``` 364 | 365 | Note that you should use `~` instead of `/home/username` when specifying the 366 | path. Also make sure to escape the `~` character. Otherwise your shell will 367 | perform interpolation on it before `powerline-go` can see it! 368 | 369 | ### Duration 370 | 371 | The duration segment requires some assistance from the shell. The shell must have a hook that gets executed immediately before the command. 372 | 373 | #### Bash 374 | 375 | Bash 4.4 includes an easy way to get a start-time, using `$PS0`. However, not all operating systems come with a sufficiently recent version of Bash installed. This example only has seconds precision. Add or modify your `.bashrc` file to include the following: 376 | 377 | ```bash 378 | INTERACTIVE_BASHPID_TIMER="/tmp/${USER}.START.$$" 379 | 380 | PS0='$(echo $SECONDS > "$INTERACTIVE_BASHPID_TIMER")' 381 | 382 | function _update_ps1() { 383 | local __ERRCODE=$? 384 | 385 | local __DURATION=0 386 | if [ -e $INTERACTIVE_BASHPID_TIMER ]; then 387 | local __END=$SECONDS 388 | local __START=$(cat "$INTERACTIVE_BASHPID_TIMER") 389 | __DURATION="$(($__END - ${__START:-__END}))" 390 | rm -f "$INTERACTIVE_BASHPID_TIMER" 391 | fi 392 | 393 | PS1="$($GOPATH/bin/powerline-go -modules duration -duration $__DURATION -error $__ERRCODE -shell bash)" 394 | } 395 | 396 | if [ "$TERM" != "linux" ] && [ -f "$GOPATH/bin/powerline-go" ]; then 397 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" 398 | fi 399 | ``` 400 | 401 | #### Zsh 402 | 403 | Using `$EPOCHREALTIME` requires loading the 'datetime' module in your `.zshrc` file, for example: 404 | 405 | ```bash 406 | zmodload zsh/datetime 407 | 408 | function preexec() { 409 | __TIMER=$EPOCHREALTIME 410 | } 411 | 412 | function powerline_precmd() { 413 | local __ERRCODE=$? 414 | local __DURATION=0 415 | 416 | if [ -n $__TIMER ]; then 417 | local __ERT=$EPOCHREALTIME 418 | __DURATION="$(($__ERT - ${__TIMER:-__ERT}))" 419 | fi 420 | 421 | PS1="$(powerline-go -modules duration -duration $__DURATION -error $__ERRCODE -shell zsh)" 422 | unset __TIMER 423 | } 424 | ``` 425 | 426 | If the 'datetime' module is unavailable or unwanted, you may replace `$EPOCHREALTIME` with `$SECONDS`, at the loss of precision. 427 | 428 | #### Fish 429 | 430 | The fish prompt, in `~/.config/fish/config.fish`, will require a minimum of changes, as Fish automatically provides `$CMD_DURATION`, although with only milliseconds accuracy. 431 | 432 | ```bash 433 | function fish_prompt 434 | set duration (math -s6 "$CMD_DURATION / 1000") 435 | $GOPATH/bin/powerline-go -modules duration -duration $duration -error $status -shell bare 436 | end 437 | ``` 438 | 439 | ## License 440 | 441 | > 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. 442 | > 443 | > 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. 444 | > 445 | > You should have received a copy of the GNU General Public License along with this program. If not, see . 446 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ### TERMS AND CONDITIONS 77 | 78 | #### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | #### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | #### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | #### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | #### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | #### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | #### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | #### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | #### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | #### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | #### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | #### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | #### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | #### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | #### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | #### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | #### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | --------------------------------------------------------------------------------