From a8f246064ed886e8fdef61b724ca4e5bfa2200f1 Mon Sep 17 00:00:00 2001 From: "L. Yeung" Date: Tue, 17 Sep 2024 03:16:40 +0800 Subject: [PATCH] fix(shell): improve initialization For supported shells: - Correct string quoting. - Reorganize initialization scripts to improve maintainability. --- src/prompt/extra.go | 11 +- src/shell/bash.go | 48 +--- src/shell/bash_test.go | 15 ++ src/shell/cmd.go | 25 ++- src/shell/cmd_test.go | 20 +- src/shell/code.go | 7 + src/shell/elvish.go | 4 +- src/shell/fish.go | 26 +-- src/shell/fish_test.go | 19 +- src/shell/init.go | 12 +- src/shell/init_test.go | 88 -------- src/shell/nu.go | 6 +- src/shell/nu_test.go | 18 +- src/shell/pwsh.go | 5 +- src/shell/pwsh_test.go | 15 ++ src/shell/scripts/omp.bash | 79 ++++--- src/shell/scripts/omp.elv | 55 +++-- src/shell/scripts/omp.fish | 46 ++-- src/shell/scripts/omp.lua | 172 +++++++-------- src/shell/scripts/omp.nu | 55 +++-- src/shell/scripts/omp.ps1 | 437 +++++++++++++++++++------------------ src/shell/scripts/omp.py | 25 --- src/shell/scripts/omp.tcsh | 37 +++- src/shell/scripts/omp.xsh | 49 +++++ src/shell/scripts/omp.zsh | 66 +++--- src/shell/tcsh.go | 17 +- src/shell/tcsh_test.go | 19 +- src/shell/xonsh.go | 20 +- src/shell/xonsh_test.go | 19 +- src/shell/zsh.go | 9 +- src/shell/zsh_test.go | 4 +- src/terminal/writer.go | 37 ++-- 32 files changed, 784 insertions(+), 681 deletions(-) delete mode 100644 src/shell/init_test.go delete mode 100644 src/shell/scripts/omp.py create mode 100644 src/shell/scripts/omp.xsh diff --git a/src/prompt/extra.go b/src/prompt/extra.go index 3efd077f..8dc8e984 100644 --- a/src/prompt/extra.go +++ b/src/prompt/extra.go @@ -101,23 +101,22 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string { switch e.Env.Shell() { case shell.ZSH: if promptType == Transient { + if !e.Env.Flags().Eval { + break + } + prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(str)) // empty RPROMPT prompt += "\nRPROMPT=''" return prompt } - return str case shell.PWSH, shell.PWSH5: if promptType == Transient { // clear the line afterwards to prevent text from being written on the same line // see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628 return str + terminal.ClearAfter() } - - return str - case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC: - return str } - return "" + return str } diff --git a/src/shell/bash.go b/src/shell/bash.go index 48dc55cc..7c8088b2 100644 --- a/src/shell/bash.go +++ b/src/shell/bash.go @@ -2,7 +2,6 @@ package shell import ( _ "embed" - "fmt" "strings" ) @@ -13,13 +12,13 @@ var bashInit string func (f Feature) Bash() Code { switch f { case CursorPositioning: - return "_omp_cursor_positioning=1" + return unixCursorPositioning case FTCSMarks: - return "_omp_ftcs_marks=1" + return unixFTCSMarks case Upgrade: - return `"$_omp_executable" upgrade` + return unixUpgrade case Notice: - return `"$_omp_executable" notice` + return unixNotice case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient: fallthrough default: @@ -32,42 +31,5 @@ func QuotePosixStr(str string) string { return "''" } - needQuoting := false - var b strings.Builder - for _, r := range str { - normal := false - switch r { - case '!', ';', '"', '(', ')', '[', ']', '{', '}', '$', '|', '&', '>', '<', '`', ' ', '#', '~', '*', '?', '=': - b.WriteRune(r) - case '\\', '\'': - b.WriteByte('\\') - b.WriteRune(r) - case '\a': - b.WriteString(`\a`) - case '\b': - b.WriteString(`\b`) - case '\f': - b.WriteString(`\f`) - case '\n': - b.WriteString(`\n`) - case '\r': - b.WriteString(`\r`) - case '\t': - b.WriteString(`\t`) - case '\v': - b.WriteString(`\v`) - default: - b.WriteRune(r) - normal = true - } - if !normal { - needQuoting = true - } - } - // the quoting form $'...' is used for a string contains any special characters - if needQuoting { - return fmt.Sprintf("$'%s'", b.String()) - } - - return b.String() + return fmt.Sprintf("$'%s'", strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(str)) } diff --git a/src/shell/bash_test.go b/src/shell/bash_test.go index d8dcafe8..845e5fad 100644 --- a/src/shell/bash_test.go +++ b/src/shell/bash_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -17,3 +18,17 @@ _omp_cursor_positioning=1` assert.Equal(t, want, got) } + +func TestQuotePosixStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `$'/tmp/"omp\'s dir"/oh-my-posh'`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `$'C:/tmp\\omp\'s dir/oh-my-posh.exe'`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, QuotePosixStr(tc.str), fmt.Sprintf("QuotePosixStr: %s", tc.str)) + } +} diff --git a/src/shell/cmd.go b/src/shell/cmd.go index e4dd5ed9..359d0cd8 100644 --- a/src/shell/cmd.go +++ b/src/shell/cmd.go @@ -2,8 +2,6 @@ package shell import ( _ "embed" - - "fmt" "strings" ) @@ -16,23 +14,32 @@ func (f Feature) Cmd() Code { return "transient_enabled = true" case RPrompt: return "rprompt_enabled = true" + case FTCSMarks: + return "ftcs_marks_enabled = true" case Tooltips: return "enable_tooltips()" case Upgrade: - return "os.execute(string.format('%s upgrade', omp_exe()))" + return `os.execute(string.format('"%s" upgrade', omp_executable))` case Notice: - return "os.execute(string.format('%s notice', omp_exe()))" - case PromptMark, PoshGit, Azure, LineError, Jobs, FTCSMarks, CursorPositioning: + return `os.execute(string.format('"%s" notice', omp_executable))` + case PromptMark, PoshGit, Azure, LineError, Jobs, CursorPositioning: fallthrough default: return "" } } -func quoteLuaStr(str string) string { +func escapeLuaStr(str string) string { if len(str) == 0 { - return "''" + return str } - - return fmt.Sprintf("'%s'", strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(str)) + // We only replace a minimal set of special characters with corresponding escape sequences, without adding surrounding quotes. + // That way the result can be later quoted with either single or double quotes in a Lua script. + return strings.NewReplacer( + `\`, `\\`, + "'", `\'`, + `"`, `\"`, + "\n", `\n`, + "\r", `\r`, + ).Replace(str) } diff --git a/src/shell/cmd_test.go b/src/shell/cmd_test.go index 7a267467..3e164e7b 100644 --- a/src/shell/cmd_test.go +++ b/src/shell/cmd_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -12,9 +13,24 @@ func TestCmdFeatures(t *testing.T) { want := `// these are the features enable_tooltips() transient_enabled = true -os.execute(string.format('%s upgrade', omp_exe())) -os.execute(string.format('%s notice', omp_exe())) +ftcs_marks_enabled = true +os.execute(string.format('"%s" upgrade', omp_executable)) +os.execute(string.format('"%s" notice', omp_executable)) rprompt_enabled = true` assert.Equal(t, want, got) } + +func TestEscapeLuaStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: ""}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `/tmp/\"omp\'s dir\"/oh-my-posh`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `C:/tmp\\omp\'s dir/oh-my-posh.exe`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, escapeLuaStr(tc.str), fmt.Sprintf("escapeLuaStr: %s", tc.str)) + } +} diff --git a/src/shell/code.go b/src/shell/code.go index 2dd5c2f8..e907dd8b 100644 --- a/src/shell/code.go +++ b/src/shell/code.go @@ -4,6 +4,13 @@ import "strings" type Code string +const ( + unixFTCSMarks Code = "_omp_ftcs_marks=1" + unixCursorPositioning Code = "_omp_cursor_positioning=1" + unixUpgrade Code = `"$_omp_executable" upgrade` + unixNotice Code = `"$_omp_executable" notice` +) + func (c Code) Indent(spaces int) Code { return Code(strings.Repeat(" ", spaces) + string(c)) } diff --git a/src/shell/elvish.go b/src/shell/elvish.go index 9a7b2198..ab23ff5f 100644 --- a/src/shell/elvish.go +++ b/src/shell/elvish.go @@ -10,9 +10,9 @@ var elvishInit string func (f Feature) Elvish() Code { switch f { case Upgrade: - return unixUpgrade + return "$_omp_executable upgrade" case Notice: - return unixNotice + return "$_omp_executable notice" case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, CursorPositioning, Tooltips, Transient, FTCSMarks: fallthrough default: diff --git a/src/shell/fish.go b/src/shell/fish.go index 4c8e642d..5212848c 100644 --- a/src/shell/fish.go +++ b/src/shell/fish.go @@ -2,7 +2,6 @@ package shell import ( _ "embed" - "fmt" "strings" ) @@ -35,27 +34,6 @@ func quoteFishStr(str string) string { if len(str) == 0 { return "''" } - needQuoting := false - var b strings.Builder - for _, r := range str { - normal := false - switch r { - case ';', '"', '(', ')', '[', ']', '{', '}', '$', '|', '&', '>', '<', ' ', '#', '~', '*', '?', '=': - b.WriteRune(r) - case '\\', '\'': - b.WriteByte('\\') - b.WriteRune(r) - default: - b.WriteRune(r) - normal = true - } - if !normal { - needQuoting = true - } - } - // single quotes are used when the string contains any special characters - if needQuoting { - return fmt.Sprintf("'%s'", b.String()) - } - return b.String() + + return fmt.Sprintf("'%s'", strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(str)) } diff --git a/src/shell/fish_test.go b/src/shell/fish_test.go index fcadce44..c4798f51 100644 --- a/src/shell/fish_test.go +++ b/src/shell/fish_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -13,9 +14,23 @@ func TestFishFeatures(t *testing.T) { enable_poshtooltips set --global _omp_transient_prompt 1 set --global _omp_ftcs_marks 1 -$_omp_executable upgrade -$_omp_executable notice +"$_omp_executable" upgrade +"$_omp_executable" notice set --global _omp_prompt_mark 1` assert.Equal(t, want, got) } + +func TestQuoteFishStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `'/tmp/"omp\'s dir"/oh-my-posh'`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `'C:/tmp\\omp\'s dir/oh-my-posh.exe'`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, quoteFishStr(tc.str), fmt.Sprintf("quoteFishStr: %s", tc.str)) + } +} diff --git a/src/shell/init.go b/src/shell/init.go index 79d342d6..faa51f26 100644 --- a/src/shell/init.go +++ b/src/shell/init.go @@ -72,7 +72,7 @@ func Init(env runtime.Environment, feats Features) string { createNuInit(env, feats) return "" default: - return fmt.Sprintf("echo \"No initialization script available for %s\"", shell) + return fmt.Sprintf(`echo "%s is not supported by Oh My Posh"`, shell) } } @@ -105,22 +105,24 @@ func PrintInit(env runtime.Environment, features Features, startTime *time.Time) configFile = quoteFishStr(configFile) script = fishInit case CMD: - executable = quoteLuaStr(executable) - configFile = quoteLuaStr(configFile) + executable = escapeLuaStr(executable) + configFile = escapeLuaStr(configFile) script = cmdInit case NU: executable = quoteNuStr(executable) configFile = quoteNuStr(configFile) script = nuInit case TCSH: - executable = QuotePosixStr(executable) - configFile = QuotePosixStr(configFile) + executable = quoteCshStr(executable) + configFile = quoteCshStr(configFile) script = tcshInit case ELVISH: executable = quotePwshOrElvishStr(executable) configFile = quotePwshOrElvishStr(configFile) script = elvishInit case XONSH: + executable = quotePythonStr(executable) + configFile = quotePythonStr(configFile) script = xonshInit default: return fmt.Sprintf("echo \"No initialization script available for %s\"", shell) diff --git a/src/shell/init_test.go b/src/shell/init_test.go deleted file mode 100644 index de7b951d..00000000 --- a/src/shell/init_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package shell - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestQuotePwshOrElvishStr(t *testing.T) { - tests := []struct { - str string - expected string - }{ - {str: ``, expected: `''`}, - {str: `/tmp/oh-my-posh`, expected: `'/tmp/oh-my-posh'`}, - {str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp''s dir/oh-my-posh'`}, - {str: `C:\tmp\oh-my-posh.exe`, expected: `'C:\tmp\oh-my-posh.exe'`}, - {str: `C:\tmp\omp's dir\oh-my-posh.exe`, expected: `'C:\tmp\omp''s dir\oh-my-posh.exe'`}, - } - for _, tc := range tests { - assert.Equal(t, tc.expected, quotePwshOrElvishStr(tc.str), fmt.Sprintf("quotePwshStr: %s", tc.str)) - } -} - -func TestQuotePosixStr(t *testing.T) { - tests := []struct { - str string - expected string - }{ - {str: ``, expected: `''`}, - {str: `/tmp/oh-my-posh`, expected: `/tmp/oh-my-posh`}, - {str: `/tmp/omp's dir/oh-my-posh`, expected: `$'/tmp/omp\'s dir/oh-my-posh'`}, - {str: `C:/tmp/oh-my-posh.exe`, expected: `C:/tmp/oh-my-posh.exe`}, - {str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `$'C:/tmp/omp\'s dir/oh-my-posh.exe'`}, - } - for _, tc := range tests { - assert.Equal(t, tc.expected, QuotePosixStr(tc.str), fmt.Sprintf("quotePosixStr: %s", tc.str)) - } -} - -func TestQuoteFishStr(t *testing.T) { - tests := []struct { - str string - expected string - }{ - {str: ``, expected: `''`}, - {str: `/tmp/oh-my-posh`, expected: `/tmp/oh-my-posh`}, - {str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp\'s dir/oh-my-posh'`}, - {str: `C:/tmp/oh-my-posh.exe`, expected: `C:/tmp/oh-my-posh.exe`}, - {str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `'C:/tmp/omp\'s dir/oh-my-posh.exe'`}, - } - for _, tc := range tests { - assert.Equal(t, tc.expected, quoteFishStr(tc.str), fmt.Sprintf("quoteFishStr: %s", tc.str)) - } -} - -func TestQuoteLuaStr(t *testing.T) { - tests := []struct { - str string - expected string - }{ - {str: ``, expected: `''`}, - {str: `/tmp/oh-my-posh`, expected: `'/tmp/oh-my-posh'`}, - {str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp\'s dir/oh-my-posh'`}, - {str: `C:/tmp/oh-my-posh.exe`, expected: `'C:/tmp/oh-my-posh.exe'`}, - {str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `'C:/tmp/omp\'s dir/oh-my-posh.exe'`}, - } - for _, tc := range tests { - assert.Equal(t, tc.expected, quoteLuaStr(tc.str), fmt.Sprintf("quoteLuaStr: %s", tc.str)) - } -} - -func TestQuoteNuStr(t *testing.T) { - tests := []struct { - str string - expected string - }{ - {str: ``, expected: `''`}, - {str: `/tmp/oh-my-posh`, expected: `"/tmp/oh-my-posh"`}, - {str: `/tmp/omp's dir/oh-my-posh`, expected: `"/tmp/omp's dir/oh-my-posh"`}, - {str: `C:/tmp/oh-my-posh.exe`, expected: `"C:/tmp/oh-my-posh.exe"`}, - {str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `"C:/tmp/omp's dir/oh-my-posh.exe"`}, - } - for _, tc := range tests { - assert.Equal(t, tc.expected, quoteNuStr(tc.str), fmt.Sprintf("quoteNuStr: %s", tc.str)) - } -} diff --git a/src/shell/nu.go b/src/shell/nu.go index 40a5191a..7f84e261 100644 --- a/src/shell/nu.go +++ b/src/shell/nu.go @@ -2,7 +2,6 @@ package shell import ( _ "embed" - "fmt" "os" "path/filepath" @@ -17,7 +16,7 @@ var nuInit string func (f Feature) Nu() Code { switch f { case Transient: - return `$env.TRANSIENT_PROMPT_COMMAND = { ^$_omp_executable print transient $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.POSH_SHELL_VERSION)" $"--execution-time=(posh_cmd_duration)" $"--status=($env.LAST_EXIT_CODE)" $"--terminal-width=(posh_width)" }` //nolint: lll + return `$env.TRANSIENT_PROMPT_COMMAND = {|| _omp_get_prompt transient }` case Upgrade: return "^$_omp_executable upgrade" case Notice: @@ -33,12 +32,13 @@ func quoteNuStr(str string) string { if len(str) == 0 { return "''" } + return fmt.Sprintf(`"%s"`, strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(str)) } func createNuInit(env runtime.Environment, features Features) { initPath := filepath.Join(env.Home(), ".oh-my-posh.nu") - f, err := os.OpenFile(initPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + f, err := os.OpenFile(initPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { return } diff --git a/src/shell/nu_test.go b/src/shell/nu_test.go index d4d3b786..af932a6c 100644 --- a/src/shell/nu_test.go +++ b/src/shell/nu_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -9,11 +10,24 @@ import ( func TestNuFeatures(t *testing.T) { got := allFeatures.Lines(NU).String("// these are the features") - //nolint: lll want := `// these are the features -$env.TRANSIENT_PROMPT_COMMAND = { ^$_omp_executable print transient $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.POSH_SHELL_VERSION)" $"--execution-time=(posh_cmd_duration)" $"--status=($env.LAST_EXIT_CODE)" $"--terminal-width=(posh_width)" } +$env.TRANSIENT_PROMPT_COMMAND = {|| _omp_get_prompt transient } ^$_omp_executable upgrade ^$_omp_executable notice` assert.Equal(t, want, got) } + +func TestQuoteNuStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `"/tmp/\"omp's dir\"/oh-my-posh"`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `"C:/tmp\\omp's dir/oh-my-posh.exe"`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, quoteNuStr(tc.str), fmt.Sprintf("quoteNuStr: %s", tc.str)) + } +} diff --git a/src/shell/pwsh.go b/src/shell/pwsh.go index 13d578c8..a3d618ed 100644 --- a/src/shell/pwsh.go +++ b/src/shell/pwsh.go @@ -2,7 +2,6 @@ package shell import ( _ "embed" - "fmt" "strings" ) @@ -38,5 +37,9 @@ func (f Feature) Pwsh() Code { } func quotePwshOrElvishStr(str string) string { + if len(str) == 0 { + return "''" + } + return fmt.Sprintf("'%s'", strings.ReplaceAll(str, "'", "''")) } diff --git a/src/shell/pwsh_test.go b/src/shell/pwsh_test.go index 8b8cc77c..b89ab280 100644 --- a/src/shell/pwsh_test.go +++ b/src/shell/pwsh_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -24,3 +25,17 @@ $global:_ompFTCSMarks = $true assert.Equal(t, want, got) } + +func TestQuotePwshOrElvishStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `'/tmp/"omp''s dir"/oh-my-posh'`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `'C:/tmp\omp''s dir/oh-my-posh.exe'`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, quotePwshOrElvishStr(tc.str), fmt.Sprintf("quotePwshOrElvishStr: %s", tc.str)) + } +} diff --git a/src/shell/scripts/omp.bash b/src/shell/scripts/omp.bash index f80379a3..1003bd8c 100644 --- a/src/shell/scripts/omp.bash +++ b/src/shell/scripts/omp.bash @@ -1,6 +1,6 @@ export POSH_THEME=::CONFIG:: export POSH_SHELL_VERSION=$BASH_VERSION -export POWERLINE_COMMAND="oh-my-posh" +export POWERLINE_COMMAND='oh-my-posh' export POSH_PID=$$ export CONDA_PROMPT_MODIFIER=false export OSTYPE=$OSTYPE @@ -10,12 +10,12 @@ if [[ $OSTYPE =~ ^(msys|cygwin) ]]; then fi # global variables -_omp_start_time="" +_omp_start_time='' _omp_stack_count=0 -_omp_elapsed=-1 -_omp_no_exit_code="true" -_omp_status_cache=0 -_omp_pipestatus_cache=0 +_omp_execution_time=-1 +_omp_no_status=true +_omp_status=0 +_omp_pipestatus=0 _omp_executable=::OMP:: # switches to enable/disable features @@ -26,7 +26,11 @@ _omp_ftcs_marks=0 PS0='${_omp_start_time:0:$((_omp_start_time="$(_omp_start_timer)",0))}$(_omp_ftcs_command_start)' # set secondary prompt -_omp_secondary_prompt=$("$_omp_executable" print secondary --shell=bash --shell-version="$BASH_VERSION") +_omp_secondary_prompt=$( + "$_omp_executable" print secondary \ + --shell=bash \ + --shell-version="$BASH_VERSION" +) function _omp_set_cursor_position() { # not supported in Midnight Commander @@ -54,7 +58,7 @@ function _omp_start_timer() { function _omp_ftcs_command_start() { if [[ $_omp_ftcs_marks == 1 ]]; then - printf "\e]133;C\a" + printf '\e]133;C\a' fi } @@ -63,26 +67,36 @@ function set_poshcontext() { return } -function _omp_print_primary() { - # Avoid unexpected expansions. +function _omp_get_primary() { + # Avoid unexpected expansions when we're generating the prompt below. shopt -u promptvars + trap 'shopt -s promptvars' RETURN local prompt if shopt -oq posix; then # Disable in POSIX mode. prompt='[NOTICE: Oh My Posh prompt is not supported in POSIX mode]\n\u@\h:\w\$ ' else - prompt=$("$_omp_executable" print primary --shell=bash --shell-version="$BASH_VERSION" --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --no-status="$_omp_no_exit_code" --terminal-width="${COLUMNS-0}" | tr -d '\0') + prompt=$( + "$_omp_executable" print primary \ + --shell=bash \ + --shell-version="$BASH_VERSION" \ + --status="$_omp_status" \ + --pipestatus="${_omp_pipestatus[*]}" \ + --no-status="$_omp_no_status" \ + --execution-time="$_omp_execution_time" \ + --stack-count="$_omp_stack_count" \ + --terminal-width="${COLUMNS-0}" | + tr -d '\0' + ) fi echo "${prompt@P}" - - # Allow command substitution in PS0. - shopt -s promptvars } -function _omp_print_secondary() { - # Avoid unexpected expansions. +function _omp_get_secondary() { + # Avoid unexpected expansions when we're generating the prompt below. shopt -u promptvars + trap 'shopt -s promptvars' RETURN if shopt -oq posix; then # Disable in POSIX mode. @@ -90,38 +104,39 @@ function _omp_print_secondary() { else echo "${_omp_secondary_prompt@P}" fi - - # Allow command substitution in PS0. - shopt -s promptvars } function _omp_hook() { - _omp_status_cache=$? _omp_pipestatus_cache=("${PIPESTATUS[@]}") + _omp_status=$? _omp_pipestatus=("${PIPESTATUS[@]}") - if [[ ${#BP_PIPESTATUS[@]} -ge ${#_omp_pipestatus_cache[@]} ]]; then - _omp_pipestatus_cache=("${BP_PIPESTATUS[@]}") + if [[ ${#BP_PIPESTATUS[@]} -ge ${#_omp_pipestatus[@]} ]]; then + _omp_pipestatus=("${BP_PIPESTATUS[@]}") fi _omp_stack_count=$((${#DIRSTACK[@]} - 1)) + _omp_execution_time=-1 if [[ $_omp_start_time ]]; then - local omp_now=$("$_omp_executable" get millis --shell=bash) - _omp_elapsed=$((omp_now - _omp_start_time)) - _omp_start_time="" - _omp_no_exit_code="false" + local omp_now=$("$_omp_executable" get millis) + _omp_execution_time=$((omp_now - _omp_start_time)) + _omp_no_status=false fi + _omp_start_time='' - if [[ ${_omp_pipestatus_cache[-1]} != "$_omp_status_cache" ]]; then - _omp_pipestatus_cache=("$_omp_status_cache") + if [[ ${_omp_pipestatus[-1]} != "$_omp_status" ]]; then + _omp_pipestatus=("$_omp_status") fi set_poshcontext _omp_set_cursor_position - PS1='$(_omp_print_primary)' - PS2='$(_omp_print_secondary)' + PS1='$(_omp_get_primary)' + PS2='$(_omp_get_secondary)' - return $_omp_status_cache + # Ensure that command substitution works in a prompt string. + shopt -s promptvars + + return $_omp_status } function _omp_install_hook() { @@ -129,7 +144,7 @@ function _omp_install_hook() { local cmd for cmd in "${PROMPT_COMMAND[@]}"; do - if [[ $cmd = "_omp_hook" ]]; then + if [[ $cmd = _omp_hook ]]; then return fi done diff --git a/src/shell/scripts/omp.elv b/src/shell/scripts/omp.elv index cba32dcf..1ab82228 100644 --- a/src/shell/scripts/omp.elv +++ b/src/shell/scripts/omp.elv @@ -1,33 +1,54 @@ set-env POSH_PID (to-string (randint 10000000000000 10000000000000000)) set-env POSH_THEME ::CONFIG:: -set-env POSH_SHELL_VERSION (elvish --version) -set-env POWERLINE_COMMAND 'oh-my-posh' +set-env POSH_SHELL_VERSION $version +set-env POWERLINE_COMMAND oh-my-posh -var _omp_error_code = 0 -var _omp_executable = ::OMP:: +var _omp_executable = (external ::OMP::) +var _omp_status = 0 +var _omp_no_status = 1 +var _omp_execution_time = -1 +var _omp_terminal_width = ($_omp_executable get width) + +fn _omp-after-readline-hook {|_| + set _omp_execution_time = -1 + + # Getting the terminal width can fail inside a prompt function, so we do this here. + set _omp_terminal_width = ($_omp_executable get width) +} + +fn _omp-after-command-hook {|m| + # The command execution time should not be available in the first prompt. + if (== $_omp_no_status 0) { + set _omp_execution_time = (printf %.0f (* $m[duration] 1000)) + } + + set _omp_no_status = 0 -fn posh-after-command-hook {|m| var error = $m[error] if (is $error $nil) { - set _omp_error_code = 0 + set _omp_status = 0 } else { try { - set _omp_error_code = $error[reason][exit-status] + set _omp_status = $error[reason][exit-status] } catch { # built-in commands don't have a status code. - set _omp_error_code = 1 + set _omp_status = 1 } } } -set edit:after-command = [ $@edit:after-command $posh-after-command-hook~ ] - -set edit:prompt = { - var cmd-duration = (printf "%.0f" (* $edit:command-duration 1000)) - (external $_omp_executable) print primary --shell=elvish --execution-time=$cmd-duration --status=$_omp_error_code --pwd=$pwd --shell-version=$E:POSH_SHELL_VERSION +fn _omp_get_prompt {|type @arguments| + $_omp_executable print $type ^ + --shell=elvish ^ + --shell-version=$E:POSH_SHELL_VERSION ^ + --status=$_omp_status ^ + --no-status=$_omp_no_status ^ + --execution-time=$_omp_execution_time ^ + --terminal-width=$_omp_terminal_width ^ + $@arguments } -set edit:rprompt = { - var cmd-duration = (printf "%.0f" (* $edit:command-duration 1000)) - (external $_omp_executable) print right --shell=elvish --execution-time=$cmd-duration --status=$_omp_error_code --pwd=$pwd --shell-version=$E:POSH_SHELL_VERSION -} +set edit:after-readline = [ $@edit:after-readline $_omp-after-readline-hook~ ] +set edit:after-command = [ $@edit:after-command $_omp-after-command-hook~ ] +set edit:prompt = {|| _omp_get_prompt primary } +set edit:rprompt = {|| _omp_get_prompt right } diff --git a/src/shell/scripts/omp.fish b/src/shell/scripts/omp.fish index ce64f6e5..54994647 100644 --- a/src/shell/scripts/omp.fish +++ b/src/shell/scripts/omp.fish @@ -24,42 +24,57 @@ function set_poshcontext return end +function _omp_get_prompt + if test (count $argv) -eq 0 + return + end + $_omp_executable print $argv[1] \ + --shell=fish \ + --shell-version=$FISH_VERSION \ + --status=$_omp_status \ + --pipestatus="$_omp_pipestatus" \ + --no-status=$_omp_no_status \ + --execution-time=$_omp_execution_time \ + --stack-count=$_omp_stack_count \ + $argv[2..] +end + # NOTE: Input function calls via `commandline --function` are put into a queue and will not be executed until an outer regular function returns. See https://fishshell.com/docs/current/cmds/commandline.html. function fish_prompt - set --local omp_status_cache_temp $status - set --local omp_pipestatus_cache_temp $pipestatus + set --local omp_status_temp $status + set --local omp_pipestatus_temp $pipestatus # clear from cursor to end of screen as # commandline --function repaint does not do this # see https://github.com/fish-shell/fish-shell/issues/8418 printf \e\[0J if test "$_omp_transient" = 1 - $_omp_executable print transient --shell fish --status $_omp_status_cache --pipestatus="$_omp_pipestatus_cache" --execution-time $_omp_duration --stack-count $_omp_stack_count --shell-version $FISH_VERSION --no-status=$_omp_no_exit_code + _omp_get_prompt transient return end if test "$_omp_new_prompt" = 0 echo -n "$_omp_current_prompt" return end - set --global _omp_status_cache $omp_status_cache_temp - set --global _omp_pipestatus_cache $omp_pipestatus_cache_temp + set --global _omp_status $omp_status_temp + set --global _omp_pipestatus $omp_pipestatus_temp + set --global _omp_no_status false + set --global _omp_execution_time "$CMD_DURATION$cmd_duration" set --global _omp_stack_count (count $dirstack) - set --global _omp_duration "$CMD_DURATION$cmd_duration" - set --global _omp_no_exit_code false # check if variable set, < 3.2 case if set --query _omp_last_command && test -z "$_omp_last_command" - set _omp_duration 0 - set _omp_no_exit_code true + set _omp_execution_time 0 + set _omp_no_status true end # works with fish >=3.2 if set --query _omp_last_status_generation && test "$_omp_last_status_generation" = "$status_generation" - set _omp_duration 0 - set _omp_no_exit_code true + set _omp_execution_time 0 + set _omp_no_status true else if test -z "$_omp_last_status_generation" # first execution - $status_generation is 0, $_omp_last_status_generation is empty - set _omp_no_exit_code true + set _omp_no_status true end if set --query status_generation @@ -81,7 +96,8 @@ function fish_prompt end # The prompt is saved for possible reuse, typically a repaint after clearing the screen buffer. - set --global _omp_current_prompt ($_omp_executable print primary --shell fish --status $_omp_status_cache --pipestatus="$_omp_pipestatus_cache" --execution-time $_omp_duration --stack-count $_omp_stack_count --shell-version $FISH_VERSION --cleared=$omp_cleared --no-status=$_omp_no_exit_code | string collect) + set --global _omp_current_prompt (_omp_get_prompt primary --cleared=$omp_cleared | string join \n | string collect) + echo -n "$_omp_current_prompt" end @@ -98,7 +114,7 @@ function fish_right_prompt end set _omp_new_prompt 0 - set --global _omp_current_rprompt ($_omp_executable print right --shell fish --status $_omp_status_cache --pipestatus="$_omp_pipestatus_cache" --execution-time $_omp_duration --stack-count $_omp_stack_count --shell-version $FISH_VERSION --no-status=$_omp_no_exit_code | string join '') + set --global _omp_current_rprompt (_omp_get_prompt right | string join '') echo -n "$_omp_current_rprompt" end @@ -154,7 +170,7 @@ function _omp_space_key_handler end set _omp_tooltip_command $tooltip_command - set --local tooltip_prompt ($_omp_executable print tooltip --shell fish --status $_omp_status_cache --pipestatus="$_omp_pipestatus_cache" --execution-time $_omp_duration --stack-count $_omp_stack_count --shell-version $FISH_VERSION --command $_omp_tooltip_command --no-status=$_omp_no_exit_code | string join '') + set --local tooltip_prompt (_omp_get_prompt tooltip --command=$_omp_tooltip_command | string join '') if test -z "$tooltip_prompt" return diff --git a/src/shell/scripts/omp.lua b/src/shell/scripts/omp.lua index 3cbea2f3..fbb4ad00 100644 --- a/src/shell/scripts/omp.lua +++ b/src/shell/scripts/omp.lua @@ -1,34 +1,24 @@ --- Upgrade notice - -local notice = [[::UPGRADENOTICE::]] - -if '::UPGRADE::' == 'true' then - print(notice) -end +---@diagnostic disable: undefined-global +---@diagnostic disable: undefined-field +---@diagnostic disable: lowercase-global -- Cache PID -os.setenv("POSH_PID", os.getpid()) +os.setenv('POSH_PID', os.getpid()) -- Helper functions local function get_priority_number(name, default) - local value = os.getenv(name) - if os.envmap ~= nil and type(os.envmap) == 'table' then - local t = os.envmap[name] - value = (t ~= nil and type(t) == 'string') and t or value - end - if type(default) == 'number' then - value = tonumber(value) - if value == nil then - return default - else - return value - end - else - return default - end + local value = os.getenv(name) + if value == nil and os.envmap ~= nil and type(os.envmap) == 'table' then + value = os.envmap[name] + end + local num = tonumber(value) + if num ~= nil then + return num + end + return default end -os.setenv("POSH_CURSOR_LINE", console.getnumlines()) + -- Environment variables local function environment_onbeginedit() @@ -41,6 +31,7 @@ local endedit_time = 0 local last_duration = 0 local rprompt_enabled = false local transient_enabled = false +local ftcs_marks_enabled = false local no_exit_code = true local cached_prompt = {} @@ -57,7 +48,7 @@ local function cache_onbeginedit() local old_cache = cached_prompt -- Start a new table for the new edit/prompt session. - cached_prompt = { cwd=cwd } + cached_prompt = { cwd = cwd } -- Copy the cached left/right prompt strings if the cwd hasn't changed. -- IMPORTANT OPTIMIZATION: This keeps the prompt highly responsive, except @@ -68,33 +59,30 @@ local function cache_onbeginedit() end end +-- Executable + +local omp_executable = '::OMP::' + -- Configuration -local function omp_exe() - return '"'..::OMP::..'"' -end - -os.setenv("POSH_THEME", ::CONFIG::) -os.setenv("POSH_SHELL_VERSION", string.format('clink v%s.%s.%s.%s', clink.version_major, clink.version_minor, clink.version_patch, clink.version_commit)) +os.setenv('POSH_THEME', '::CONFIG::') +os.setenv('POSH_SHELL_VERSION', string.format('clink v%s.%s.%s.%s', clink.version_major, clink.version_minor, clink.version_patch, clink.version_commit)) -- Execution helpers local function can_async() if (clink.version_encoded or 0) >= 10030001 then - return settings.get("prompt.async") + return settings.get('prompt.async') end end local function run_posh_command(command) - command = '"'..command..'"' - local _, ismain = coroutine.running() - local output - if ismain then - output = io.popen(command):read("*a") - else - output = io.popenyield(command):read("*a") + command = string.format('""%s" %s"', omp_executable, command) + local _, is_main = coroutine.running() + if is_main then + return io.popen(command):read('*a') end - return output + return io.popenyield(command):read('*a') end -- Duration functions @@ -105,10 +93,8 @@ local function os_clock_millis() -- OMP to get the time in milliseconds. if (clink.version_encoded or 0) >= 10020030 then return math.floor(os.clock() * 1000) - else - local prompt_exe = string.format('%s get millis --shell=cmd', omp_exe()) - return run_posh_command(prompt_exe) end + return run_posh_command('get millis') end local function duration_onbeginedit() @@ -125,7 +111,7 @@ end local function duration_onendedit(input) endedit_time = 0 -- For an empty command, the execution time should not be evaluated. - if string.gsub(input, "^%s*(.-)%s*$", "%1") ~= "" then + if string.gsub(input, '^%s*(.-)%s*$', '%1') ~= '' then endedit_time = os_clock_millis() end end @@ -134,43 +120,47 @@ end local function execution_time_option() if last_duration ~= nil then - return "--execution-time "..last_duration + return '--execution-time=' .. last_duration end - return "" + return '' end -local function error_level_option() - if os.geterrorlevel ~= nil and settings.get("cmd.get_errorlevel") then - return "--status "..os.geterrorlevel() +local function status_option() + if os.geterrorlevel ~= nil and settings.get('cmd.get_errorlevel') then + return '--status=' .. os.geterrorlevel() end - return "" + return '' end -local function no_exit_code_option() +local function no_status_option() if no_exit_code then - return "--no-status" + return '--no-status' end - return "" + return '' end -local function get_posh_prompt(rprompt) - local prompt = "primary" - if rprompt then - prompt = "right" - end - local prompt_exe = string.format('%s print %s --shell=cmd %s %s %s', omp_exe(), prompt, execution_time_option(), error_level_option(), no_exit_code_option()) - return run_posh_command(prompt_exe) +local function get_posh_prompt(prompt_type, ...) + os.setenv('POSH_CURSOR_LINE', console.getnumlines()) + local command = table.concat({ + 'print', + prompt_type, + '--shell=cmd', + status_option(), + no_status_option(), + execution_time_option(), + ... + }, ' ') + return run_posh_command(command) end local function set_posh_tooltip(tip_command) - if tip_command ~= "" and tip_command ~= cached_prompt.tip_command then + if tip_command ~= '' and tip_command ~= cached_prompt.tip_command then -- Escape special characters properly, if any. - local escaped_tip_command = string.gsub(tip_command, '(\\+)"', '%1%1"'):gsub('(\\+)$', '%1%1'):gsub('"', '\\"'):gsub('([&<>%(%)@%^|])', '^%1') - - local prompt_exe = string.format('%s print tooltip --shell=cmd %s --command="%s"', omp_exe(), error_level_option(), escaped_tip_command) - local tooltip = run_posh_command(prompt_exe) + local escaped_tip_command = string.gsub(tip_command, '(\\+)"', '%1%1"'):gsub('(\\+)$', '%1%1'):gsub('"', '\\"'):gsub('([&<>%(%)@|%^])', '^%1'):gsub('%%', '%%%%') + local command_option = string.format('--command "%s"', escaped_tip_command) + local tooltip = get_posh_prompt('tooltip', command_option) -- Do not cache an empty tooltip. - if tooltip == "" then + if tooltip == '' then return end cached_prompt.tip_command = tip_command @@ -185,25 +175,10 @@ local function display_cached_prompt() cached_prompt.only_use_cache = nil end -local function async_collect_posh_prompts() - -- Generate the left prompt. - cached_prompt.left = get_posh_prompt(false) - - -- Generate the right prompt, if needed. - if rprompt_enabled then - display_cached_prompt() -- Show left side; don't wait for right side. - cached_prompt.right = get_posh_prompt(true) - end -end - local function command_executed_mark(input) - if string.gsub(input, "^%s*(.-)%s*$", "%1") ~= "" then - no_exit_code = false - else - no_exit_code = true - end - if "::FTCS_MARKS::" == "true" then - clink.print("\x1b]133;C\007", NONL) + no_exit_code = string.gsub(input, '^%s*(.-)%s*$', '%1') == '' + if ftcs_marks_enabled then + clink.print('\x1b]133;C\007', NONL) end end @@ -216,7 +191,7 @@ function p:filter(prompt) -- Get a left prompt immediately if nothing is available yet. if not cached_prompt.left then - cached_prompt.left = get_posh_prompt(false) + cached_prompt.left = get_posh_prompt('primary') need_left = false end @@ -228,10 +203,10 @@ function p:filter(prompt) -- function was defined. That way if a new prompt starts (which -- discards the old coroutine) and a new coroutine starts, the old -- coroutine won't stomp on the new cached_prompt table. - clink.promptcoroutine(function () + clink.promptcoroutine(function() -- Generate left prompt, if needed. if need_left then - cached_prompt.left = get_posh_prompt(false) + cached_prompt.left = get_posh_prompt('primary') end -- Generate right prompt, if needed. if rprompt_enabled then @@ -239,17 +214,17 @@ function p:filter(prompt) -- Show left side while right side is being generated. display_cached_prompt() end - cached_prompt.right = get_posh_prompt(true) + cached_prompt.right = get_posh_prompt('right') else cached_prompt.right = nil end end) else if need_left then - cached_prompt.left = get_posh_prompt(false) + cached_prompt.left = get_posh_prompt('primary') end if rprompt_enabled then - cached_prompt.right = get_posh_prompt(true) + cached_prompt.right = get_posh_prompt('right') end end end @@ -269,10 +244,9 @@ function p:transientfilter(prompt) return nil end - local prompt_exe = string.format('%s print transient --shell=cmd %s %s', omp_exe(), error_level_option(), no_exit_code_option()) - prompt = run_posh_command(prompt_exe) + prompt = get_posh_prompt('transient') - if prompt == "" then + if prompt == '' then prompt = nil end @@ -280,7 +254,7 @@ function p:transientfilter(prompt) end function p:transientrightfilter(prompt) - return "", false + return '', false end -- Event handlers @@ -303,12 +277,12 @@ end -- Tooltips -function ohmyposh_space(rl_buffer) +function _omp_space_keybinding(rl_buffer) -- Insert space first, in case it might affect the tip word, e.g. it could -- split "gitcommit" into "git commit". - rl_buffer:insert(" ") + rl_buffer:insert(' ') -- Get the first word of command line as tip. - local tip_command = rl_buffer:getbuffer():gsub("^%s*([^%s]*).*$", "%1") + local tip_command = rl_buffer:getbuffer():gsub('^%s*(.-)%s*$', '%1') -- Generate a tooltip asynchronously (via coroutine) if available, otherwise -- generate a tooltip immediately. @@ -318,7 +292,7 @@ function ohmyposh_space(rl_buffer) elseif cached_prompt.coroutine then -- No action needed; a tooltip coroutine is already running. else - cached_prompt.coroutine = coroutine.create(function () + cached_prompt.coroutine = coroutine.create(function() set_posh_tooltip(tip_command) if cached_prompt.coroutine == coroutine.running() then cached_prompt.coroutine = nil @@ -333,5 +307,5 @@ local function enable_tooltips() return end - rl.setbinding(' ', [["luafunc:ohmyposh_space"]], 'emacs') + rl.setbinding(' ', [["luafunc:_omp_space_keybinding"]], 'emacs') end diff --git a/src/shell/scripts/omp.nu b/src/shell/scripts/omp.nu index 963ce897..e66bde0a 100644 --- a/src/shell/scripts/omp.nu +++ b/src/shell/scripts/omp.nu @@ -4,28 +4,47 @@ if ($env.config? | is-not-empty) { } $env.POWERLINE_COMMAND = 'oh-my-posh' -$env.POSH_THEME = ::CONFIG:: +$env.POSH_THEME = (echo ::CONFIG::) $env.PROMPT_INDICATOR = "" $env.POSH_PID = (random uuid) $env.POSH_SHELL_VERSION = (version | get version) -let _omp_executable: string = ::OMP:: - -def posh_cmd_duration [] { - # We have to do this because the initial value of `$env.CMD_DURATION_MS` is always `0823`, - # which is an official setting. - # See https://github.com/nushell/nushell/discussions/6402#discussioncomment-3466687. - if $env.CMD_DURATION_MS == "0823" { 0 } else { $env.CMD_DURATION_MS } -} - -def posh_width [] { - (term size).columns | into string -} +let _omp_executable: string = (echo ::OMP::) # PROMPTS -$env.PROMPT_MULTILINE_INDICATOR = (^$_omp_executable print secondary --shell=nu $"--shell-version=($env.POSH_SHELL_VERSION)") -$env.PROMPT_COMMAND = { || +def --wrapped _omp_get_prompt [ + type: string, + ...args: string +] { + mut execution_time = -1 + mut no_status = true + # We have to do this because the initial value of `$env.CMD_DURATION_MS` is always `0823`, which is an official setting. + # See https://github.com/nushell/nushell/discussions/6402#discussioncomment-3466687. + if $env.CMD_DURATION_MS != '0823' { + $execution_time = $env.CMD_DURATION_MS + $no_status = false + } + + ( + ^$_omp_executable print $type + --shell=nu + $"--shell-version=($env.POSH_SHELL_VERSION)" + $"--status=($env.LAST_EXIT_CODE)" + $"--no-status=($no_status)" + $"--execution-time=($execution_time)" + $"--terminal-width=((term size).columns)" + ...$args + ) +} + +$env.PROMPT_MULTILINE_INDICATOR = ( + ^$_omp_executable print secondary + --shell=nu + $"--shell-version=($env.POSH_SHELL_VERSION)" +) + +$env.PROMPT_COMMAND = {|| # hack to set the cursor line to 1 when the user clears the screen # this obviously isn't bulletproof, but it's a start mut clear = false @@ -37,9 +56,7 @@ $env.PROMPT_COMMAND = { || do --env $env.SET_POSHCONTEXT } - ^$_omp_executable print primary --shell=nu $"--shell-version=($env.POSH_SHELL_VERSION)" $"--execution-time=(posh_cmd_duration)" $"--status=($env.LAST_EXIT_CODE)" $"--terminal-width=(posh_width)" $"--cleared=($clear)" + _omp_get_prompt primary $"--cleared=($clear)" } -$env.PROMPT_COMMAND_RIGHT = { || - ^$_omp_executable print right --shell=nu $"--shell-version=($env.POSH_SHELL_VERSION)" $"--execution-time=(posh_cmd_duration)" $"--status=($env.LAST_EXIT_CODE)" $"--terminal-width=(posh_width)" -} +$env.PROMPT_COMMAND_RIGHT = {|| _omp_get_prompt right } diff --git a/src/shell/scripts/omp.ps1 b/src/shell/scripts/omp.ps1 index 3fe9c461..3de42794 100644 --- a/src/shell/scripts/omp.ps1 +++ b/src/shell/scripts/omp.ps1 @@ -48,28 +48,24 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $env:POSH_THEME = (Resolve-Path -Path ::CONFIG::).ProviderPath } - function Start-Utf8Process { - param( - [string]$FileName, - [string[]]$Arguments = @() - ) + function Invoke-Utf8Posh { + param([string[]]$Arguments = @()) if ($script:ConstrainedLanguageMode) { - $standardOut = Invoke-Expression "& `$FileName `$Arguments 2>&1" - $standardOut -join "`n" + $output = Invoke-Expression "& `$global:_ompExecutable `$Arguments 2>&1" + $output -join "`n" return } $Process = New-Object System.Diagnostics.Process $StartInfo = $Process.StartInfo - $StartInfo.FileName = $FileName + $StartInfo.FileName = $global:_ompExecutable if ($StartInfo.ArgumentList.Add) { # ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+) # ref-1: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0 # ref-2: https://docs.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.2#net-framework-vs-net-core $Arguments | ForEach-Object -Process { $StartInfo.ArgumentList.Add($_) } - } - else { + } else { # escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) $escapedArgs = $Arguments | ForEach-Object { # escape N consecutive backslash(es), which are followed by a double quote, to 2N consecutive ones @@ -113,8 +109,6 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $stdoutTask.Result } - function Set-PoshContext([bool]$originalStatus) {} - function Get-NonFSWD { # We only need to return a non-filesystem working directory. if ($PWD.Provider.Name -ne 'FileSystem') { @@ -131,6 +125,191 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $terminalWidth } + function Get-FileHyperlink { + param( + [Parameter(Mandatory, ValuefromPipeline = $True)] + [string]$Uri, + [Parameter(ValuefromPipeline = $True)] + [string]$Name + ) + + if (!$Name) { + # if name not set, uri is used as the name of the hyperlink + $Name = $Uri + } + + if ($null -ne $env:WSL_DISTRO_NAME) { + # wsl conversion if needed + $Uri = &wslpath -m $Uri + } + + # return an ANSI formatted hyperlink + return "`e]8;;file://$Uri`e\$Name`e]8;;`e\" + } + + function Set-TransientPrompt { + $previousOutputEncoding = [Console]::OutputEncoding + try { + $script:TransientPrompt = $true + [Console]::OutputEncoding = [Text.Encoding]::UTF8 + [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() + } finally { + [Console]::OutputEncoding = $previousOutputEncoding + } + } + + function Set-PoshPromptType { + if ($script:TransientPrompt -eq $true) { + $script:PromptType = "transient" + $script:TransientPrompt = $false + return + } + + # for details about the trick to detect a debugging context, see these comments: + # 1) https://github.com/JanDeDobbeleer/oh-my-posh/issues/2483#issuecomment-1175761456 + # 2) https://github.com/JanDeDobbeleer/oh-my-posh/issues/2502#issuecomment-1179968052 + # 3) https://github.com/JanDeDobbeleer/oh-my-posh/issues/5153 + if ($Host.Runspace.Debugger.InBreakpoint) { + $script:PromptType = "debug" + return + } + + $script:PromptType = "primary" + + if ($global:_ompJobCount) { + $script:JobCount = (Get-Job -State Running).Count + } + + if ($global:_ompAzure) { + try { + $env:POSH_AZURE_SUBSCRIPTION = Get-AzContext | ConvertTo-Json + } catch {} + } + + if ($global:_ompPoshGit) { + try { + $global:GitStatus = Get-GitStatus + $env:POSH_GIT_STATUS = $global:GitStatus | ConvertTo-Json + } catch {} + } + } + + function Update-PoshErrorCode { + $lastHistory = Get-History -ErrorAction Ignore -Count 1 + + # error code should be updated only when a non-empty command is run + if (($null -eq $lastHistory) -or ($script:LastHistoryId -eq $lastHistory.Id)) { + $script:ExecutionTime = 0 + $script:NoExitCode = $true + return + } + + $script:NoExitCode = $false + $script:LastHistoryId = $lastHistory.Id + $script:ExecutionTime = ($lastHistory.EndExecutionTime - $lastHistory.StartExecutionTime).TotalMilliseconds + if ($script:OriginalLastExecutionStatus) { + $script:ErrorCode = 0 + return + } + + $invocationInfo = try { + # retrieve info of the most recent error + $global:Error[0] | Where-Object { $_ -ne $null } | Select-Object -ExpandProperty InvocationInfo + } catch { $null } + + # check if the last command caused the last error + if ($null -ne $invocationInfo -and $lastHistory.CommandLine -eq $invocationInfo.Line) { + $script:ErrorCode = 1 + return + } + + if ($script:OriginalLastExitCode -is [int] -and $script:OriginalLastExitCode -ne 0) { + # native app exit code + $script:ErrorCode = $script:OriginalLastExitCode + return + } + } + + function Get-PoshPrompt { + param( + [string]$Type, + [string[]]$Arguments + ) + $nonFSWD = Get-NonFSWD + $stackCount = Get-PoshStackCount + $terminalWidth = Get-TerminalWidth + Invoke-Utf8Posh @( + "print", $Type + "--shell=$script:ShellName" + "--shell-version=$script:PSVersion" + "--status=$script:ErrorCode" + "--no-status=$script:NoExitCode" + "--execution-time=$script:ExecutionTime" + "--pswd=$nonFSWD" + "--stack-count=$stackCount" + "--terminal-width=$terminalWidth" + "--job-count=$script:JobCount" + if ($Arguments) { $Arguments } + ) + } + + $promptFunction = { + # store the orignal last command execution status + if ($global:NVS_ORIGINAL_LASTEXECUTIONSTATUS -is [bool]) { + # make it compatible with NVS auto-switching, if enabled + $script:OriginalLastExecutionStatus = $global:NVS_ORIGINAL_LASTEXECUTIONSTATUS + } else { + $script:OriginalLastExecutionStatus = $? + } + # store the orignal last exit code + $script:OriginalLastExitCode = $global:LASTEXITCODE + + Set-PoshPromptType + + if ($script:PromptType -ne 'transient') { + Update-PoshErrorCode + } + + Set-PoshContext $script:ErrorCode + + # set the cursor positions, they are zero based so align with other platforms + $env:POSH_CURSOR_LINE = $Host.UI.RawUI.CursorPosition.Y + 1 + $env:POSH_CURSOR_COLUMN = $Host.UI.RawUI.CursorPosition.X + 1 + + $output = Get-PoshPrompt $script:PromptType + # make sure PSReadLine knows if we have a multiline prompt + Set-PSReadLineOption -ExtraPromptLineCount (($output | Measure-Object -Line).Lines - 1) + + # The output can be multi-line, joining them ensures proper rendering. + $output = $output -join "`n" + + if ($script:PromptType -eq 'transient') { + # Workaround to prevent a command from eating the tail of a transient prompt, when we're at the end of the line. + $command = '' + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$command, [ref]$null) + if ($command) { + $output += " `b`b" + } + } + + $output + + # remove any posh-git status + $env:POSH_GIT_STATUS = $null + + # restore the orignal last exit code + $global:LASTEXITCODE = $script:OriginalLastExitCode + } + + $Function:prompt = $promptFunction + + # set secondary prompt + Set-PSReadLineOption -ContinuationPrompt ((Invoke-Utf8Posh @("print", "secondary", "--shell=$script:ShellName")) -join "`n") + + ### Exported Functions ### + + function Set-PoshContext([bool]$originalStatus) {} + function Enable-PoshTooltips { if ($script:ConstrainedLanguageMode) { return @@ -151,35 +330,21 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { } $script:TooltipCommand = $command - $column = $Host.UI.RawUI.CursorPosition.X - $terminalWidth = Get-TerminalWidth - $nonFSWD = Get-NonFSWD - $stackCount = global:Get-PoshStackCount - $standardOut = (Start-Utf8Process $global:_ompExecutable @("print", "tooltip", "--status=$script:ErrorCode", "--shell=$script:ShellName", "--pswd=$nonFSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--command=$command", "--shell-version=$script:PSVersion", "--column=$column", "--terminal-width=$terminalWidth", "--no-status=$script:NoExitCode", "--job-count=$script:JobCount")) -join '' - if (!$standardOut) { + $output = (Get-PoshPrompt "tooltip" @( + "--column=$($Host.UI.RawUI.CursorPosition.X)" + "--command=$command" + )) -join '' + if (!$output) { return } - Write-Host $standardOut -NoNewline + Write-Host $output -NoNewline # Workaround to prevent the text after cursor from disappearing when the tooltip is printed. [Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ') [Microsoft.PowerShell.PSConsoleReadLine]::Undo() - } - finally {} - } - } - - function Set-TransientPrompt { - $previousOutputEncoding = [Console]::OutputEncoding - try { - $script:TransientPrompt = $true - [Console]::OutputEncoding = [Text.Encoding]::UTF8 - [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() - } - finally { - [Console]::OutputEncoding = $previousOutputEncoding + } finally {} } } @@ -197,8 +362,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $script:TooltipCommand = '' Set-TransientPrompt } - } - finally { + } finally { [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() if ($global:_ompFTCSMarks -and $executingCommand) { # Write FTCS_COMMAND_EXECUTED after accepting the input - it should still happen before execution @@ -216,16 +380,15 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $script:TooltipCommand = '' Set-TransientPrompt } - } - finally { + } finally { [Microsoft.PowerShell.PSConsoleReadLine]::CopyOrCancelLine() } } } function Enable-PoshLineError { - $validLine = (Start-Utf8Process $global:_ompExecutable @("print", "valid", "--shell=$script:ShellName")) -join "`n" - $errorLine = (Start-Utf8Process $global:_ompExecutable @("print", "error", "--shell=$script:ShellName")) -join "`n" + $validLine = (Invoke-Utf8Posh @("print", "valid", "--shell=$script:ShellName")) -join "`n" + $errorLine = (Invoke-Utf8Posh @("print", "error", "--shell=$script:ShellName")) -join "`n" Set-PSReadLineOption -PromptText $validLine, $errorLine } @@ -264,40 +427,24 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { $Format = 'json' ) - $configString = Start-Utf8Process $global:_ompExecutable @("config", "export", "--format=$Format") - # if no path, copy to clipboard by default if ($FilePath) { # https://stackoverflow.com/questions/3038337/powershell-resolve-path-that-might-not-exist $FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) - [IO.File]::WriteAllLines($FilePath, $configString) } - else { - Set-Clipboard $configString - Write-Output "Theme copied to clipboard" - } - } - function Get-FileHyperlink { - param( - [Parameter(Mandatory, ValuefromPipeline = $True)] - [string]$uri, - [Parameter(ValuefromPipeline = $True)] - [string]$name + $output = Invoke-Utf8Posh @( + "config", "export" + "--format=$Format" + "--output=$FilePath" ) - - $esc = [char]27 - if (!$name) { - # if name not set, uri is used as the name of the hyperlink - $name = $uri + if (!$output) { + Write-Host "Theme exported to $(Get-FileHyperlink $FilePath)." + return } - if ($null -ne $env:WSL_DISTRO_NAME) { - # wsl conversion if needed - $uri = &wslpath -m $uri - } - - # return an ANSI formatted hyperlink - return "$esc]8;;file://$uri$esc\$name$esc]8;;$esc\" + # When no path is provided, copy the output to clipboard. + Set-Clipboard $output + Write-Host 'Theme copied to clipboard.' } function Get-PoshThemes { @@ -329,19 +476,28 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock { Write-Host $logo $themes = Get-ChildItem -Path "$Path/*" -Include '*.omp.json' | Sort-Object Name if ($List -eq $true) { - $themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders - } - else { + $themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -Uri $_.FullName } } | Format-Table -HideTableHeaders + } else { $nonFSWD = Get-NonFSWD + $stackCount = Get-PoshStackCount + $terminalWidth = Get-TerminalWidth $themes | ForEach-Object -Process { - Write-Host "Theme: $(Get-FileHyperlink -uri $_.FullName -Name ($_.BaseName -replace '\.omp$', ''))`n" - Start-Utf8Process $global:_ompExecutable @("print", "primary", "--config=$($_.FullName)", "--pswd=$nonFSWD", "--shell=$script:ShellName") + Write-Host "Theme: $(Get-FileHyperlink -Uri $_.FullName -Name ($_.BaseName -replace '\.omp$', ''))`n" + Invoke-Utf8Posh @( + "print", "primary" + "--config=$($_.FullName)" + "--shell=$script:ShellName" + "--shell-version=$script:PSVersion" + "--pswd=$nonFSWD" + "--stack-count=$stackCount" + "--terminal-width=$terminalWidth" + ) Write-Host "`n" } } Write-Host @" -Themes location: $(Get-FileHyperlink -uri "$Path") +Themes location: $(Get-FileHyperlink -Uri "$Path") To change your theme, adjust the init script in $PROFILE. Example: @@ -350,139 +506,6 @@ Example: "@ } - function Set-PoshPromptType { - if ($script:TransientPrompt -eq $true) { - $script:PromptType = "transient" - $script:TransientPrompt = $false - return - } - - # for details about the trick to detect a debugging context, see these comments: - # 1) https://github.com/JanDeDobbeleer/oh-my-posh/issues/2483#issuecomment-1175761456 - # 2) https://github.com/JanDeDobbeleer/oh-my-posh/issues/2502#issuecomment-1179968052 - # 3) https://github.com/JanDeDobbeleer/oh-my-posh/issues/5153 - if ($Host.Runspace.Debugger.InBreakpoint) { - $script:PromptType = "debug" - return - } - - $script:PromptType = "primary" - - if ($global:_ompJobCount) { - $script:JobCount = (Get-Job -State Running).Count - } - - if ($global:_ompAzure) { - try { - $env:POSH_AZURE_SUBSCRIPTION = Get-AzContext | ConvertTo-Json - } - catch {} - } - - if ($global:_ompPoshGit) { - try { - $global:GitStatus = Get-GitStatus - $env:POSH_GIT_STATUS = $global:GitStatus | ConvertTo-Json - } - catch {} - } - } - - function Update-PoshErrorCode { - $lastHistory = Get-History -ErrorAction Ignore -Count 1 - - # error code should be updated only when a non-empty command is run - if (($null -eq $lastHistory) -or ($script:LastHistoryId -eq $lastHistory.Id)) { - $script:ExecutionTime = 0 - $script:NoExitCode = $true - return - } - - $script:NoExitCode = $false - $script:LastHistoryId = $lastHistory.Id - $script:ExecutionTime = ($lastHistory.EndExecutionTime - $lastHistory.StartExecutionTime).TotalMilliseconds - if ($script:OriginalLastExecutionStatus) { - $script:ErrorCode = 0 - return - } - - $invocationInfo = try { - # retrieve info of the most recent error - $global:Error[0] | Where-Object { $_ -ne $null } | Select-Object -ExpandProperty InvocationInfo - } - catch { $null } - - # check if the last command caused the last error - if ($null -ne $invocationInfo -and $lastHistory.CommandLine -eq $invocationInfo.Line) { - $script:ErrorCode = 1 - return - } - - if ($script:OriginalLastExitCode -is [int] -and $script:OriginalLastExitCode -ne 0) { - # native app exit code - $script:ErrorCode = $script:OriginalLastExitCode - return - } - } - - $promptFunction = { - # store the orignal last command execution status - if ($global:NVS_ORIGINAL_LASTEXECUTIONSTATUS -is [bool]) { - # make it compatible with NVS auto-switching, if enabled - $script:OriginalLastExecutionStatus = $global:NVS_ORIGINAL_LASTEXECUTIONSTATUS - } - else { - $script:OriginalLastExecutionStatus = $? - } - # store the orignal last exit code - $script:OriginalLastExitCode = $global:LASTEXITCODE - - Set-PoshPromptType - - if ($script:PromptType -ne 'transient') { - Update-PoshErrorCode - } - - Set-PoshContext $script:ErrorCode - - $nonFSWD = Get-NonFSWD - $stackCount = global:Get-PoshStackCount - $terminalWidth = Get-TerminalWidth - - # set the cursor positions, they are zero based so align with other platforms - $env:POSH_CURSOR_LINE = $Host.UI.RawUI.CursorPosition.Y + 1 - $env:POSH_CURSOR_COLUMN = $Host.UI.RawUI.CursorPosition.X + 1 - - $standardOut = Start-Utf8Process $global:_ompExecutable @("print", $script:PromptType, "--status=$script:ErrorCode", "--pswd=$nonFSWD", "--execution-time=$script:ExecutionTime", "--stack-count=$stackCount", "--shell-version=$script:PSVersion", "--terminal-width=$terminalWidth", "--shell=$script:ShellName", "--no-status=$script:NoExitCode", "--job-count=$script:JobCount") - # make sure PSReadLine knows if we have a multiline prompt - Set-PSReadLineOption -ExtraPromptLineCount (($standardOut | Measure-Object -Line).Lines - 1) - - # The output can be multi-line, joining them ensures proper rendering. - $output = $standardOut -join "`n" - - if ($script:PromptType -eq 'transient') { - # Workaround to prevent a command from eating the tail of a transient prompt, when we're at the end of the line. - $command = '' - [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$command, [ref]$null) - if ($command) { - $output += " `b`b" - } - } - - $output - - # remove any posh-git status - $env:POSH_GIT_STATUS = $null - - # restore the orignal last exit code - $global:LASTEXITCODE = $script:OriginalLastExitCode - } - - $Function:prompt = $promptFunction - - # set secondary prompt - Set-PSReadLineOption -ContinuationPrompt ((Start-Utf8Process $global:_ompExecutable @("print", "secondary", "--shell=$script:ShellName")) -join "`n") - # perform cleanup on removal so a new initialization in current session works if (!$script:ConstrainedLanguageMode) { $ExecutionContext.SessionState.Module.OnRemove += { @@ -506,11 +529,6 @@ Example: } } - $notice = Start-Utf8Process $global:_ompExecutable @("notice") - if ($notice) { - Write-Host $notice -NoNewline - } - Export-ModuleMember -Function @( "Set-PoshContext" "Enable-PoshTooltips" @@ -518,7 +536,6 @@ Example: "Enable-PoshLineError" "Export-PoshTheme" "Get-PoshThemes" - "Start-Utf8Process" "prompt" ) } | Import-Module -Global diff --git a/src/shell/scripts/omp.py b/src/shell/scripts/omp.py deleted file mode 100644 index 3cc52837..00000000 --- a/src/shell/scripts/omp.py +++ /dev/null @@ -1,25 +0,0 @@ -import uuid - -$POWERLINE_COMMAND = "oh-my-posh" -$POSH_THEME = r"::CONFIG::" -$POSH_PID = uuid.uuid4().hex -$POSH_SHELL_VERSION = $XONSH_VERSION -$POSH_EXECUTABLE = r"::OMP::" - -def get_command_context(): - last_cmd = __xonsh__.history[-1] if __xonsh__.history else None - status = last_cmd.rtn if last_cmd else 0 - duration = round((last_cmd.ts[1] - last_cmd.ts[0]) * 1000) if last_cmd else 0 - return status, duration - -def posh_primary(): - status, duration = get_command_context() - return $(@($POSH_EXECUTABLE) print primary --shell=xonsh --status=@(status) --execution-time=@(duration) --shell-version=@($POSH_SHELL_VERSION)) - -def posh_right(): - status, duration = get_command_context() - return $(@($POSH_EXECUTABLE) print right --shell=xonsh --status=@(status) --execution-time=@(duration) --shell-version=@($POSH_SHELL_VERSION)) - - -$PROMPT = posh_primary -$RIGHT_PROMPT = posh_right diff --git a/src/shell/scripts/omp.tcsh b/src/shell/scripts/omp.tcsh index c105bc55..290e1e2b 100644 --- a/src/shell/scripts/omp.tcsh +++ b/src/shell/scripts/omp.tcsh @@ -1,20 +1,33 @@ setenv POWERLINE_COMMAND "oh-my-posh"; setenv POSH_THEME ::CONFIG::; -setenv POSH_SHELL_VERSION ""; +setenv POSH_SHELL_VERSION "$tcsh"; setenv POSH_PID $$; setenv OSTYPE "$OSTYPE"; -if ("$OSTYPE" =~ {msys,cygwin}*) then - setenv POSH_PID "`cat /proc/$$/winpid`"; -endif +if ( "$OSTYPE" =~ {msys,cygwin}* ) setenv POSH_PID "`cat /proc/$$/winpid`"; -set POSH_COMMAND = ::OMP::; -set USER_PRECMD = "`alias precmd`"; -set USER_POSTCMD = "`alias postcmd`"; -set POSH_PRECMD = 'set POSH_CMD_STATUS = $status;set POSH_END_TIME = `$POSH_COMMAND get millis`;set POSH_DURATION = 0;if ( $POSH_START_TIME != -1 ) @ POSH_DURATION = $POSH_END_TIME - $POSH_START_TIME;set prompt = "`$POSH_COMMAND print primary --shell=tcsh --status=$POSH_CMD_STATUS --execution-time=$POSH_DURATION`";set POSH_START_TIME = -1'; -set POSH_POSTCMD = 'set POSH_START_TIME = `$POSH_COMMAND get millis`'; +if ( ! $?_omp_enabled ) alias precmd ' + set _omp_status = $status; + set _omp_execution_time = -1; + set _omp_last_cmd = `echo $_:q`; + if ( $#_omp_last_cmd && $?_omp_cmd_executed ) @ _omp_execution_time = `"$_omp_executable" get millis` - $_omp_start_time; + unset _omp_last_cmd; + unset _omp_cmd_executed; + @ _omp_stack_count = $#dirstack - 1; + set prompt = "`$_omp_executable:q print primary + --shell=tcsh + --shell-version=$tcsh + --status=$_omp_status + --execution-time=$_omp_execution_time + --stack-count=$_omp_stack_count`"; +'"`alias precmd`"; -alias precmd "$POSH_PRECMD;$USER_PRECMD"; -alias postcmd "$POSH_POSTCMD;$USER_POSTCMD"; +if ( ! $?_omp_enabled ) alias postcmd ' + set _omp_start_time = `"$_omp_executable" get millis`; + set _omp_cmd_executed; +'"`alias postcmd`"; -set POSH_START_TIME = `$POSH_COMMAND get millis`; +set _omp_enabled; +set _omp_executable = ::OMP::; +set _omp_execution_time = -1; +set _omp_start_time = -1; diff --git a/src/shell/scripts/omp.xsh b/src/shell/scripts/omp.xsh new file mode 100644 index 00000000..a43f69ed --- /dev/null +++ b/src/shell/scripts/omp.xsh @@ -0,0 +1,49 @@ +import uuid + +$POWERLINE_COMMAND = "oh-my-posh" +$POSH_THEME = ::CONFIG:: +$POSH_PID = uuid.uuid4().hex +$POSH_SHELL_VERSION = $XONSH_VERSION + +_omp_executable = ::OMP:: +_omp_history_length = 0 + +def _omp_get_context(): + global _omp_history_length + status = 0 + duration = -1 + + if __xonsh__.history: + last_cmd = __xonsh__.history[-1] + if last_cmd: + status = last_cmd.rtn + + history_length = len(__xonsh__.history) + if history_length != _omp_history_length: + _omp_history_length = history_length + duration = round((last_cmd.ts[1] - last_cmd.ts[0]) * 1000) + + return status, duration + +def _omp_get_prompt(type: str, *args: str): + status, duration = _omp_get_context() + return $( + @(_omp_executable) print @(type) \ + --save-cache \ + --shell=xonsh \ + --shell-version=$XONSH_VERSION \ + --status=@(status) \ + --execution-time=@(duration) \ + @(args) + ) + +def _omp_get_primary(): + return _omp_get_prompt('primary') + +def _omp_get_right(): + return _omp_get_prompt('right') + +$PROMPT = _omp_get_primary +# When the primary prompt has multiple lines, the right prompt is always displayed on the first line, which is inconsistent with other supported shells. +# The behavior is controlled by Xonsh, and there is no way to change it. +$RIGHT_PROMPT = _omp_get_right diff --git a/src/shell/scripts/omp.zsh b/src/shell/scripts/omp.zsh index 89273307..d2d8d3f4 100644 --- a/src/shell/scripts/omp.zsh +++ b/src/shell/scripts/omp.zsh @@ -1,7 +1,7 @@ export POSH_THEME=::CONFIG:: export POSH_SHELL_VERSION=$ZSH_VERSION export POSH_PID=$$ -export POWERLINE_COMMAND="oh-my-posh" +export POWERLINE_COMMAND='oh-my-posh' export CONDA_PROMPT_MODIFIER=false export POSH_PROMPT_COUNT=0 export ZLE_RPROMPT_INDENT=0 @@ -12,6 +12,7 @@ if [[ $OSTYPE =~ ^(msys|cygwin) ]]; then fi _omp_executable=::OMP:: +_omp_tooltip_command='' # switches to enable/disable features _omp_cursor_positioning=0 @@ -31,7 +32,7 @@ function _omp_set_cursor_position() { stty raw -echo min 0 local pos - echo -en "\033[6n" >/dev/tty + echo -en '\033[6n' >/dev/tty read -r -d R pos pos=${pos:2} # strip off the esc-[ local parts=(${(s:;:)pos}) @@ -49,27 +50,28 @@ function set_poshcontext() { function _omp_preexec() { if [[ $_omp_ftcs_marks == 0 ]]; then - printf "\033]133;C\007" + printf '\033]133;C\007' fi _omp_start_time=$($_omp_executable get millis) } function _omp_precmd() { - _omp_status_cache=$? - _omp_pipestatus_cache=(${pipestatus[@]}) + _omp_status=$? + _omp_pipestatus=(${pipestatus[@]}) _omp_stack_count=${#dirstack[@]} - _omp_elapsed=-1 - _omp_no_exit_code="true" + _omp_execution_time=-1 + _omp_no_status=true + _omp_tooltip_command='' if [ $_omp_start_time ]; then - local omp_now=$($_omp_executable get millis --shell=zsh) - _omp_elapsed=$(($omp_now - $_omp_start_time)) - _omp_no_exit_code="false" + local omp_now=$($_omp_executable get millis) + _omp_execution_time=$(($omp_now - $_omp_start_time)) + _omp_no_status=false fi - if [[ ${_omp_pipestatus_cache[-1]} != "$_omp_status_cache" ]]; then - _omp_pipestatus_cache=("$_omp_status_cache") + if [[ ${_omp_pipestatus[-1]} != "$_omp_status" ]]; then + _omp_pipestatus=("$_omp_status") fi count=$((POSH_PROMPT_COUNT + 1)) @@ -86,8 +88,8 @@ function _omp_precmd() { setopt PROMPT_PERCENT PS2=$_omp_secondary_prompt + eval "$(_omp_get_prompt primary --eval)" - eval "$($_omp_executable print primary --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --eval --shell=zsh --shell-version="$ZSH_VERSION" --no-status="$_omp_no_exit_code")" unset _omp_start_time } @@ -116,6 +118,20 @@ function _omp_cleanup() { _omp_cleanup unset -f _omp_cleanup +function _omp_get_prompt() { + local type=$1 + local args=("${@[2,-1]}") + $_omp_executable print $type \ + --shell=zsh \ + --shell-version=$ZSH_VERSION \ + --status=$_omp_status \ + --pipestatus="${_omp_pipestatus[*]}" \ + --no-status=$_omp_no_status \ + --execution-time=$_omp_execution_time \ + --stack-count=$_omp_stack_count \ + ${args[@]} +} + function _omp_render_tooltip() { if [[ $KEYS != ' ' ]]; then return @@ -130,7 +146,7 @@ function _omp_render_tooltip() { fi _omp_tooltip_command="$tooltip_command" - local tooltip=$($_omp_executable print tooltip --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --command="$tooltip_command" --shell=zsh --shell-version="$ZSH_VERSION" --no-status="$_omp_no_exit_code") + local tooltip=$(_omp_get_prompt tooltip --command="$tooltip_command") if [[ -z $tooltip ]]; then return fi @@ -148,23 +164,21 @@ function _omp_zle-line-init() { local -i ret=$? (( $+zle_bracketed_paste )) && print -r -n - $zle_bracketed_paste[2] - _omp_tooltip_command='' - eval "$($_omp_executable print transient --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --eval --shell=zsh --shell-version="$ZSH_VERSION" --no-status="$_omp_no_exit_code")" + eval "$(_omp_get_prompt transient --eval)" zle .reset-prompt - # Exit the shell if we receive EOT. - if [[ $ret == 0 && $KEYS == $'\4' ]]; then - exit - fi - if ((ret)); then # TODO (fix): this is not equal to sending a SIGINT, since the status code ($?) is set to 1 instead of 130. zle .send-break - else - # Enter - zle .accept-line fi - return ret + + # Exit the shell if we receive EOT. + if [[ $KEYS == $'\4' ]]; then + exit + fi + + zle .accept-line + return $ret } # Helper function for calling a widget before the specified OMP function. @@ -205,7 +219,7 @@ function _omp_create_widget() { } function enable_poshtooltips() { - local widget=${$(bindkey " "):2} + local widget=${$(bindkey ' '):2} if [[ -z $widget ]]; then widget=self-insert diff --git a/src/shell/tcsh.go b/src/shell/tcsh.go index a6eac334..04a5ede8 100644 --- a/src/shell/tcsh.go +++ b/src/shell/tcsh.go @@ -2,6 +2,8 @@ package shell import ( _ "embed" + "fmt" + "strings" ) //go:embed scripts/omp.tcsh @@ -10,12 +12,23 @@ var tcshInit string func (f Feature) Tcsh() Code { switch f { case Upgrade: - return "$POSH_COMMAND upgrade;" + return `"$_omp_executable" upgrade;` case Notice: - return "$POSH_COMMAND notice;" + return `"$_omp_executable" notice;` case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, FTCSMarks, CursorPositioning: fallthrough default: return "" } } + +func quoteCshStr(str string) string { + if len(str) == 0 { + return "''" + } + + // An non-working edge case: there is no way to preserve a newline ('\n') in command substitution. + // Therefore, we can only get a limited string without newlines for "eval". + return fmt.Sprintf("'%s'", strings.NewReplacer("'", `'"'"'`, + "!", `\!`).Replace(str)) +} diff --git a/src/shell/tcsh_test.go b/src/shell/tcsh_test.go index 292b2c64..438e39e6 100644 --- a/src/shell/tcsh_test.go +++ b/src/shell/tcsh_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -10,8 +11,22 @@ func TestTcshFeatures(t *testing.T) { got := allFeatures.Lines(TCSH).String("// these are the features") want := `// these are the features -$POSH_COMMAND upgrade; -$POSH_COMMAND notice;` +"$_omp_executable" upgrade; +"$_omp_executable" notice;` assert.Equal(t, want, got) } + +func TestQuoteCshStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"!/oh-my-posh`, expected: `'/tmp/"omp'"'"'s dir"\!/oh-my-posh'`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `'C:/tmp\omp'"'"'s dir/oh-my-posh.exe'`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, quoteCshStr(tc.str), fmt.Sprintf("quoteCshStr: %s", tc.str)) + } +} diff --git a/src/shell/xonsh.go b/src/shell/xonsh.go index 6aaabfd4..083b0f68 100644 --- a/src/shell/xonsh.go +++ b/src/shell/xonsh.go @@ -2,20 +2,34 @@ package shell import ( _ "embed" + "fmt" + "strings" ) -//go:embed scripts/omp.py +//go:embed scripts/omp.xsh var xonshInit string func (f Feature) Xonsh() Code { switch f { case Upgrade: - return "@($POSH_EXECUTABLE) upgrade" + return "@(_omp_executable) upgrade" case Notice: - return "@($POSH_EXECUTABLE) notice" + return "@(_omp_executable) notice" case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, CursorPositioning, FTCSMarks: fallthrough default: return "" } } + +func quotePythonStr(str string) string { + if len(str) == 0 { + return "''" + } + + return fmt.Sprintf("'%s'", strings.NewReplacer( + "'", `'"'"'`, + `\`, `\\`, + "\n", `\n`, + ).Replace(str)) +} diff --git a/src/shell/xonsh_test.go b/src/shell/xonsh_test.go index 321f3fcc..4d8ce7d7 100644 --- a/src/shell/xonsh_test.go +++ b/src/shell/xonsh_test.go @@ -1,6 +1,7 @@ package shell import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -10,8 +11,22 @@ func TestXonshFeatures(t *testing.T) { got := allFeatures.Lines(XONSH).String("// these are the features") want := `// these are the features -@($POSH_EXECUTABLE) upgrade -@($POSH_EXECUTABLE) notice` +@(_omp_executable) upgrade +@(_omp_executable) notice` assert.Equal(t, want, got) } + +func TestQuotePythonStr(t *testing.T) { + tests := []struct { + str string + expected string + }{ + {str: "", expected: "''"}, + {str: `/tmp/"omp's dir"/oh-my-posh`, expected: `'/tmp/"omp'"'"'s dir"/oh-my-posh'`}, + {str: `C:/tmp\omp's dir/oh-my-posh.exe`, expected: `'C:/tmp\\omp'"'"'s dir/oh-my-posh.exe'`}, + } + for _, tc := range tests { + assert.Equal(t, tc.expected, quotePythonStr(tc.str), fmt.Sprintf("quotePythonStr: %s", tc.str)) + } +} diff --git a/src/shell/zsh.go b/src/shell/zsh.go index 443ef7ec..6262cdab 100644 --- a/src/shell/zsh.go +++ b/src/shell/zsh.go @@ -7,21 +7,16 @@ import ( //go:embed scripts/omp.zsh var zshInit string -const ( - unixUpgrade = "$_omp_executable upgrade" - unixNotice = "$_omp_executable notice" -) - func (f Feature) Zsh() Code { switch f { case CursorPositioning: - return "_omp_cursor_positioning=1" + return unixCursorPositioning case Tooltips: return "enable_poshtooltips" case Transient: return "_omp_create_widget zle-line-init _omp_zle-line-init" case FTCSMarks: - return "_omp_ftcs_marks=1" + return unixFTCSMarks case Upgrade: return unixUpgrade case Notice: diff --git a/src/shell/zsh_test.go b/src/shell/zsh_test.go index 592b6250..d98a8e91 100644 --- a/src/shell/zsh_test.go +++ b/src/shell/zsh_test.go @@ -13,8 +13,8 @@ func TestZshFeatures(t *testing.T) { enable_poshtooltips _omp_create_widget zle-line-init _omp_zle-line-init _omp_ftcs_marks=1 -$_omp_executable upgrade -$_omp_executable notice +"$_omp_executable" upgrade +"$_omp_executable" notice _omp_cursor_positioning=1` assert.Equal(t, want, got) diff --git a/src/terminal/writer.go b/src/terminal/writer.go index baee0ffe..01ce8e09 100644 --- a/src/terminal/writer.go +++ b/src/terminal/writer.go @@ -183,34 +183,29 @@ func ClearAfter() string { } func FormatTitle(title string) string { + switch Shell { // These shells don't support setting the console title. - if Shell == shell.ELVISH || Shell == shell.XONSH { + case shell.ELVISH, shell.XONSH, shell.TCSH: return "" - } + case shell.BASH, shell.ZSH: + title = trimAnsi(title) + s := new(strings.Builder) - title = trimAnsi(title) + // We have to do this to prevent the shell from misidentifying escape sequences. + for _, char := range title { + escaped, shouldEscape := formats.EscapeSequences[char] + if shouldEscape { + s.WriteString(escaped) + continue + } - if Plain { - return title - } - - if Shell != shell.BASH && Shell != shell.ZSH { - return fmt.Sprintf(formats.Title, title) - } - - // We have to do this to prevent Bash/Zsh from misidentifying escape sequences. - s := new(strings.Builder) - for _, char := range title { - escaped, shouldEscape := formats.EscapeSequences[char] - if shouldEscape { - s.WriteString(escaped) - continue + s.WriteRune(char) } - s.WriteRune(char) + return fmt.Sprintf(formats.Title, s.String()) + default: + return fmt.Sprintf(formats.Title, trimAnsi(title)) } - - return fmt.Sprintf(formats.Title, s.String()) } func EscapeText(text string) string {