refactor(config): extract to module

This commit is contained in:
Jan De Dobbeleer 2024-07-01 17:01:05 +02:00 committed by Jan De Dobbeleer
parent 1855a786dc
commit 3c6abf6163
35 changed files with 1355 additions and 1302 deletions

View file

@ -7,7 +7,7 @@ import (
"slices"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/spf13/cobra"
@ -38,12 +38,12 @@ Exports the ~/myconfig.omp.json config file to toml and prints the result to std
Run: func(_ *cobra.Command, _ []string) {
env := &platform.Shell{
CmdFlags: &platform.Flags{
Config: config,
Config: configFlag,
},
}
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
if len(output) == 0 && len(format) == 0 {
// usage error
@ -69,11 +69,11 @@ Exports the ~/myconfig.omp.json config file to toml and prints the result to std
switch format {
case "json", "jsonc":
format = engine.JSON
format = config.JSON
case "toml", "tml":
format = engine.TOML
format = config.TOML
case "yaml", "yml":
format = engine.YAML
format = config.YAML
default:
// data error
os.Exit(65)

View file

@ -3,6 +3,7 @@ package cli
import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
@ -49,7 +50,7 @@ Exports the config to an image file using customized output options.`,
Run: func(_ *cobra.Command, _ []string) {
env := &platform.Shell{
CmdFlags: &platform.Flags{
Config: config,
Config: configFlag,
Shell: shell.GENERIC,
TerminalWidth: 150,
},
@ -57,7 +58,7 @@ Exports the config to an image file using customized output options.`,
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
// set sane defaults for things we don't print
cfg.ConsoleTitleTemplate = ""

View file

@ -3,7 +3,7 @@ package cli
import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/spf13/cobra"
@ -41,13 +41,13 @@ A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
Run: func(_ *cobra.Command, _ []string) {
env := &platform.Shell{
CmdFlags: &platform.Flags{
Config: config,
Config: configFlag,
Migrate: true,
},
}
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
if write {
cfg.BackupAndMigrate()
return

View file

@ -3,7 +3,7 @@ package cli
import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/spf13/cobra"
@ -36,13 +36,13 @@ A backup of the current config can be found at ~/myconfig.omp.json.bak.`,
Run: func(_ *cobra.Command, _ []string) {
env := &platform.Shell{
CmdFlags: &platform.Flags{
Config: config,
Config: configFlag,
},
}
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
cfg.MigrateGlyphs = true
if len(format) == 0 {

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/jandedobbeleer/oh-my-posh/src/build"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
@ -24,7 +25,7 @@ var debugCmd = &cobra.Command{
env := &platform.Shell{
CmdFlags: &platform.Flags{
Config: config,
Config: configFlag,
Debug: true,
PWD: pwd,
Shell: shellName,
@ -35,7 +36,7 @@ var debugCmd = &cobra.Command{
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
// add variables to the environment
env.Var = cfg.Var

View file

@ -3,7 +3,7 @@ package cli
import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
@ -58,7 +58,7 @@ func runInit(shellName string) {
env := &platform.Shell{
CmdFlags: &platform.Flags{
Shell: shellName,
Config: config,
Config: configFlag,
Strict: strict,
Manual: manual,
},
@ -66,7 +66,7 @@ func runInit(shellName string) {
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
shell.Transient = cfg.TransientPrompt != nil
shell.ErrorLine = cfg.ErrorLine != nil || cfg.ValidLine != nil
@ -79,7 +79,8 @@ func runInit(shellName string) {
if !cfg.DisableCursorPositioning && (i == 0 && block.Newline) {
shell.CursorPositioning = true
}
if block.Type == engine.RPrompt {
if block.Type == config.RPrompt {
shell.RPrompt = true
}
}

View file

@ -50,7 +50,7 @@ var printCmd = &cobra.Command{
}
flags := &platform.Flags{
Config: config,
Config: configFlag,
PWD: pwd,
PSWD: pswd,
ErrorCode: status,

View file

@ -10,7 +10,7 @@ import (
)
var (
config string
configFlag string
displayVersion bool
)
@ -48,7 +48,7 @@ var (
)
func init() {
RootCmd.PersistentFlags().StringVarP(&config, "config", "c", "", "config file path")
RootCmd.PersistentFlags().StringVarP(&configFlag, "config", "c", "", "config file path")
RootCmd.Flags().BoolVarP(&initialize, "init", "i", false, "init (deprecated)")
RootCmd.Flags().BoolVar(&displayVersion, "version", false, "version")
RootCmd.Flags().StringVarP(&shellName, "shell", "s", "", "shell (deprecated)")

View file

@ -5,7 +5,7 @@ import (
"runtime"
"slices"
"github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
@ -48,7 +48,7 @@ var upgradeCmd = &cobra.Command{
return
}
cfg := engine.LoadConfig(env)
cfg := config.Load(env)
if _, hasNotice := upgrade.Notice(env, true); !hasNotice {
if !cfg.DisableNotice {

107
src/config/backup.go Normal file
View file

@ -0,0 +1,107 @@
package config
import (
"bytes"
"io"
"os"
"strings"
json "github.com/goccy/go-json"
yaml "github.com/goccy/go-yaml"
toml "github.com/pelletier/go-toml/v2"
)
func (cfg *Config) Backup() {
dst := cfg.origin + ".bak"
source, err := os.Open(cfg.origin)
if err != nil {
return
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return
}
defer destination.Close()
_, err = io.Copy(destination, source)
if err != nil {
return
}
}
func (cfg *Config) Export(format string) string {
if len(format) != 0 {
cfg.Format = format
}
var result bytes.Buffer
switch cfg.Format {
case YAML:
prefix := "# yaml-language-server: $schema=https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n"
yamlEncoder := yaml.NewEncoder(&result)
err := yamlEncoder.Encode(cfg)
if err != nil {
return ""
}
return prefix + result.String()
case JSON:
jsonEncoder := json.NewEncoder(&result)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.SetIndent("", " ")
_ = jsonEncoder.Encode(cfg)
prefix := "{\n \"$schema\": \"https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\","
data := strings.Replace(result.String(), "{", prefix, 1)
return escapeGlyphs(data, cfg.MigrateGlyphs)
case TOML:
prefix := "#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n"
tomlEncoder := toml.NewEncoder(&result)
tomlEncoder.SetIndentTables(true)
err := tomlEncoder.Encode(cfg)
if err != nil {
return ""
}
return prefix + result.String()
}
// unsupported format
return ""
}
func (cfg *Config) BackupAndMigrate() {
cfg.Backup()
cfg.Migrate()
cfg.Write(cfg.Format)
}
func (cfg *Config) Write(format string) {
content := cfg.Export(format)
if len(content) == 0 {
// we are unable to perform the export
os.Exit(65)
return
}
destination := cfg.Output
if len(destination) == 0 {
destination = cfg.origin
}
f, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return
}
defer func() {
_ = f.Close()
}()
_, err = f.WriteString(content)
if err != nil {
return
}
}

119
src/config/block.go Normal file
View file

@ -0,0 +1,119 @@
package config
import (
"sync"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
// BlockType type of block
type BlockType string
// BlockAlignment aligment of a Block
type BlockAlignment string
// Overflow defines how to handle a right block that overflows with the previous block
type Overflow string
const (
// Prompt writes one or more Segments
Prompt BlockType = "prompt"
// LineBreak creates a line break in the prompt
LineBreak BlockType = "newline"
// RPrompt is a right aligned prompt
RPrompt BlockType = "rprompt"
// Left aligns left
Left BlockAlignment = "left"
// Right aligns right
Right BlockAlignment = "right"
// Break adds a line break
Break Overflow = "break"
// Hide hides the block
Hide Overflow = "hide"
)
// Block defines a part of the prompt with optional segments
type Block struct {
Type BlockType `json:"type,omitempty" toml:"type,omitempty"`
Alignment BlockAlignment `json:"alignment,omitempty" toml:"alignment,omitempty"`
Segments []*Segment `json:"segments,omitempty" toml:"segments,omitempty"`
Newline bool `json:"newline,omitempty" toml:"newline,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
Overflow Overflow `json:"overflow,omitempty" toml:"overflow,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty"`
// Deprecated: keep the logic for legacy purposes
HorizontalOffset int `json:"horizontal_offset,omitempty" toml:"horizontal_offset,omitempty"`
VerticalOffset int `json:"vertical_offset,omitempty" toml:"vertical_offset,omitempty"`
MaxWidth int `json:"max_width,omitempty" toml:"max_width,omitempty"`
MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty"`
env platform.Environment
}
func (b *Block) Init(env platform.Environment) {
b.env = env
b.executeSegmentLogic()
}
func (b *Block) InitPlain(env platform.Environment, config *Config) {
terminal.Init(shell.GENERIC)
terminal.BackgroundColor = shell.ConsoleBackgroundColor(env, config.TerminalBackground)
terminal.AnsiColors = config.MakeColors()
b.env = env
b.executeSegmentLogic()
}
func (b *Block) Enabled() bool {
if b.Type == LineBreak {
return true
}
for _, segment := range b.Segments {
if segment.Enabled {
return true
}
}
return false
}
func (b *Block) executeSegmentLogic() {
if shouldHideForWidth(b.env, b.MinWidth, b.MaxWidth) {
return
}
b.setEnabledSegments()
b.setSegmentsText()
}
func (b *Block) setEnabledSegments() {
wg := sync.WaitGroup{}
wg.Add(len(b.Segments))
defer wg.Wait()
for _, segment := range b.Segments {
go func(s *Segment) {
defer wg.Done()
s.SetEnabled(b.env)
}(segment)
}
}
func (b *Block) setSegmentsText() {
wg := sync.WaitGroup{}
wg.Add(len(b.Segments))
defer wg.Wait()
for _, segment := range b.Segments {
go func(s *Segment) {
defer wg.Done()
s.setText()
}(segment)
}
}

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"testing"

75
src/config/config.go Normal file
View file

@ -0,0 +1,75 @@
package config
import (
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
const (
JSON string = "json"
YAML string = "yaml"
TOML string = "toml"
Version = 2
)
// Config holds all the theme for rendering the prompt
type Config struct {
Version int `json:"version" toml:"version"`
FinalSpace bool `json:"final_space,omitempty" toml:"final_space,omitempty"`
ConsoleTitleTemplate string `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"`
TerminalBackground string `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"`
AccentColor string `json:"accent_color,omitempty" toml:"accent_color,omitempty"`
Blocks []*Block `json:"blocks,omitempty" toml:"blocks,omitempty"`
Tooltips []*Segment `json:"tooltips,omitempty" toml:"tooltips,omitempty"`
TransientPrompt *Segment `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty"`
ValidLine *Segment `json:"valid_line,omitempty" toml:"valid_line,omitempty"`
ErrorLine *Segment `json:"error_line,omitempty" toml:"error_line,omitempty"`
SecondaryPrompt *Segment `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty"`
DebugPrompt *Segment `json:"debug_prompt,omitempty" toml:"debug_prompt,omitempty"`
Palette terminal.Palette `json:"palette,omitempty" toml:"palette,omitempty"`
Palettes *terminal.Palettes `json:"palettes,omitempty" toml:"palettes,omitempty"`
Cycle terminal.Cycle `json:"cycle,omitempty" toml:"cycle,omitempty"`
ShellIntegration bool `json:"shell_integration,omitempty" toml:"shell_integration,omitempty"`
PWD string `json:"pwd,omitempty" toml:"pwd,omitempty"`
Var map[string]any `json:"var,omitempty" toml:"var,omitempty"`
DisableCursorPositioning bool `json:"disable_cursor_positioning,omitempty" toml:"disable_cursor_positioning,omitempty"`
PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty"`
DisableNotice bool `json:"disable_notice,omitempty" toml:"disable_notice,omitempty"`
AutoUpgrade bool `json:"auto_upgrade,omitempty" toml:"auto_upgrade,omitempty"`
ITermFeatures terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty"`
// Deprecated
OSC99 bool `json:"osc99,omitempty" toml:"osc99,omitempty"`
Output string `json:"-" toml:"-"`
MigrateGlyphs bool `json:"-" toml:"-"`
Format string `json:"-" toml:"-"`
origin string
// eval bool
updated bool
env platform.Environment
}
func (cfg *Config) MakeColors() terminal.ColorString {
cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1"
return terminal.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env)
}
func (cfg *Config) getPalette() terminal.Palette {
if cfg.Palettes == nil {
return cfg.Palette
}
tmpl := &template.Text{
Template: cfg.Palettes.Template,
Env: cfg.env,
}
if palette, err := tmpl.Render(); err == nil {
if p, ok := cfg.Palettes.List[palette]; ok {
return p
}
}
return cfg.Palette
}

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"testing"
@ -11,25 +11,6 @@ import (
mock2 "github.com/stretchr/testify/mock"
)
func TestEscapeGlyphs(t *testing.T) {
cases := []struct {
Input string
Expected string
}{
{Input: "󰉋", Expected: "\\udb80\\ude4b"},
{Input: "a", Expected: "a"},
{Input: "\ue0b4", Expected: "\\ue0b4"},
{Input: "\ufd03", Expected: "\\ufd03"},
{Input: "}", Expected: "}"},
{Input: "🏚", Expected: "🏚"},
{Input: "\U000F011B", Expected: "\\udb80\\udd1b"},
{Input: "󰄛", Expected: "\\udb80\\udd1b"},
}
for _, tc := range cases {
assert.Equal(t, tc.Expected, escapeGlyphs(tc.Input, false), tc.Input)
}
}
func TestGetPalette(t *testing.T) {
palette := terminal.Palette{
"red": "#ff0000",

199
src/config/default.go Normal file
View file

@ -0,0 +1,199 @@
package config
import (
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/segments"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
func Default(env platform.Environment, warning bool) *Config {
exitBackgroundTemplate := "{{ if gt .Code 0 }}p:red{{ end }}"
exitTemplate := " {{ if gt .Code 0 }}\uf00d{{ else }}\uf00c{{ end }} "
if warning {
exitBackgroundTemplate = "p:red"
exitTemplate = " CONFIG ERROR "
}
cfg := &Config{
Version: 2,
FinalSpace: true,
Blocks: []*Block{
{
Type: Prompt,
Alignment: Left,
Segments: []*Segment{
{
Type: SESSION,
Style: Diamond,
LeadingDiamond: "\ue0b6",
TrailingDiamond: "\ue0b0",
Background: "p:yellow",
Foreground: "p:black",
Template: " {{ if .SSHSession }}\ueba9 {{ end }}{{ .UserName }} ",
},
{
Type: PATH,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:orange",
Foreground: "p:white",
Properties: properties.Map{
properties.Style: "folder",
},
Template: " \uea83 {{ path .Path .Location }} ",
},
{
Type: GIT,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:green",
BackgroundTemplates: []string{
"{{ if or (.Working.Changed) (.Staging.Changed) }}p:yellow{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:red{{ end }}",
"{{ if gt .Ahead 0 }}#49416D{{ end }}",
"{{ if gt .Behind 0 }}#7A306C{{ end }}",
},
Foreground: "p:black",
ForegroundTemplates: []string{
"{{ if or (.Working.Changed) (.Staging.Changed) }}p:black{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:white{{ end }}",
"{{ if gt .Ahead 0 }}p:white{{ end }}",
},
Properties: properties.Map{
segments.BranchMaxLength: 25,
segments.FetchStatus: true,
segments.FetchUpstreamIcon: true,
},
Template: " {{ if .UpstreamURL }}{{ url .UpstreamIcon .UpstreamURL }} {{ end }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uf044 {{ .Working.String }}{{ end }}{{ if .Staging.Changed }} \uf046 {{ .Staging.String }}{{ end }} ", //nolint:lll
},
{
Type: ROOT,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:yellow",
Foreground: "p:white",
Template: " \uf0e7 ",
},
{
Type: STATUS,
Style: Diamond,
LeadingDiamond: "<transparent,background>\ue0b0</>",
TrailingDiamond: "\ue0b4",
Background: "p:blue",
BackgroundTemplates: []string{
exitBackgroundTemplate,
},
Foreground: "p:white",
Properties: properties.Map{
properties.AlwaysEnabled: true,
},
Template: exitTemplate,
},
},
},
{
Type: RPrompt,
Segments: []*Segment{
{
Type: NODE,
Style: Plain,
Background: "transparent",
Foreground: "p:green",
Template: "\ue718 ",
Properties: properties.Map{
segments.HomeEnabled: false,
segments.FetchPackageManager: false,
segments.DisplayMode: "files",
},
},
{
Type: GOLANG,
Style: Plain,
Background: "transparent",
Foreground: "p:blue",
Template: "\ue626 ",
Properties: properties.Map{
properties.FetchVersion: false,
},
},
{
Type: PYTHON,
Style: Plain,
Background: "transparent",
Foreground: "p:yellow",
Template: "\ue235 ",
Properties: properties.Map{
properties.FetchVersion: false,
segments.DisplayMode: "files",
segments.FetchVirtualEnv: false,
},
},
{
Type: SHELL,
Style: Plain,
Background: "transparent",
Foreground: "p:white",
Template: "in <p:blue><b>{{ .Name }}</b></> ",
},
{
Type: TIME,
Style: Plain,
Background: "transparent",
Foreground: "p:white",
Template: "at <p:blue><b>{{ .CurrentDate | date \"15:04:05\" }}</b></>",
},
},
},
},
ConsoleTitleTemplate: "{{ .Shell }} in {{ .Folder }}",
Palette: terminal.Palette{
"black": "#262B44",
"blue": "#4B95E9",
"green": "#59C9A5",
"orange": "#F07623",
"red": "#D81E5B",
"white": "#E0DEF4",
"yellow": "#F3AE35",
},
SecondaryPrompt: &Segment{
Background: "transparent",
Foreground: "p:black",
Template: "<p:yellow,transparent>\ue0b6</><,p:yellow> > </><p:yellow,transparent>\ue0b0</> ",
},
TransientPrompt: &Segment{
Background: "transparent",
Foreground: "p:black",
Template: "<p:yellow,transparent>\ue0b6</><,p:yellow> {{ .Folder }} </><p:yellow,transparent>\ue0b0</> ",
},
Tooltips: []*Segment{
{
Type: AWS,
Style: Diamond,
LeadingDiamond: "\ue0b0",
TrailingDiamond: "\ue0b4",
Background: "p:orange",
Foreground: "p:white",
Template: " \ue7ad {{ .Profile }}{{ if .Region }}@{{ .Region }}{{ end }} ",
Properties: properties.Map{
properties.DisplayDefault: true,
},
Tips: []string{"aws"},
},
{
Type: AZ,
Style: Diamond,
LeadingDiamond: "\ue0b0",
TrailingDiamond: "\ue0b4",
Background: "p:blue",
Foreground: "p:white",
Template: " \uebd8 {{ .Name }} ",
Properties: properties.Map{
properties.DisplayDefault: true,
},
Tips: []string{"az"},
},
},
}
cfg.env = env
return cfg
}

95
src/config/load.go Normal file
View file

@ -0,0 +1,95 @@
package config
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/gookit/goutil/jsonutil"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
json "github.com/goccy/go-json"
yaml "github.com/goccy/go-yaml"
toml "github.com/pelletier/go-toml/v2"
)
// LoadConfig returns the default configuration including possible user overrides
func Load(env platform.Environment) *Config {
cfg := loadConfig(env)
// only migrate automatically when the switch isn't set
if !env.Flags().Migrate && cfg.Version < Version {
cfg.BackupAndMigrate()
}
if !cfg.ShellIntegration {
return cfg
}
// bash - ok
// fish - ok
// pwsh - ok
// zsh - ok
// cmd - ok, as of v1.4.25 (chrisant996/clink#457, fixed in chrisant996/clink@8a5d7ea)
// nu - built-in (and bugged) feature - nushell/nushell#5585, https://www.nushell.sh/blog/2022-08-16-nushell-0_67.html#shell-integration-fdncred-and-tyriar
// elv - broken OSC sequences
// xonsh - broken OSC sequences
// tcsh - overall broken, FTCS_COMMAND_EXECUTED could be added to POSH_POSTCMD in the future
switch env.Shell() {
case shell.ELVISH, shell.XONSH, shell.TCSH, shell.NU:
cfg.ShellIntegration = false
}
return cfg
}
func loadConfig(env platform.Environment) *Config {
defer env.Trace(time.Now())
configFile := env.Flags().Config
if len(configFile) == 0 {
env.Debug("no config file specified, using default")
return Default(env, false)
}
var cfg Config
cfg.origin = configFile
cfg.Format = strings.TrimPrefix(filepath.Ext(configFile), ".")
cfg.env = env
data, err := os.ReadFile(configFile)
if err != nil {
env.DebugF("error reading config file: %s", err)
return Default(env, true)
}
switch cfg.Format {
case "yml", "yaml":
cfg.Format = YAML
err = yaml.Unmarshal(data, &cfg)
case "jsonc", "json":
cfg.Format = JSON
str := jsonutil.StripComments(string(data))
data = []byte(str)
decoder := json.NewDecoder(bytes.NewReader(data))
err = decoder.Decode(&cfg)
case "toml", "tml":
cfg.Format = TOML
err = toml.Unmarshal(data, &cfg)
default:
err = fmt.Errorf("unsupported config file format: %s", cfg.Format)
}
if err != nil {
env.DebugF("error decoding config file: %s", err)
return Default(env, true)
}
return &cfg
}

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"fmt"
@ -30,7 +30,7 @@ func (cfg *Config) Migrate() {
cfg.ConsoleTitleTemplate = strings.ReplaceAll(cfg.ConsoleTitleTemplate, ".Path", ".PWD")
}
cfg.updated = true
cfg.Version = configVersion
cfg.Version = Version
}
func (segment *Segment) migrate(env platform.Environment, version int) {
@ -43,7 +43,7 @@ func (segment *Segment) migrate(env platform.Environment, version int) {
}
func (segment *Segment) migrationOne(env platform.Environment) {
if err := segment.mapSegmentWithWriter(env); err != nil {
if err := segment.MapSegmentWithWriter(env); err != nil {
return
}
// General properties that need replacement
@ -163,7 +163,7 @@ func (segment *Segment) migrationOne(env platform.Environment) {
}
func (segment *Segment) migrationTwo(env platform.Environment) {
if err := segment.mapSegmentWithWriter(env); err != nil {
if err := segment.MapSegmentWithWriter(env); err != nil {
return
}
if !segment.hasProperty(segmentTemplate) {

View file

@ -0,0 +1,140 @@
package config
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/platform/net"
)
type ConnectionError struct {
reason string
}
func (f *ConnectionError) Error() string {
return f.reason
}
type codePoints map[uint64]uint64
func getGlyphCodePoints() (codePoints, error) {
var codePoints = make(codePoints)
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cncl()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://ohmyposh.dev/codepoints.csv", nil)
if err != nil {
return codePoints, &ConnectionError{reason: err.Error()}
}
response, err := net.HTTPClient.Do(request)
if err != nil {
return codePoints, err
}
defer response.Body.Close()
bytes, err := io.ReadAll(response.Body)
if err != nil {
return codePoints, err
}
lines := strings.Split(string(bytes), "\n")
for _, line := range lines {
fields := strings.Split(line, ",")
if len(fields) < 2 {
continue
}
oldGlyph, err := strconv.ParseUint(fields[0], 16, 32)
if err != nil {
continue
}
newGlyph, err := strconv.ParseUint(fields[1], 16, 32)
if err != nil {
continue
}
codePoints[oldGlyph] = newGlyph
}
return codePoints, nil
}
func escapeGlyphs(s string, migrate bool) string {
shouldExclude := func(r rune) bool {
if r < 0x1000 { // Basic Multilingual Plane
return true
}
if r > 0x1F600 && r < 0x1F64F { // Emoticons
return true
}
if r > 0x1F300 && r < 0x1F5FF { // Misc Symbols and Pictographs
return true
}
if r > 0x1F680 && r < 0x1F6FF { // Transport and Map
return true
}
if r > 0x2600 && r < 0x26FF { // Misc symbols
return true
}
if r > 0x2700 && r < 0x27BF { // Dingbats
return true
}
if r > 0xFE00 && r < 0xFE0F { // Variation Selectors
return true
}
if r > 0x1F900 && r < 0x1F9FF { // Supplemental Symbols and Pictographs
return true
}
if r > 0x1F1E6 && r < 0x1F1FF { // Flags
return true
}
return false
}
var cp codePoints
var err error
if migrate {
cp, err = getGlyphCodePoints()
if err != nil {
migrate = false
}
}
var builder strings.Builder
for _, r := range s {
// exclude regular characters and emojis
if shouldExclude(r) {
builder.WriteRune(r)
continue
}
if migrate {
if val, OK := cp[uint64(r)]; OK {
r = rune(val)
}
}
if r > 0x10000 {
// calculate surrogate pairs
one := 0xd800 + (((r - 0x10000) >> 10) & 0x3ff)
two := 0xdc00 + ((r - 0x10000) & 0x3ff)
quoted := fmt.Sprintf("\\u%04x\\u%04x", one, two)
builder.WriteString(quoted)
continue
}
quoted := fmt.Sprintf("\\u%04x", r)
builder.WriteString(quoted)
}
return builder.String()
}

View file

@ -0,0 +1,35 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetCodePoints(t *testing.T) {
codepoints, err := getGlyphCodePoints()
if connectionError, ok := err.(*ConnectionError); ok {
t.Log(connectionError.Error())
return
}
assert.Equal(t, 1939, len(codepoints))
}
func TestEscapeGlyphs(t *testing.T) {
cases := []struct {
Input string
Expected string
}{
{Input: "󰉋", Expected: "\\udb80\\ude4b"},
{Input: "a", Expected: "a"},
{Input: "\ue0b4", Expected: "\\ue0b4"},
{Input: "\ufd03", Expected: "\\ufd03"},
{Input: "}", Expected: "}"},
{Input: "🏚", Expected: "🏚"},
{Input: "\U000F011B", Expected: "\\udb80\\udd1b"},
{Input: "󰄛", Expected: "\\udb80\\udd1b"},
}
for _, tc := range cases {
assert.Equal(t, tc.Expected, escapeGlyphs(tc.Input, false), tc.Input)
}
}

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"testing"

View file

@ -1,4 +1,4 @@
package engine
package config
import "github.com/jandedobbeleer/oh-my-posh/src/platform"

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"testing"

260
src/config/segment.go Normal file
View file

@ -0,0 +1,260 @@
package config
import (
"fmt"
"runtime/debug"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
c "golang.org/x/text/cases"
"golang.org/x/text/language"
)
// SegmentStyle the style of segment, for more information, see the constants
type SegmentStyle string
func (s *SegmentStyle) resolve(env platform.Environment, context any) SegmentStyle {
txtTemplate := &template.Text{
Context: context,
Env: env,
}
txtTemplate.Template = string(*s)
value, err := txtTemplate.Render()
// default to Plain
if err != nil || len(value) == 0 {
return Plain
}
return SegmentStyle(value)
}
type Segment struct {
Type SegmentType `json:"type,omitempty" toml:"type,omitempty"`
Tips []string `json:"tips,omitempty" toml:"tips,omitempty"`
Style SegmentStyle `json:"style,omitempty" toml:"style,omitempty"`
PowerlineSymbol string `json:"powerline_symbol,omitempty" toml:"powerline_symbol,omitempty"`
LeadingPowerlineSymbol string `json:"leading_powerline_symbol,omitempty" toml:"leading_powerline_symbol,omitempty"`
InvertPowerline bool `json:"invert_powerline,omitempty" toml:"invert_powerline,omitempty"`
Foreground string `json:"foreground,omitempty" toml:"foreground,omitempty"`
ForegroundTemplates template.List `json:"foreground_templates,omitempty" toml:"foreground_templates,omitempty"`
Background string `json:"background,omitempty" toml:"background,omitempty"`
BackgroundTemplates template.List `json:"background_templates,omitempty" toml:"background_templates,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty"`
Template string `json:"template,omitempty" toml:"template,omitempty"`
Templates template.List `json:"templates,omitempty" toml:"templates,omitempty"`
TemplatesLogic template.Logic `json:"templates_logic,omitempty" toml:"templates_logic,omitempty"`
Properties properties.Map `json:"properties,omitempty" toml:"properties,omitempty"`
Interactive bool `json:"interactive,omitempty" toml:"interactive,omitempty"`
Alias string `json:"alias,omitempty" toml:"alias,omitempty"`
MaxWidth int `json:"max_width,omitempty" toml:"max_width,omitempty"`
MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
Enabled bool `json:"-" toml:"-"`
Colors *terminal.Colors
Text string
env platform.Environment
writer SegmentWriter
styleCache SegmentStyle
name string
// debug info
Duration time.Duration
NameLength int
}
func (segment *Segment) Name() string {
if len(segment.name) != 0 {
return segment.name
}
name := segment.Alias
if len(name) == 0 {
name = c.Title(language.English).String(string(segment.Type))
}
segment.name = name
return name
}
func (segment *Segment) SetEnabled(env platform.Environment) {
defer func() {
err := recover()
if err == nil {
return
}
// display a message explaining omp failed(with the err)
message := fmt.Sprintf("\noh-my-posh fatal error rendering %s segment:%s\n\n%s\n", segment.Type, err, debug.Stack())
fmt.Println(message)
segment.Enabled = true
}()
// segment timings for debug purposes
var start time.Time
if env.Flags().Debug {
start = time.Now()
segment.NameLength = len(segment.Name())
defer func() {
segment.Duration = time.Since(start)
}()
}
err := segment.MapSegmentWithWriter(env)
if err != nil || !segment.shouldIncludeFolder() {
return
}
segment.env.DebugF("Segment: %s", segment.Name())
// validate toggles
if toggles, OK := segment.env.Cache().Get(platform.TOGGLECACHE); OK && len(toggles) > 0 {
list := strings.Split(toggles, ",")
for _, toggle := range list {
if SegmentType(toggle) == segment.Type || toggle == segment.Alias {
return
}
}
}
if shouldHideForWidth(segment.env, segment.MinWidth, segment.MaxWidth) {
return
}
if segment.writer.Enabled() {
segment.Enabled = true
env.TemplateCache().AddSegmentData(segment.Name(), segment.writer)
}
}
func (segment *Segment) setText() {
if !segment.Enabled {
return
}
segment.Text = segment.string()
segment.Enabled = len(strings.ReplaceAll(segment.Text, " ", "")) > 0
if !segment.Enabled {
segment.env.TemplateCache().RemoveSegmentData(segment.Name())
}
}
func (segment *Segment) string() string {
var templatesResult string
if !segment.Templates.Empty() {
templatesResult = segment.Templates.Resolve(segment.writer, segment.env, "", segment.TemplatesLogic)
if len(segment.Template) == 0 {
return templatesResult
}
}
if len(segment.Template) == 0 {
segment.Template = segment.writer.Template()
}
tmpl := &template.Text{
Template: segment.Template,
Context: segment.writer,
Env: segment.env,
TemplatesResult: templatesResult,
}
text, err := tmpl.Render()
if err != nil {
return err.Error()
}
return text
}
func (segment *Segment) shouldIncludeFolder() bool {
if segment.env == nil {
return true
}
cwdIncluded := segment.cwdIncluded()
cwdExcluded := segment.cwdExcluded()
return cwdIncluded && !cwdExcluded
}
func (segment *Segment) cwdIncluded() bool {
value, ok := segment.Properties[properties.IncludeFolders]
if !ok {
// IncludeFolders isn't specified, everything is included
return true
}
list := properties.ParseStringArray(value)
if len(list) == 0 {
// IncludeFolders is an empty array, everything is included
return true
}
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
}
func (segment *Segment) cwdExcluded() bool {
value, ok := segment.Properties[properties.ExcludeFolders]
if !ok {
value = segment.Properties[properties.IgnoreFolders]
}
list := properties.ParseStringArray(value)
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
}
func (segment *Segment) ResolveForeground() string {
if segment.Colors == nil {
segment.Colors = &terminal.Colors{}
}
if len(segment.Colors.Foreground) == 0 {
segment.Colors.Foreground = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground)
}
return segment.Colors.Foreground
}
func (segment *Segment) ResolveBackground() string {
if segment.Colors == nil {
segment.Colors = &terminal.Colors{}
}
if len(segment.Colors.Background) == 0 {
segment.Colors.Background = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background)
}
return segment.Colors.Background
}
func (segment *Segment) ResolveStyle() SegmentStyle {
if len(segment.styleCache) != 0 {
return segment.styleCache
}
segment.styleCache = segment.Style.resolve(segment.env, segment.writer)
return segment.styleCache
}
func (segment *Segment) IsPowerline() bool {
style := segment.ResolveStyle()
return style == Powerline || style == Accordion
}
func (segment *Segment) HasEmptyDiamondAtEnd() bool {
if segment.ResolveStyle() != Diamond {
return false
}
return len(segment.TrailingDiamond) == 0
}

View file

@ -1,4 +1,4 @@
package engine
package config
import (
"encoding/json"
@ -22,7 +22,7 @@ func TestMapSegmentWriterCanMap(t *testing.T) {
Type: SESSION,
}
env := new(mock.MockedEnvironment)
err := sc.mapSegmentWithWriter(env)
err := sc.MapSegmentWithWriter(env)
assert.NoError(t, err)
assert.NotNil(t, sc.writer)
}
@ -32,7 +32,7 @@ func TestMapSegmentWriterCannotMap(t *testing.T) {
Type: "nilwriter",
}
env := new(mock.MockedEnvironment)
err := sc.mapSegmentWithWriter(env)
err := sc.MapSegmentWithWriter(env)
assert.Error(t, err)
}
@ -157,13 +157,13 @@ func TestGetColors(t *testing.T) {
if tc.Background {
segment.Background = tc.DefaultColor
segment.BackgroundTemplates = tc.Templates
color := segment.background()
color := segment.ResolveBackground()
assert.Equal(t, tc.ExpectedColor, color, tc.Case)
continue
}
segment.Foreground = tc.DefaultColor
segment.ForegroundTemplates = tc.Templates
color := segment.foreground()
color := segment.ResolveForeground()
assert.Equal(t, tc.ExpectedColor, color, tc.Case)
}
}

View file

@ -1,59 +1,15 @@
package engine
package config
import (
"errors"
"fmt"
"runtime/debug"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/segments"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
c "golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Segment represent a single segment and it's configuration
type Segment struct {
Type SegmentType `json:"type,omitempty" toml:"type,omitempty"`
Tips []string `json:"tips,omitempty" toml:"tips,omitempty"`
Style SegmentStyle `json:"style,omitempty" toml:"style,omitempty"`
PowerlineSymbol string `json:"powerline_symbol,omitempty" toml:"powerline_symbol,omitempty"`
LeadingPowerlineSymbol string `json:"leading_powerline_symbol,omitempty" toml:"leading_powerline_symbol,omitempty"`
InvertPowerline bool `json:"invert_powerline,omitempty" toml:"invert_powerline,omitempty"`
Foreground string `json:"foreground,omitempty" toml:"foreground,omitempty"`
ForegroundTemplates template.List `json:"foreground_templates,omitempty" toml:"foreground_templates,omitempty"`
Background string `json:"background,omitempty" toml:"background,omitempty"`
BackgroundTemplates template.List `json:"background_templates,omitempty" toml:"background_templates,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty"`
Template string `json:"template,omitempty" toml:"template,omitempty"`
Templates template.List `json:"templates,omitempty" toml:"templates,omitempty"`
TemplatesLogic template.Logic `json:"templates_logic,omitempty" toml:"templates_logic,omitempty"`
Properties properties.Map `json:"properties,omitempty" toml:"properties,omitempty"`
Interactive bool `json:"interactive,omitempty" toml:"interactive,omitempty"`
Alias string `json:"alias,omitempty" toml:"alias,omitempty"`
MaxWidth int `json:"max_width,omitempty" toml:"max_width,omitempty"`
MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
Enabled bool `json:"-" toml:"-"`
colors *terminal.Colors
env platform.Environment
writer SegmentWriter
text string
styleCache SegmentStyle
name string
// debug info
duration time.Duration
nameLength int
}
// SegmentType the type of segment, for more information, see the constants
type SegmentType string
// SegmentWriter is the interface used to define what and if to write to the prompt
type SegmentWriter interface {
@ -62,26 +18,6 @@ type SegmentWriter interface {
Init(props properties.Properties, env platform.Environment)
}
// SegmentStyle the style of segment, for more information, see the constants
type SegmentStyle string
func (s *SegmentStyle) Resolve(env platform.Environment, context any) SegmentStyle {
txtTemplate := &template.Text{
Context: context,
Env: env,
}
txtTemplate.Template = string(*s)
value, err := txtTemplate.Render()
// default to Plain
if err != nil || len(value) == 0 {
return Plain
}
return SegmentStyle(value)
}
// SegmentType the type of segment, for more information, see the constants
type SegmentType string
const (
// Plain writes it without ornaments
Plain SegmentStyle = "plain"
@ -372,102 +308,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
YTM: func() SegmentWriter { return &segments.Ytm{} },
}
func (segment *Segment) style() SegmentStyle {
if len(segment.styleCache) != 0 {
return segment.styleCache
}
segment.styleCache = segment.Style.Resolve(segment.env, segment.writer)
return segment.styleCache
}
func (segment *Segment) shouldIncludeFolder() bool {
if segment.env == nil {
return true
}
cwdIncluded := segment.cwdIncluded()
cwdExcluded := segment.cwdExcluded()
return cwdIncluded && !cwdExcluded
}
func (segment *Segment) isPowerline() bool {
style := segment.style()
return style == Powerline || style == Accordion
}
func (segment *Segment) hasEmptyDiamondAtEnd() bool {
if segment.style() != Diamond {
return false
}
return len(segment.TrailingDiamond) == 0
}
func (segment *Segment) cwdIncluded() bool {
value, ok := segment.Properties[properties.IncludeFolders]
if !ok {
// IncludeFolders isn't specified, everything is included
return true
}
list := properties.ParseStringArray(value)
if len(list) == 0 {
// IncludeFolders is an empty array, everything is included
return true
}
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
}
func (segment *Segment) cwdExcluded() bool {
value, ok := segment.Properties[properties.ExcludeFolders]
if !ok {
value = segment.Properties[properties.IgnoreFolders]
}
list := properties.ParseStringArray(value)
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
}
func (segment *Segment) shouldInvokeWithTip(tip string) bool {
for _, t := range segment.Tips {
if t == tip {
return true
}
}
return false
}
func (segment *Segment) foreground() string {
if segment.colors == nil {
segment.colors = &terminal.Colors{}
}
if len(segment.colors.Foreground) == 0 {
segment.colors.Foreground = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground)
}
return segment.colors.Foreground
}
func (segment *Segment) background() string {
if segment.colors == nil {
segment.colors = &terminal.Colors{}
}
if len(segment.colors.Background) == 0 {
segment.colors.Background = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background)
}
return segment.colors.Background
}
func (segment *Segment) mapSegmentWithWriter(env platform.Environment) error {
func (segment *Segment) MapSegmentWithWriter(env platform.Environment) error {
segment.env = env
if segment.Properties == nil {
@ -487,107 +328,3 @@ func (segment *Segment) mapSegmentWithWriter(env platform.Environment) error {
return errors.New("unable to map writer")
}
func (segment *Segment) string() string {
var templatesResult string
if !segment.Templates.Empty() {
templatesResult = segment.Templates.Resolve(segment.writer, segment.env, "", segment.TemplatesLogic)
if len(segment.Template) == 0 {
return templatesResult
}
}
if len(segment.Template) == 0 {
segment.Template = segment.writer.Template()
}
tmpl := &template.Text{
Template: segment.Template,
Context: segment.writer,
Env: segment.env,
TemplatesResult: templatesResult,
}
text, err := tmpl.Render()
if err != nil {
return err.Error()
}
return text
}
func (segment *Segment) Name() string {
if len(segment.name) != 0 {
return segment.name
}
name := segment.Alias
if len(name) == 0 {
name = c.Title(language.English).String(string(segment.Type))
}
segment.name = name
return name
}
func (segment *Segment) SetEnabled(env platform.Environment) {
defer func() {
err := recover()
if err == nil {
return
}
// display a message explaining omp failed(with the err)
message := fmt.Sprintf("\noh-my-posh fatal error rendering %s segment:%s\n\n%s\n", segment.Type, err, debug.Stack())
fmt.Println(message)
segment.Enabled = true
}()
// segment timings for debug purposes
var start time.Time
if env.Flags().Debug {
start = time.Now()
segment.nameLength = len(segment.Name())
defer func() {
segment.duration = time.Since(start)
}()
}
err := segment.mapSegmentWithWriter(env)
if err != nil || !segment.shouldIncludeFolder() {
return
}
segment.env.DebugF("Segment: %s", segment.Name())
// validate toggles
if toggles, OK := segment.env.Cache().Get(platform.TOGGLECACHE); OK && len(toggles) > 0 {
list := strings.Split(toggles, ",")
for _, toggle := range list {
if SegmentType(toggle) == segment.Type || toggle == segment.Alias {
return
}
}
}
if shouldHideForWidth(segment.env, segment.MinWidth, segment.MaxWidth) {
return
}
if segment.writer.Enabled() {
segment.Enabled = true
env.TemplateCache().AddSegmentData(segment.Name(), segment.writer)
}
}
func (segment *Segment) SetText() {
if !segment.Enabled {
return
}
segment.text = segment.string()
segment.Enabled = len(strings.ReplaceAll(segment.text, " ", "")) > 0
if !segment.Enabled {
segment.env.TemplateCache().RemoveSegmentData(segment.Name())
}
}

View file

@ -1,328 +0,0 @@
package engine
import (
"strings"
"sync"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
// BlockType type of block
type BlockType string
// BlockAlignment aligment of a Block
type BlockAlignment string
// Overflow defines how to handle a right block that overflows with the previous block
type Overflow string
const (
// Prompt writes one or more Segments
Prompt BlockType = "prompt"
// LineBreak creates a line break in the prompt
LineBreak BlockType = "newline"
// RPrompt is a right aligned prompt
RPrompt BlockType = "rprompt"
// Left aligns left
Left BlockAlignment = "left"
// Right aligns right
Right BlockAlignment = "right"
// Break adds a line break
Break Overflow = "break"
// Hide hides the block
Hide Overflow = "hide"
)
// Block defines a part of the prompt with optional segments
type Block struct {
Type BlockType `json:"type,omitempty" toml:"type,omitempty"`
Alignment BlockAlignment `json:"alignment,omitempty" toml:"alignment,omitempty"`
Segments []*Segment `json:"segments,omitempty" toml:"segments,omitempty"`
Newline bool `json:"newline,omitempty" toml:"newline,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
Overflow Overflow `json:"overflow,omitempty" toml:"overflow,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty"`
// Deprecated: keep the logic for legacy purposes
HorizontalOffset int `json:"horizontal_offset,omitempty" toml:"horizontal_offset,omitempty"`
VerticalOffset int `json:"vertical_offset,omitempty" toml:"vertical_offset,omitempty"`
MaxWidth int `json:"max_width,omitempty" toml:"max_width,omitempty"`
MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty"`
env platform.Environment
activeSegment *Segment
previousActiveSegment *Segment
}
func (b *Block) Init(env platform.Environment) {
b.env = env
b.executeSegmentLogic()
}
func (b *Block) InitPlain(env platform.Environment, config *Config) {
terminal.Init(shell.GENERIC)
terminal.BackgroundColor = shell.ConsoleBackgroundColor(env, config.TerminalBackground)
terminal.AnsiColors = config.MakeColors()
b.env = env
b.executeSegmentLogic()
}
func (b *Block) executeSegmentLogic() {
if shouldHideForWidth(b.env, b.MinWidth, b.MaxWidth) {
return
}
b.setEnabledSegments()
b.setSegmentsText()
}
func (b *Block) setActiveSegment(segment *Segment) {
b.activeSegment = segment
terminal.Interactive = segment.Interactive
terminal.SetColors(segment.background(), segment.foreground())
}
func (b *Block) Enabled() bool {
if b.Type == LineBreak {
return true
}
for _, segment := range b.Segments {
if segment.Enabled {
return true
}
}
return false
}
func (b *Block) setEnabledSegments() {
wg := sync.WaitGroup{}
wg.Add(len(b.Segments))
defer wg.Wait()
for _, segment := range b.Segments {
go func(s *Segment) {
defer wg.Done()
s.SetEnabled(b.env)
}(segment)
}
}
func (b *Block) setSegmentsText() {
wg := sync.WaitGroup{}
wg.Add(len(b.Segments))
defer wg.Wait()
for _, segment := range b.Segments {
go func(s *Segment) {
defer wg.Done()
s.SetText()
}(segment)
}
}
func (b *Block) RenderSegments() (string, int) {
b.filterSegments()
for i, segment := range b.Segments {
if colors, newCycle := cycle.Loop(); colors != nil {
cycle = &newCycle
segment.colors = colors
}
if i == 0 && len(b.LeadingDiamond) > 0 {
segment.LeadingDiamond = b.LeadingDiamond
}
if i == len(b.Segments)-1 && len(b.TrailingDiamond) > 0 {
segment.TrailingDiamond = b.TrailingDiamond
}
b.setActiveSegment(segment)
b.renderActiveSegment()
}
b.writeSeparator(true)
return terminal.String()
}
func (b *Block) filterSegments() {
segments := make([]*Segment, 0)
for _, segment := range b.Segments {
if !segment.Enabled && segment.style() != Accordion {
continue
}
segments = append(segments, segment)
}
b.Segments = segments
}
func (b *Block) renderActiveSegment() {
b.writeSeparator(false)
switch b.activeSegment.style() {
case Plain, Powerline:
terminal.Write(terminal.Background, terminal.Foreground, b.activeSegment.text)
case Diamond:
background := terminal.Transparent
if b.previousActiveSegment != nil && b.previousActiveSegment.hasEmptyDiamondAtEnd() {
background = b.previousActiveSegment.background()
}
terminal.Write(background, terminal.Background, b.activeSegment.LeadingDiamond)
terminal.Write(terminal.Background, terminal.Foreground, b.activeSegment.text)
case Accordion:
if b.activeSegment.Enabled {
terminal.Write(terminal.Background, terminal.Foreground, b.activeSegment.text)
}
}
b.previousActiveSegment = b.activeSegment
terminal.SetParentColors(b.previousActiveSegment.background(), b.previousActiveSegment.foreground())
}
func (b *Block) writeSeparator(final bool) {
isCurrentDiamond := b.activeSegment.style() == Diamond
if final && isCurrentDiamond {
terminal.Write(terminal.Transparent, terminal.Background, b.activeSegment.TrailingDiamond)
return
}
isPreviousDiamond := b.previousActiveSegment != nil && b.previousActiveSegment.style() == Diamond
if isPreviousDiamond {
b.adjustTrailingDiamondColorOverrides()
}
if isPreviousDiamond && isCurrentDiamond && len(b.activeSegment.LeadingDiamond) == 0 {
terminal.Write(terminal.Background, terminal.ParentBackground, b.previousActiveSegment.TrailingDiamond)
return
}
if isPreviousDiamond && len(b.previousActiveSegment.TrailingDiamond) > 0 {
terminal.Write(terminal.Transparent, terminal.ParentBackground, b.previousActiveSegment.TrailingDiamond)
}
isPowerline := b.activeSegment.isPowerline()
shouldOverridePowerlineLeadingSymbol := func() bool {
if !isPowerline {
return false
}
if isPowerline && len(b.activeSegment.LeadingPowerlineSymbol) == 0 {
return false
}
if b.previousActiveSegment != nil && b.previousActiveSegment.isPowerline() {
return false
}
return true
}
if shouldOverridePowerlineLeadingSymbol() {
terminal.Write(terminal.Transparent, terminal.Background, b.activeSegment.LeadingPowerlineSymbol)
return
}
resolvePowerlineSymbol := func() string {
if isPowerline {
return b.activeSegment.PowerlineSymbol
}
if b.previousActiveSegment != nil && b.previousActiveSegment.isPowerline() {
return b.previousActiveSegment.PowerlineSymbol
}
return ""
}
symbol := resolvePowerlineSymbol()
if len(symbol) == 0 {
return
}
bgColor := terminal.Background
if final || !isPowerline {
bgColor = terminal.Transparent
}
if b.activeSegment.style() == Diamond && len(b.activeSegment.LeadingDiamond) == 0 {
bgColor = terminal.Background
}
if b.activeSegment.InvertPowerline {
terminal.Write(b.getPowerlineColor(), bgColor, symbol)
return
}
terminal.Write(bgColor, b.getPowerlineColor(), symbol)
}
func (b *Block) adjustTrailingDiamondColorOverrides() {
// as we now already adjusted the activeSegment, we need to change the value
// of background and foreground to parentBackground and parentForeground
// this will still break when using parentBackground and parentForeground as keywords
// in a trailing diamond, but let's fix that when it happens as it requires either a rewrite
// of the logic for diamonds or storing grandparents as well like one happy family.
if b.previousActiveSegment == nil || len(b.previousActiveSegment.TrailingDiamond) == 0 {
return
}
if !strings.Contains(b.previousActiveSegment.TrailingDiamond, terminal.Background) && !strings.Contains(b.previousActiveSegment.TrailingDiamond, terminal.Foreground) {
return
}
match := regex.FindNamedRegexMatch(terminal.AnchorRegex, b.previousActiveSegment.TrailingDiamond)
if len(match) == 0 {
return
}
adjustOverride := func(anchor, override string) {
newOverride := override
switch override {
case terminal.Foreground:
newOverride = terminal.ParentForeground
case terminal.Background:
newOverride = terminal.ParentBackground
}
if override == newOverride {
return
}
newAnchor := strings.Replace(match[terminal.ANCHOR], override, newOverride, 1)
b.previousActiveSegment.TrailingDiamond = strings.Replace(b.previousActiveSegment.TrailingDiamond, anchor, newAnchor, 1)
}
if len(match[terminal.BG]) > 0 {
adjustOverride(match[terminal.ANCHOR], match[terminal.BG])
}
if len(match[terminal.FG]) > 0 {
adjustOverride(match[terminal.ANCHOR], match[terminal.FG])
}
}
func (b *Block) getPowerlineColor() string {
if b.previousActiveSegment == nil {
return terminal.Transparent
}
if b.previousActiveSegment.style() == Diamond && len(b.previousActiveSegment.TrailingDiamond) == 0 {
return b.previousActiveSegment.background()
}
if b.activeSegment.style() == Diamond && len(b.activeSegment.LeadingDiamond) == 0 {
return b.previousActiveSegment.background()
}
if !b.previousActiveSegment.isPowerline() {
return terminal.Transparent
}
return b.previousActiveSegment.background()
}

View file

@ -1,525 +0,0 @@
package engine
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
json "github.com/goccy/go-json"
yaml "github.com/goccy/go-yaml"
"github.com/gookit/goutil/jsonutil"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/segments"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
toml "github.com/pelletier/go-toml/v2"
)
const (
JSON string = "json"
YAML string = "yaml"
TOML string = "toml"
configVersion = 2
)
// Config holds all the theme for rendering the prompt
type Config struct {
Version int `json:"version" toml:"version"`
FinalSpace bool `json:"final_space,omitempty" toml:"final_space,omitempty"`
ConsoleTitleTemplate string `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"`
TerminalBackground string `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"`
AccentColor string `json:"accent_color,omitempty" toml:"accent_color,omitempty"`
Blocks []*Block `json:"blocks,omitempty" toml:"blocks,omitempty"`
Tooltips []*Segment `json:"tooltips,omitempty" toml:"tooltips,omitempty"`
TransientPrompt *Segment `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty"`
ValidLine *Segment `json:"valid_line,omitempty" toml:"valid_line,omitempty"`
ErrorLine *Segment `json:"error_line,omitempty" toml:"error_line,omitempty"`
SecondaryPrompt *Segment `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty"`
DebugPrompt *Segment `json:"debug_prompt,omitempty" toml:"debug_prompt,omitempty"`
Palette terminal.Palette `json:"palette,omitempty" toml:"palette,omitempty"`
Palettes *terminal.Palettes `json:"palettes,omitempty" toml:"palettes,omitempty"`
Cycle terminal.Cycle `json:"cycle,omitempty" toml:"cycle,omitempty"`
ShellIntegration bool `json:"shell_integration,omitempty" toml:"shell_integration,omitempty"`
PWD string `json:"pwd,omitempty" toml:"pwd,omitempty"`
Var map[string]any `json:"var,omitempty" toml:"var,omitempty"`
DisableCursorPositioning bool `json:"disable_cursor_positioning,omitempty" toml:"disable_cursor_positioning,omitempty"`
PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty"`
DisableNotice bool `json:"disable_notice,omitempty" toml:"disable_notice,omitempty"`
AutoUpgrade bool `json:"auto_upgrade,omitempty" toml:"auto_upgrade,omitempty"`
ITermFeatures terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty"`
// Deprecated
OSC99 bool `json:"osc99,omitempty" toml:"osc99,omitempty"`
Output string `json:"-" toml:"-"`
MigrateGlyphs bool `json:"-" toml:"-"`
Format string `json:"-" toml:"-"`
origin string
// eval bool
updated bool
env platform.Environment
}
// MakeColors creates instance of AnsiColors to use in AnsiWriter according to
// environment and configuration.
func (cfg *Config) MakeColors() terminal.ColorString {
cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1"
return terminal.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env)
}
func (cfg *Config) getPalette() terminal.Palette {
if cfg.Palettes == nil {
return cfg.Palette
}
tmpl := &template.Text{
Template: cfg.Palettes.Template,
Env: cfg.env,
}
if palette, err := tmpl.Render(); err == nil {
if p, ok := cfg.Palettes.List[palette]; ok {
return p
}
}
return cfg.Palette
}
// LoadConfig returns the default configuration including possible user overrides
func LoadConfig(env platform.Environment) *Config {
cfg := loadConfig(env)
// only migrate automatically when the switch isn't set
if !env.Flags().Migrate && cfg.Version < configVersion {
cfg.BackupAndMigrate()
}
if !cfg.ShellIntegration {
return cfg
}
// bash - ok
// fish - ok
// pwsh - ok
// zsh - ok
// cmd - ok, as of v1.4.25 (chrisant996/clink#457, fixed in chrisant996/clink@8a5d7ea)
// nu - built-in (and bugged) feature - nushell/nushell#5585, https://www.nushell.sh/blog/2022-08-16-nushell-0_67.html#shell-integration-fdncred-and-tyriar
// elv - broken OSC sequences
// xonsh - broken OSC sequences
// tcsh - overall broken, FTCS_COMMAND_EXECUTED could be added to POSH_POSTCMD in the future
switch env.Shell() {
case shell.ELVISH, shell.XONSH, shell.TCSH, shell.NU:
cfg.ShellIntegration = false
}
return cfg
}
func loadConfig(env platform.Environment) *Config {
defer env.Trace(time.Now())
configFile := env.Flags().Config
if len(configFile) == 0 {
env.Debug("no config file specified, using default")
return defaultConfig(env, false)
}
var cfg Config
cfg.origin = configFile
cfg.Format = strings.TrimPrefix(filepath.Ext(configFile), ".")
cfg.env = env
data, err := os.ReadFile(configFile)
if err != nil {
env.DebugF("error reading config file: %s", err)
return defaultConfig(env, true)
}
switch cfg.Format {
case "yml", "yaml":
cfg.Format = YAML
err = yaml.Unmarshal(data, &cfg)
case "jsonc", "json":
cfg.Format = JSON
str := jsonutil.StripComments(string(data))
data = []byte(str)
decoder := json.NewDecoder(bytes.NewReader(data))
err = decoder.Decode(&cfg)
case "toml", "tml":
cfg.Format = TOML
err = toml.Unmarshal(data, &cfg)
default:
err = fmt.Errorf("unsupported config file format: %s", cfg.Format)
}
if err != nil {
env.DebugF("error decoding config file: %s", err)
return defaultConfig(env, true)
}
return &cfg
}
func (cfg *Config) Export(format string) string {
if len(format) != 0 {
cfg.Format = format
}
var result bytes.Buffer
switch cfg.Format {
case YAML:
prefix := "# yaml-language-server: $schema=https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n"
yamlEncoder := yaml.NewEncoder(&result)
err := yamlEncoder.Encode(cfg)
if err != nil {
return ""
}
return prefix + result.String()
case JSON:
jsonEncoder := json.NewEncoder(&result)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.SetIndent("", " ")
_ = jsonEncoder.Encode(cfg)
prefix := "{\n \"$schema\": \"https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\","
data := strings.Replace(result.String(), "{", prefix, 1)
return escapeGlyphs(data, cfg.MigrateGlyphs)
case TOML:
prefix := "#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n"
tomlEncoder := toml.NewEncoder(&result)
tomlEncoder.SetIndentTables(true)
err := tomlEncoder.Encode(cfg)
if err != nil {
return ""
}
return prefix + result.String()
}
// unsupported format
return ""
}
func (cfg *Config) BackupAndMigrate() {
cfg.Backup()
cfg.Migrate()
cfg.Write(cfg.Format)
}
func (cfg *Config) Write(format string) {
content := cfg.Export(format)
if len(content) == 0 {
// we are unable to perform the export
os.Exit(65)
return
}
destination := cfg.Output
if len(destination) == 0 {
destination = cfg.origin
}
f, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return
}
defer func() {
_ = f.Close()
}()
_, err = f.WriteString(content)
if err != nil {
return
}
}
func (cfg *Config) Backup() {
dst := cfg.origin + ".bak"
source, err := os.Open(cfg.origin)
if err != nil {
return
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return
}
defer destination.Close()
_, err = io.Copy(destination, source)
if err != nil {
return
}
}
func escapeGlyphs(s string, migrate bool) string {
shouldExclude := func(r rune) bool {
if r < 0x1000 { // Basic Multilingual Plane
return true
}
if r > 0x1F600 && r < 0x1F64F { // Emoticons
return true
}
if r > 0x1F300 && r < 0x1F5FF { // Misc Symbols and Pictographs
return true
}
if r > 0x1F680 && r < 0x1F6FF { // Transport and Map
return true
}
if r > 0x2600 && r < 0x26FF { // Misc symbols
return true
}
if r > 0x2700 && r < 0x27BF { // Dingbats
return true
}
if r > 0xFE00 && r < 0xFE0F { // Variation Selectors
return true
}
if r > 0x1F900 && r < 0x1F9FF { // Supplemental Symbols and Pictographs
return true
}
if r > 0x1F1E6 && r < 0x1F1FF { // Flags
return true
}
return false
}
var cp codePoints
var err error
if migrate {
cp, err = getGlyphCodePoints()
if err != nil {
migrate = false
}
}
var builder strings.Builder
for _, r := range s {
// exclude regular characters and emojis
if shouldExclude(r) {
builder.WriteRune(r)
continue
}
if migrate {
if val, OK := cp[uint64(r)]; OK {
r = rune(val)
}
}
if r > 0x10000 {
// calculate surrogate pairs
one := 0xd800 + (((r - 0x10000) >> 10) & 0x3ff)
two := 0xdc00 + ((r - 0x10000) & 0x3ff)
quoted := fmt.Sprintf("\\u%04x\\u%04x", one, two)
builder.WriteString(quoted)
continue
}
quoted := fmt.Sprintf("\\u%04x", r)
builder.WriteString(quoted)
}
return builder.String()
}
func defaultConfig(env platform.Environment, warning bool) *Config {
exitBackgroundTemplate := "{{ if gt .Code 0 }}p:red{{ end }}"
exitTemplate := " {{ if gt .Code 0 }}\uf00d{{ else }}\uf00c{{ end }} "
if warning {
exitBackgroundTemplate = "p:red"
exitTemplate = " CONFIG ERROR "
}
cfg := &Config{
Version: 2,
FinalSpace: true,
Blocks: []*Block{
{
Type: Prompt,
Alignment: Left,
Segments: []*Segment{
{
Type: SESSION,
Style: Diamond,
LeadingDiamond: "\ue0b6",
TrailingDiamond: "\ue0b0",
Background: "p:yellow",
Foreground: "p:black",
Template: " {{ if .SSHSession }}\ueba9 {{ end }}{{ .UserName }} ",
},
{
Type: PATH,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:orange",
Foreground: "p:white",
Properties: properties.Map{
properties.Style: "folder",
},
Template: " \uea83 {{ path .Path .Location }} ",
},
{
Type: GIT,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:green",
BackgroundTemplates: []string{
"{{ if or (.Working.Changed) (.Staging.Changed) }}p:yellow{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:red{{ end }}",
"{{ if gt .Ahead 0 }}#49416D{{ end }}",
"{{ if gt .Behind 0 }}#7A306C{{ end }}",
},
Foreground: "p:black",
ForegroundTemplates: []string{
"{{ if or (.Working.Changed) (.Staging.Changed) }}p:black{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:white{{ end }}",
"{{ if gt .Ahead 0 }}p:white{{ end }}",
},
Properties: properties.Map{
segments.BranchMaxLength: 25,
segments.FetchStatus: true,
segments.FetchUpstreamIcon: true,
},
Template: " {{ if .UpstreamURL }}{{ url .UpstreamIcon .UpstreamURL }} {{ end }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uf044 {{ .Working.String }}{{ end }}{{ if .Staging.Changed }} \uf046 {{ .Staging.String }}{{ end }} ", //nolint:lll
},
{
Type: ROOT,
Style: Powerline,
PowerlineSymbol: "\ue0b0",
Background: "p:yellow",
Foreground: "p:white",
Template: " \uf0e7 ",
},
{
Type: STATUS,
Style: Diamond,
LeadingDiamond: "<transparent,background>\ue0b0</>",
TrailingDiamond: "\ue0b4",
Background: "p:blue",
BackgroundTemplates: []string{
exitBackgroundTemplate,
},
Foreground: "p:white",
Properties: properties.Map{
properties.AlwaysEnabled: true,
},
Template: exitTemplate,
},
},
},
{
Type: RPrompt,
Segments: []*Segment{
{
Type: NODE,
Style: Plain,
Background: "transparent",
Foreground: "p:green",
Template: "\ue718 ",
Properties: properties.Map{
segments.HomeEnabled: false,
segments.FetchPackageManager: false,
segments.DisplayMode: "files",
},
},
{
Type: GOLANG,
Style: Plain,
Background: "transparent",
Foreground: "p:blue",
Template: "\ue626 ",
Properties: properties.Map{
properties.FetchVersion: false,
},
},
{
Type: PYTHON,
Style: Plain,
Background: "transparent",
Foreground: "p:yellow",
Template: "\ue235 ",
Properties: properties.Map{
properties.FetchVersion: false,
segments.DisplayMode: "files",
segments.FetchVirtualEnv: false,
},
},
{
Type: SHELL,
Style: Plain,
Background: "transparent",
Foreground: "p:white",
Template: "in <p:blue><b>{{ .Name }}</b></> ",
},
{
Type: TIME,
Style: Plain,
Background: "transparent",
Foreground: "p:white",
Template: "at <p:blue><b>{{ .CurrentDate | date \"15:04:05\" }}</b></>",
},
},
},
},
ConsoleTitleTemplate: "{{ .Shell }} in {{ .Folder }}",
Palette: terminal.Palette{
"black": "#262B44",
"blue": "#4B95E9",
"green": "#59C9A5",
"orange": "#F07623",
"red": "#D81E5B",
"white": "#E0DEF4",
"yellow": "#F3AE35",
},
SecondaryPrompt: &Segment{
Background: "transparent",
Foreground: "p:black",
Template: "<p:yellow,transparent>\ue0b6</><,p:yellow> > </><p:yellow,transparent>\ue0b0</> ",
},
TransientPrompt: &Segment{
Background: "transparent",
Foreground: "p:black",
Template: "<p:yellow,transparent>\ue0b6</><,p:yellow> {{ .Folder }} </><p:yellow,transparent>\ue0b0</> ",
},
Tooltips: []*Segment{
{
Type: AWS,
Style: Diamond,
LeadingDiamond: "\ue0b0",
TrailingDiamond: "\ue0b4",
Background: "p:orange",
Foreground: "p:white",
Template: " \ue7ad {{ .Profile }}{{ if .Region }}@{{ .Region }}{{ end }} ",
Properties: properties.Map{
properties.DisplayDefault: true,
},
Tips: []string{"aws"},
},
{
Type: AZ,
Style: Diamond,
LeadingDiamond: "\ue0b0",
TrailingDiamond: "\ue0b4",
Background: "p:blue",
Foreground: "p:white",
Template: " \uebd8 {{ .Name }} ",
Properties: properties.Map{
properties.DisplayDefault: true,
},
Tips: []string{"az"},
},
},
}
cfg.env = env
return cfg
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/log"
)
@ -21,14 +22,14 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
titleStartTime := time.Now()
e.Env.Debug("Segment: Title")
title := e.getTitleTemplateText()
consoleTitle := &Segment{
name: "ConsoleTitle",
nameLength: 12,
consoleTitle := &config.Segment{
Alias: "ConsoleTitle",
NameLength: 12,
Enabled: len(e.Config.ConsoleTitleTemplate) > 0,
text: title,
duration: time.Since(titleStartTime),
Text: title,
Duration: time.Since(titleStartTime),
}
largestSegmentNameLength := consoleTitle.nameLength
largestSegmentNameLength := consoleTitle.NameLength
// render prompt
e.write(log.Text("\nPrompt:\n\n").Green().Bold().Plain().String())
@ -36,14 +37,14 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
e.write(log.Text("\n\nSegments:\n\n").Green().Bold().Plain().String())
var segments []*Segment
var segments []*config.Segment
segments = append(segments, consoleTitle)
for _, block := range e.Config.Blocks {
for _, segment := range block.Segments {
segments = append(segments, segment)
if segment.nameLength > largestSegmentNameLength {
largestSegmentNameLength = segment.nameLength
if segment.NameLength > largestSegmentNameLength {
largestSegmentNameLength = segment.NameLength
}
}
}
@ -51,7 +52,7 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
// 22 is the color for false/true and 7 is the reset color
largestSegmentNameLength += 22 + 7
for _, segment := range segments {
duration := segment.duration.Milliseconds()
duration := segment.Duration.Milliseconds()
var active log.Text
if segment.Enabled {
active = log.Text("true").Yellow()
@ -65,11 +66,12 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Run duration:").Green().Bold().Plain(), time.Since(startTime)))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Cache path:").Green().Bold().Plain(), e.Env.CachePath()))
config := e.Env.Flags().Config
if len(config) == 0 {
config = "no --config set, using default built-in configuration"
cfg := e.Env.Flags().Config
if len(cfg) == 0 {
cfg = "no --config set, using default built-in configuration"
}
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Config path:").Green().Bold().Plain(), config))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Config path:").Green().Bold().Plain(), cfg))
e.write(log.Text("\nLogs:\n\n").Green().Bold().Plain().String())
e.write(e.Env.Logs())

View file

@ -3,7 +3,9 @@ package engine
import (
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
@ -14,7 +16,7 @@ var (
)
type Engine struct {
Config *Config
Config *config.Config
Env platform.Environment
Plain bool
@ -22,6 +24,9 @@ type Engine struct {
currentLineLength int
rprompt string
rpromptLength int
activeSegment *config.Segment
previousActiveSegment *config.Segment
}
func (e *Engine) write(text string) {
@ -175,13 +180,13 @@ func (e *Engine) getTitleTemplateText() string {
return ""
}
func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool {
defer e.patchPowerShellBleed()
// This is deprecated but we leave it in to not break configs
// It is encouraged to used "newline": true on block level
// rather than the standalone the linebreak block
if block.Type == LineBreak {
if block.Type == config.LineBreak {
// do not print a newline to avoid a leading space
// when we're printin the first primary prompt in
// the shell
@ -193,7 +198,7 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
// when in bash, for rprompt blocks we need to write plain
// and wrap in escaped mode or the prompt will not render correctly
if e.Env.Shell() == shell.BASH && block.Type == RPrompt {
if e.Env.Shell() == shell.BASH && block.Type == config.RPrompt {
block.InitPlain(e.Env, e.Config)
} else {
block.Init(e.Env)
@ -210,7 +215,7 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
e.newline()
}
text, length := block.RenderSegments()
text, length := e.renderBlockSegments(block)
// do not print anything when we don't have any text
if length == 0 {
@ -218,18 +223,18 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
}
switch block.Type { //nolint:exhaustive
case Prompt:
case config.Prompt:
if block.VerticalOffset != 0 {
e.write(terminal.ChangeLine(block.VerticalOffset))
}
if block.Alignment == Left {
if block.Alignment == config.Left {
e.currentLineLength += length
e.write(text)
return true
}
if block.Alignment != Right {
if block.Alignment != config.Right {
return false
}
@ -237,9 +242,9 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
// we can't print the right block as there's not enough room available
if !OK {
switch block.Overflow {
case Break:
case config.Break:
e.newline()
case Hide:
case config.Hide:
// make sure to fill if needed
if padText, OK := e.shouldFill(block.Filler, space, 0); OK {
e.write(padText)
@ -269,7 +274,7 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) bool {
prompt += text
e.write(prompt)
case RPrompt:
case config.RPrompt:
e.rprompt = text
e.rpromptLength = length
}
@ -281,7 +286,7 @@ func (e *Engine) patchPowerShellBleed() {
// when in PowerShell, we need to clear the line after the prompt
// to avoid the background being printed on the next line
// when at the end of the buffer.
// See https://github.com/JanDeDobbeleer/oh-my-posh/issues/65
// See https://githue.com/JanDeDobbeleer/oh-my-posh/issues/65
if e.Env.Shell() != shell.PWSH && e.Env.Shell() != shell.PWSH5 {
return
}
@ -293,3 +298,216 @@ func (e *Engine) patchPowerShellBleed() {
e.write(terminal.ClearAfter())
}
func (e *Engine) renderBlockSegments(block *config.Block) (string, int) {
e.filterSegments(block)
for i, segment := range block.Segments {
if colors, newCycle := cycle.Loop(); colors != nil {
cycle = &newCycle
segment.Colors = colors
}
if i == 0 && len(block.LeadingDiamond) > 0 {
segment.LeadingDiamond = block.LeadingDiamond
}
if i == len(block.Segments)-1 && len(block.TrailingDiamond) > 0 {
segment.TrailingDiamond = block.TrailingDiamond
}
e.setActiveSegment(segment)
e.renderActiveSegment()
}
e.writeSeparator(true)
return terminal.String()
}
func (e *Engine) filterSegments(block *config.Block) {
segments := make([]*config.Segment, 0)
for _, segment := range block.Segments {
if !segment.Enabled && segment.ResolveStyle() != config.Accordion {
continue
}
segments = append(segments, segment)
}
block.Segments = segments
}
func (e *Engine) setActiveSegment(segment *config.Segment) {
e.activeSegment = segment
terminal.Interactive = segment.Interactive
terminal.SetColors(segment.ResolveBackground(), segment.ResolveForeground())
}
func (e *Engine) renderActiveSegment() {
e.writeSeparator(false)
switch e.activeSegment.ResolveStyle() {
case config.Plain, config.Powerline:
terminal.Write(terminal.Background, terminal.Foreground, e.activeSegment.Text)
case config.Diamond:
background := terminal.Transparent
if e.previousActiveSegment != nil && e.previousActiveSegment.HasEmptyDiamondAtEnd() {
background = e.previousActiveSegment.ResolveBackground()
}
terminal.Write(background, terminal.Background, e.activeSegment.LeadingDiamond)
terminal.Write(terminal.Background, terminal.Foreground, e.activeSegment.Text)
case config.Accordion:
if e.activeSegment.Enabled {
terminal.Write(terminal.Background, terminal.Foreground, e.activeSegment.Text)
}
}
e.previousActiveSegment = e.activeSegment
terminal.SetParentColors(e.previousActiveSegment.ResolveBackground(), e.previousActiveSegment.ResolveForeground())
}
func (e *Engine) writeSeparator(final bool) {
isCurrentDiamond := e.activeSegment.ResolveStyle() == config.Diamond
if final && isCurrentDiamond {
terminal.Write(terminal.Transparent, terminal.Background, e.activeSegment.TrailingDiamond)
return
}
isPreviousDiamond := e.previousActiveSegment != nil && e.previousActiveSegment.ResolveStyle() == config.Diamond
if isPreviousDiamond {
e.adjustTrailingDiamondColorOverrides()
}
if isPreviousDiamond && isCurrentDiamond && len(e.activeSegment.LeadingDiamond) == 0 {
terminal.Write(terminal.Background, terminal.ParentBackground, e.previousActiveSegment.TrailingDiamond)
return
}
if isPreviousDiamond && len(e.previousActiveSegment.TrailingDiamond) > 0 {
terminal.Write(terminal.Transparent, terminal.ParentBackground, e.previousActiveSegment.TrailingDiamond)
}
isPowerline := e.activeSegment.IsPowerline()
shouldOverridePowerlineLeadingSymbol := func() bool {
if !isPowerline {
return false
}
if isPowerline && len(e.activeSegment.LeadingPowerlineSymbol) == 0 {
return false
}
if e.previousActiveSegment != nil && e.previousActiveSegment.IsPowerline() {
return false
}
return true
}
if shouldOverridePowerlineLeadingSymbol() {
terminal.Write(terminal.Transparent, terminal.Background, e.activeSegment.LeadingPowerlineSymbol)
return
}
resolvePowerlineSymbol := func() string {
if isPowerline {
return e.activeSegment.PowerlineSymbol
}
if e.previousActiveSegment != nil && e.previousActiveSegment.IsPowerline() {
return e.previousActiveSegment.PowerlineSymbol
}
return ""
}
symbol := resolvePowerlineSymbol()
if len(symbol) == 0 {
return
}
bgColor := terminal.Background
if final || !isPowerline {
bgColor = terminal.Transparent
}
if e.activeSegment.ResolveStyle() == config.Diamond && len(e.activeSegment.LeadingDiamond) == 0 {
bgColor = terminal.Background
}
if e.activeSegment.InvertPowerline {
terminal.Write(e.getPowerlineColor(), bgColor, symbol)
return
}
terminal.Write(bgColor, e.getPowerlineColor(), symbol)
}
func (e *Engine) getPowerlineColor() string {
if e.previousActiveSegment == nil {
return terminal.Transparent
}
if e.previousActiveSegment.ResolveStyle() == config.Diamond && len(e.previousActiveSegment.TrailingDiamond) == 0 {
return e.previousActiveSegment.ResolveBackground()
}
if e.activeSegment.ResolveStyle() == config.Diamond && len(e.activeSegment.LeadingDiamond) == 0 {
return e.previousActiveSegment.ResolveBackground()
}
if !e.previousActiveSegment.IsPowerline() {
return terminal.Transparent
}
return e.previousActiveSegment.ResolveBackground()
}
func (e *Engine) adjustTrailingDiamondColorOverrides() {
// as we now already adjusted the activeSegment, we need to change the value
// of background and foreground to parentBackground and parentForeground
// this will still break when using parentBackground and parentForeground as keywords
// in a trailing diamond, but let's fix that when it happens as it requires either a rewrite
// of the logic for diamonds or storing grandparents as well like one happy family.
if e.previousActiveSegment == nil || len(e.previousActiveSegment.TrailingDiamond) == 0 {
return
}
if !strings.Contains(e.previousActiveSegment.TrailingDiamond, terminal.Background) && !strings.Contains(e.previousActiveSegment.TrailingDiamond, terminal.Foreground) {
return
}
match := regex.FindNamedRegexMatch(terminal.AnchorRegex, e.previousActiveSegment.TrailingDiamond)
if len(match) == 0 {
return
}
adjustOverride := func(anchor, override string) {
newOverride := override
switch override {
case terminal.Foreground:
newOverride = terminal.ParentForeground
case terminal.Background:
newOverride = terminal.ParentBackground
}
if override == newOverride {
return
}
newAnchor := strings.Replace(match[terminal.ANCHOR], override, newOverride, 1)
e.previousActiveSegment.TrailingDiamond = strings.Replace(e.previousActiveSegment.TrailingDiamond, anchor, newAnchor, 1)
}
if len(match[terminal.BG]) > 0 {
adjustOverride(match[terminal.ANCHOR], match[terminal.BG])
}
if len(match[terminal.FG]) > 0 {
adjustOverride(match[terminal.ANCHOR], match[terminal.FG])
}
}

View file

@ -4,6 +4,7 @@ import (
"errors"
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/mock"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
@ -89,7 +90,7 @@ func TestPrintPWD(t *testing.T) {
engine := &Engine{
Env: env,
Config: &Config{
Config: &config.Config{
PWD: tc.Config,
OSC99: tc.OSC99,
},
@ -113,7 +114,7 @@ func engineRender() {
env.Init()
defer env.Close()
cfg := LoadConfig(env)
cfg := config.Load(env)
terminal.Init(shell.GENERIC)
terminal.BackgroundColor = shell.ConsoleBackgroundColor(env, cfg.TerminalBackground)
@ -189,7 +190,7 @@ func TestGetTitle(t *testing.T) {
terminal.Init(shell.GENERIC)
engine := &Engine{
Config: &Config{
Config: &config.Config{
ConsoleTitleTemplate: tc.Template,
},
Env: env,
@ -250,7 +251,7 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
terminal.Init(shell.GENERIC)
engine := &Engine{
Config: &Config{
Config: &config.Config{
ConsoleTitleTemplate: tc.Template,
},
Env: env,

View file

@ -1,61 +0,0 @@
package engine
import (
"context"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/platform/net"
)
type codePoints map[uint64]uint64
func getGlyphCodePoints() (codePoints, error) {
var codePoints = make(codePoints)
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cncl()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://ohmyposh.dev/codepoints.csv", nil)
if err != nil {
return codePoints, &ConnectionError{reason: err.Error()}
}
response, err := net.HTTPClient.Do(request)
if err != nil {
return codePoints, err
}
defer response.Body.Close()
bytes, err := io.ReadAll(response.Body)
if err != nil {
return codePoints, err
}
lines := strings.Split(string(bytes), "\n")
for _, line := range lines {
fields := strings.Split(line, ",")
if len(fields) < 2 {
continue
}
oldGlyph, err := strconv.ParseUint(fields[0], 16, 32)
if err != nil {
continue
}
newGlyph, err := strconv.ParseUint(fields[1], 16, 32)
if err != nil {
continue
}
codePoints[oldGlyph] = newGlyph
}
return codePoints, nil
}

View file

@ -1,16 +0,0 @@
package engine
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetCodePoints(t *testing.T) {
codepoints, err := getGlyphCodePoints()
if connectionError, ok := err.(*ConnectionError); ok {
t.Log(connectionError.Error())
return
}
assert.Equal(t, 1939, len(codepoints))
}

View file

@ -1,6 +1,7 @@
package engine
import (
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
@ -15,7 +16,7 @@ func New(flags *platform.Flags) *Engine {
}
env.Init()
cfg := LoadConfig(env)
cfg := config.Load(env)
if cfg.PatchPwshBleed {
patchPowerShellBleed(env.Shell(), flags)

View file

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
@ -49,7 +50,7 @@ func (e *Engine) Primary() string {
renderRPrompt = false
}
if block.Type == RPrompt && !renderRPrompt {
if block.Type == config.RPrompt && !renderRPrompt {
continue
}
@ -121,7 +122,7 @@ func (e *Engine) Primary() string {
func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context
e.Env.LoadTemplateCache()
var prompt *Segment
var prompt *config.Segment
switch promptType {
case Debug:
prompt = e.Config.DebugPrompt
@ -136,7 +137,7 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
}
if prompt == nil {
prompt = &Segment{}
prompt = &config.Segment{}
}
getTemplate := func(template string) string {
@ -214,9 +215,9 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
}
func (e *Engine) RPrompt() string {
filterRPromptBlock := func(blocks []*Block) *Block {
filterRPromptBlock := func(blocks []*config.Block) *config.Block {
for _, block := range blocks {
if block.Type == RPrompt {
if block.Type == config.RPrompt {
return block
}
}
@ -233,7 +234,7 @@ func (e *Engine) RPrompt() string {
return ""
}
text, length := block.RenderSegments()
text, length := e.renderBlockSegments(block)
e.rpromptLength = length
return text
}

View file

@ -3,24 +3,23 @@ package engine
import (
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
func (e *Engine) Tooltip(tip string) string {
tip = strings.Trim(tip, " ")
tooltips := make([]*Segment, 0, 1)
tooltips := make([]*config.Segment, 0, 1)
for _, tooltip := range e.Config.Tooltips {
if !tooltip.shouldInvokeWithTip(tip) {
if !e.shouldInvokeWithTip(tooltip, tip) {
continue
}
if err := tooltip.mapSegmentWithWriter(e.Env); err != nil {
continue
}
tooltip.SetEnabled(e.Env)
if !tooltip.writer.Enabled() {
if !tooltip.Enabled {
continue
}
@ -32,8 +31,8 @@ func (e *Engine) Tooltip(tip string) string {
}
// little hack to reuse the current logic
block := &Block{
Alignment: Right,
block := &config.Block{
Alignment: config.Right,
Segments: tooltips,
}
@ -43,7 +42,7 @@ func (e *Engine) Tooltip(tip string) string {
if !block.Enabled() {
return ""
}
text, _ := block.RenderSegments()
text, _ := e.renderBlockSegments(block)
return text
case shell.PWSH, shell.PWSH5:
block.InitPlain(e.Env, e.Config)
@ -56,7 +55,7 @@ func (e *Engine) Tooltip(tip string) string {
return ""
}
text, length := block.RenderSegments()
text, length := e.renderBlockSegments(block)
space := consoleWidth - e.Env.Flags().Column - length
if space <= 0 {
@ -72,3 +71,13 @@ func (e *Engine) Tooltip(tip string) string {
return ""
}
func (e *Engine) shouldInvokeWithTip(segment *config.Segment, tip string) bool {
for _, t := range segment.Tips {
if t == tip {
return true
}
}
return false
}