fix(shell): improve initialization

For supported shells:

- Correct string quoting.
- Reorganize initialization scripts to improve maintainability.
This commit is contained in:
L. Yeung 2024-09-17 03:16:40 +08:00 committed by Jan De Dobbeleer
parent 2050157aa7
commit a8f246064e
32 changed files with 784 additions and 681 deletions

View file

@ -101,23 +101,22 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
switch e.Env.Shell() { switch e.Env.Shell() {
case shell.ZSH: case shell.ZSH:
if promptType == Transient { if promptType == Transient {
if !e.Env.Flags().Eval {
break
}
prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(str)) prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(str))
// empty RPROMPT // empty RPROMPT
prompt += "\nRPROMPT=''" prompt += "\nRPROMPT=''"
return prompt return prompt
} }
return str
case shell.PWSH, shell.PWSH5: case shell.PWSH, shell.PWSH5:
if promptType == Transient { if promptType == Transient {
// clear the line afterwards to prevent text from being written on the same line // clear the line afterwards to prevent text from being written on the same line
// see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628 // see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628
return str + terminal.ClearAfter() return str + terminal.ClearAfter()
} }
return str
case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
return str
} }
return "" return str
} }

View file

@ -2,7 +2,6 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"strings" "strings"
) )
@ -13,13 +12,13 @@ var bashInit string
func (f Feature) Bash() Code { func (f Feature) Bash() Code {
switch f { switch f {
case CursorPositioning: case CursorPositioning:
return "_omp_cursor_positioning=1" return unixCursorPositioning
case FTCSMarks: case FTCSMarks:
return "_omp_ftcs_marks=1" return unixFTCSMarks
case Upgrade: case Upgrade:
return `"$_omp_executable" upgrade` return unixUpgrade
case Notice: case Notice:
return `"$_omp_executable" notice` return unixNotice
case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient: case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient:
fallthrough fallthrough
default: default:
@ -32,42 +31,5 @@ func QuotePosixStr(str string) string {
return "''" return "''"
} }
needQuoting := false return fmt.Sprintf("$'%s'", strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(str))
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()
} }

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -17,3 +18,17 @@ _omp_cursor_positioning=1`
assert.Equal(t, want, got) 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))
}
}

View file

@ -2,8 +2,6 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt"
"strings" "strings"
) )
@ -16,23 +14,32 @@ func (f Feature) Cmd() Code {
return "transient_enabled = true" return "transient_enabled = true"
case RPrompt: case RPrompt:
return "rprompt_enabled = true" return "rprompt_enabled = true"
case FTCSMarks:
return "ftcs_marks_enabled = true"
case Tooltips: case Tooltips:
return "enable_tooltips()" return "enable_tooltips()"
case Upgrade: case Upgrade:
return "os.execute(string.format('%s upgrade', omp_exe()))" return `os.execute(string.format('"%s" upgrade', omp_executable))`
case Notice: case Notice:
return "os.execute(string.format('%s notice', omp_exe()))" return `os.execute(string.format('"%s" notice', omp_executable))`
case PromptMark, PoshGit, Azure, LineError, Jobs, FTCSMarks, CursorPositioning: case PromptMark, PoshGit, Azure, LineError, Jobs, CursorPositioning:
fallthrough fallthrough
default: default:
return "" return ""
} }
} }
func quoteLuaStr(str string) string { func escapeLuaStr(str string) string {
if len(str) == 0 { if len(str) == 0 {
return "''" return str
} }
// We only replace a minimal set of special characters with corresponding escape sequences, without adding surrounding quotes.
return fmt.Sprintf("'%s'", strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(str)) // 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)
} }

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -12,9 +13,24 @@ func TestCmdFeatures(t *testing.T) {
want := `// these are the features want := `// these are the features
enable_tooltips() enable_tooltips()
transient_enabled = true transient_enabled = true
os.execute(string.format('%s upgrade', omp_exe())) ftcs_marks_enabled = true
os.execute(string.format('%s notice', omp_exe())) os.execute(string.format('"%s" upgrade', omp_executable))
os.execute(string.format('"%s" notice', omp_executable))
rprompt_enabled = true` rprompt_enabled = true`
assert.Equal(t, want, got) 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))
}
}

View file

@ -4,6 +4,13 @@ import "strings"
type Code string 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 { func (c Code) Indent(spaces int) Code {
return Code(strings.Repeat(" ", spaces) + string(c)) return Code(strings.Repeat(" ", spaces) + string(c))
} }

View file

@ -10,9 +10,9 @@ var elvishInit string
func (f Feature) Elvish() Code { func (f Feature) Elvish() Code {
switch f { switch f {
case Upgrade: case Upgrade:
return unixUpgrade return "$_omp_executable upgrade"
case Notice: case Notice:
return unixNotice return "$_omp_executable notice"
case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, CursorPositioning, Tooltips, Transient, FTCSMarks: case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, CursorPositioning, Tooltips, Transient, FTCSMarks:
fallthrough fallthrough
default: default:

View file

@ -2,7 +2,6 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"strings" "strings"
) )
@ -35,27 +34,6 @@ func quoteFishStr(str string) string {
if len(str) == 0 { if len(str) == 0 {
return "''" return "''"
} }
needQuoting := false
var b strings.Builder return fmt.Sprintf("'%s'", strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(str))
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()
} }

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -13,9 +14,23 @@ func TestFishFeatures(t *testing.T) {
enable_poshtooltips enable_poshtooltips
set --global _omp_transient_prompt 1 set --global _omp_transient_prompt 1
set --global _omp_ftcs_marks 1 set --global _omp_ftcs_marks 1
$_omp_executable upgrade "$_omp_executable" upgrade
$_omp_executable notice "$_omp_executable" notice
set --global _omp_prompt_mark 1` set --global _omp_prompt_mark 1`
assert.Equal(t, want, got) 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))
}
}

View file

@ -72,7 +72,7 @@ func Init(env runtime.Environment, feats Features) string {
createNuInit(env, feats) createNuInit(env, feats)
return "" return ""
default: 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) configFile = quoteFishStr(configFile)
script = fishInit script = fishInit
case CMD: case CMD:
executable = quoteLuaStr(executable) executable = escapeLuaStr(executable)
configFile = quoteLuaStr(configFile) configFile = escapeLuaStr(configFile)
script = cmdInit script = cmdInit
case NU: case NU:
executable = quoteNuStr(executable) executable = quoteNuStr(executable)
configFile = quoteNuStr(configFile) configFile = quoteNuStr(configFile)
script = nuInit script = nuInit
case TCSH: case TCSH:
executable = QuotePosixStr(executable) executable = quoteCshStr(executable)
configFile = QuotePosixStr(configFile) configFile = quoteCshStr(configFile)
script = tcshInit script = tcshInit
case ELVISH: case ELVISH:
executable = quotePwshOrElvishStr(executable) executable = quotePwshOrElvishStr(executable)
configFile = quotePwshOrElvishStr(configFile) configFile = quotePwshOrElvishStr(configFile)
script = elvishInit script = elvishInit
case XONSH: case XONSH:
executable = quotePythonStr(executable)
configFile = quotePythonStr(configFile)
script = xonshInit script = xonshInit
default: default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell) return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)

View file

@ -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))
}
}

View file

@ -2,7 +2,6 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -17,7 +16,7 @@ var nuInit string
func (f Feature) Nu() Code { func (f Feature) Nu() Code {
switch f { switch f {
case Transient: 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: case Upgrade:
return "^$_omp_executable upgrade" return "^$_omp_executable upgrade"
case Notice: case Notice:
@ -33,12 +32,13 @@ func quoteNuStr(str string) string {
if len(str) == 0 { if len(str) == 0 {
return "''" return "''"
} }
return fmt.Sprintf(`"%s"`, strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(str)) return fmt.Sprintf(`"%s"`, strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(str))
} }
func createNuInit(env runtime.Environment, features Features) { func createNuInit(env runtime.Environment, features Features) {
initPath := filepath.Join(env.Home(), ".oh-my-posh.nu") 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 { if err != nil {
return return
} }

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -9,11 +10,24 @@ import (
func TestNuFeatures(t *testing.T) { func TestNuFeatures(t *testing.T) {
got := allFeatures.Lines(NU).String("// these are the features") got := allFeatures.Lines(NU).String("// these are the features")
//nolint: lll
want := `// these are the features 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 upgrade
^$_omp_executable notice` ^$_omp_executable notice`
assert.Equal(t, want, got) 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))
}
}

View file

@ -2,7 +2,6 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"strings" "strings"
) )
@ -38,5 +37,9 @@ func (f Feature) Pwsh() Code {
} }
func quotePwshOrElvishStr(str string) string { func quotePwshOrElvishStr(str string) string {
if len(str) == 0 {
return "''"
}
return fmt.Sprintf("'%s'", strings.ReplaceAll(str, "'", "''")) return fmt.Sprintf("'%s'", strings.ReplaceAll(str, "'", "''"))
} }

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -24,3 +25,17 @@ $global:_ompFTCSMarks = $true
assert.Equal(t, want, got) 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))
}
}

View file

@ -1,6 +1,6 @@
export POSH_THEME=::CONFIG:: export POSH_THEME=::CONFIG::
export POSH_SHELL_VERSION=$BASH_VERSION export POSH_SHELL_VERSION=$BASH_VERSION
export POWERLINE_COMMAND="oh-my-posh" export POWERLINE_COMMAND='oh-my-posh'
export POSH_PID=$$ export POSH_PID=$$
export CONDA_PROMPT_MODIFIER=false export CONDA_PROMPT_MODIFIER=false
export OSTYPE=$OSTYPE export OSTYPE=$OSTYPE
@ -10,12 +10,12 @@ if [[ $OSTYPE =~ ^(msys|cygwin) ]]; then
fi fi
# global variables # global variables
_omp_start_time="" _omp_start_time=''
_omp_stack_count=0 _omp_stack_count=0
_omp_elapsed=-1 _omp_execution_time=-1
_omp_no_exit_code="true" _omp_no_status=true
_omp_status_cache=0 _omp_status=0
_omp_pipestatus_cache=0 _omp_pipestatus=0
_omp_executable=::OMP:: _omp_executable=::OMP::
# switches to enable/disable features # 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)' PS0='${_omp_start_time:0:$((_omp_start_time="$(_omp_start_timer)",0))}$(_omp_ftcs_command_start)'
# set secondary prompt # 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() { function _omp_set_cursor_position() {
# not supported in Midnight Commander # not supported in Midnight Commander
@ -54,7 +58,7 @@ function _omp_start_timer() {
function _omp_ftcs_command_start() { function _omp_ftcs_command_start() {
if [[ $_omp_ftcs_marks == 1 ]]; then if [[ $_omp_ftcs_marks == 1 ]]; then
printf "\e]133;C\a" printf '\e]133;C\a'
fi fi
} }
@ -63,26 +67,36 @@ function set_poshcontext() {
return return
} }
function _omp_print_primary() { function _omp_get_primary() {
# Avoid unexpected expansions. # Avoid unexpected expansions when we're generating the prompt below.
shopt -u promptvars shopt -u promptvars
trap 'shopt -s promptvars' RETURN
local prompt local prompt
if shopt -oq posix; then if shopt -oq posix; then
# Disable in POSIX mode. # Disable in POSIX mode.
prompt='[NOTICE: Oh My Posh prompt is not supported in POSIX mode]\n\u@\h:\w\$ ' prompt='[NOTICE: Oh My Posh prompt is not supported in POSIX mode]\n\u@\h:\w\$ '
else 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 fi
echo "${prompt@P}" echo "${prompt@P}"
# Allow command substitution in PS0.
shopt -s promptvars
} }
function _omp_print_secondary() { function _omp_get_secondary() {
# Avoid unexpected expansions. # Avoid unexpected expansions when we're generating the prompt below.
shopt -u promptvars shopt -u promptvars
trap 'shopt -s promptvars' RETURN
if shopt -oq posix; then if shopt -oq posix; then
# Disable in POSIX mode. # Disable in POSIX mode.
@ -90,38 +104,39 @@ function _omp_print_secondary() {
else else
echo "${_omp_secondary_prompt@P}" echo "${_omp_secondary_prompt@P}"
fi fi
# Allow command substitution in PS0.
shopt -s promptvars
} }
function _omp_hook() { function _omp_hook() {
_omp_status_cache=$? _omp_pipestatus_cache=("${PIPESTATUS[@]}") _omp_status=$? _omp_pipestatus=("${PIPESTATUS[@]}")
if [[ ${#BP_PIPESTATUS[@]} -ge ${#_omp_pipestatus_cache[@]} ]]; then if [[ ${#BP_PIPESTATUS[@]} -ge ${#_omp_pipestatus[@]} ]]; then
_omp_pipestatus_cache=("${BP_PIPESTATUS[@]}") _omp_pipestatus=("${BP_PIPESTATUS[@]}")
fi fi
_omp_stack_count=$((${#DIRSTACK[@]} - 1)) _omp_stack_count=$((${#DIRSTACK[@]} - 1))
_omp_execution_time=-1
if [[ $_omp_start_time ]]; then if [[ $_omp_start_time ]]; then
local omp_now=$("$_omp_executable" get millis --shell=bash) local omp_now=$("$_omp_executable" get millis)
_omp_elapsed=$((omp_now - _omp_start_time)) _omp_execution_time=$((omp_now - _omp_start_time))
_omp_start_time="" _omp_no_status=false
_omp_no_exit_code="false"
fi fi
_omp_start_time=''
if [[ ${_omp_pipestatus_cache[-1]} != "$_omp_status_cache" ]]; then if [[ ${_omp_pipestatus[-1]} != "$_omp_status" ]]; then
_omp_pipestatus_cache=("$_omp_status_cache") _omp_pipestatus=("$_omp_status")
fi fi
set_poshcontext set_poshcontext
_omp_set_cursor_position _omp_set_cursor_position
PS1='$(_omp_print_primary)' PS1='$(_omp_get_primary)'
PS2='$(_omp_print_secondary)' 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() { function _omp_install_hook() {
@ -129,7 +144,7 @@ function _omp_install_hook() {
local cmd local cmd
for cmd in "${PROMPT_COMMAND[@]}"; do for cmd in "${PROMPT_COMMAND[@]}"; do
if [[ $cmd = "_omp_hook" ]]; then if [[ $cmd = _omp_hook ]]; then
return return
fi fi
done done

View file

@ -1,33 +1,54 @@
set-env POSH_PID (to-string (randint 10000000000000 10000000000000000)) set-env POSH_PID (to-string (randint 10000000000000 10000000000000000))
set-env POSH_THEME ::CONFIG:: set-env POSH_THEME ::CONFIG::
set-env POSH_SHELL_VERSION (elvish --version) set-env POSH_SHELL_VERSION $version
set-env POWERLINE_COMMAND 'oh-my-posh' set-env POWERLINE_COMMAND oh-my-posh
var _omp_error_code = 0 var _omp_executable = (external ::OMP::)
var _omp_executable = ::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] var error = $m[error]
if (is $error $nil) { if (is $error $nil) {
set _omp_error_code = 0 set _omp_status = 0
} else { } else {
try { try {
set _omp_error_code = $error[reason][exit-status] set _omp_status = $error[reason][exit-status]
} catch { } catch {
# built-in commands don't have a status code. # 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~ ] fn _omp_get_prompt {|type @arguments|
$_omp_executable print $type ^
set edit:prompt = { --shell=elvish ^
var cmd-duration = (printf "%.0f" (* $edit:command-duration 1000)) --shell-version=$E:POSH_SHELL_VERSION ^
(external $_omp_executable) print primary --shell=elvish --execution-time=$cmd-duration --status=$_omp_error_code --pwd=$pwd --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 = { set edit:after-readline = [ $@edit:after-readline $_omp-after-readline-hook~ ]
var cmd-duration = (printf "%.0f" (* $edit:command-duration 1000)) set edit:after-command = [ $@edit:after-command $_omp-after-command-hook~ ]
(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:prompt = {|| _omp_get_prompt primary }
} set edit:rprompt = {|| _omp_get_prompt right }

View file

@ -24,42 +24,57 @@ function set_poshcontext
return return
end 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. # 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 function fish_prompt
set --local omp_status_cache_temp $status set --local omp_status_temp $status
set --local omp_pipestatus_cache_temp $pipestatus set --local omp_pipestatus_temp $pipestatus
# clear from cursor to end of screen as # clear from cursor to end of screen as
# commandline --function repaint does not do this # commandline --function repaint does not do this
# see https://github.com/fish-shell/fish-shell/issues/8418 # see https://github.com/fish-shell/fish-shell/issues/8418
printf \e\[0J printf \e\[0J
if test "$_omp_transient" = 1 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 return
end end
if test "$_omp_new_prompt" = 0 if test "$_omp_new_prompt" = 0
echo -n "$_omp_current_prompt" echo -n "$_omp_current_prompt"
return return
end end
set --global _omp_status_cache $omp_status_cache_temp set --global _omp_status $omp_status_temp
set --global _omp_pipestatus_cache $omp_pipestatus_cache_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_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 # check if variable set, < 3.2 case
if set --query _omp_last_command && test -z "$_omp_last_command" if set --query _omp_last_command && test -z "$_omp_last_command"
set _omp_duration 0 set _omp_execution_time 0
set _omp_no_exit_code true set _omp_no_status true
end end
# works with fish >=3.2 # works with fish >=3.2
if set --query _omp_last_status_generation && test "$_omp_last_status_generation" = "$status_generation" if set --query _omp_last_status_generation && test "$_omp_last_status_generation" = "$status_generation"
set _omp_duration 0 set _omp_execution_time 0
set _omp_no_exit_code true set _omp_no_status true
else if test -z "$_omp_last_status_generation" else if test -z "$_omp_last_status_generation"
# first execution - $status_generation is 0, $_omp_last_status_generation is empty # first execution - $status_generation is 0, $_omp_last_status_generation is empty
set _omp_no_exit_code true set _omp_no_status true
end end
if set --query status_generation if set --query status_generation
@ -81,7 +96,8 @@ function fish_prompt
end end
# The prompt is saved for possible reuse, typically a repaint after clearing the screen buffer. # 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" echo -n "$_omp_current_prompt"
end end
@ -98,7 +114,7 @@ function fish_right_prompt
end end
set _omp_new_prompt 0 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" echo -n "$_omp_current_rprompt"
end end
@ -154,7 +170,7 @@ function _omp_space_key_handler
end end
set _omp_tooltip_command $tooltip_command 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" if test -z "$tooltip_prompt"
return return

View file

@ -1,34 +1,24 @@
-- Upgrade notice ---@diagnostic disable: undefined-global
---@diagnostic disable: undefined-field
local notice = [[::UPGRADENOTICE::]] ---@diagnostic disable: lowercase-global
if '::UPGRADE::' == 'true' then
print(notice)
end
-- Cache PID -- Cache PID
os.setenv("POSH_PID", os.getpid()) os.setenv('POSH_PID', os.getpid())
-- Helper functions -- Helper functions
local function get_priority_number(name, default) local function get_priority_number(name, default)
local value = os.getenv(name) local value = os.getenv(name)
if os.envmap ~= nil and type(os.envmap) == 'table' then if value == nil and os.envmap ~= nil and type(os.envmap) == 'table' then
local t = os.envmap[name] value = os.envmap[name]
value = (t ~= nil and type(t) == 'string') and t or value end
end local num = tonumber(value)
if type(default) == 'number' then if num ~= nil then
value = tonumber(value) return num
if value == nil then end
return default return default
else
return value
end
else
return default
end
end end
os.setenv("POSH_CURSOR_LINE", console.getnumlines())
-- Environment variables -- Environment variables
local function environment_onbeginedit() local function environment_onbeginedit()
@ -41,6 +31,7 @@ local endedit_time = 0
local last_duration = 0 local last_duration = 0
local rprompt_enabled = false local rprompt_enabled = false
local transient_enabled = false local transient_enabled = false
local ftcs_marks_enabled = false
local no_exit_code = true local no_exit_code = true
local cached_prompt = {} local cached_prompt = {}
@ -57,7 +48,7 @@ local function cache_onbeginedit()
local old_cache = cached_prompt local old_cache = cached_prompt
-- Start a new table for the new edit/prompt session. -- 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. -- Copy the cached left/right prompt strings if the cwd hasn't changed.
-- IMPORTANT OPTIMIZATION: This keeps the prompt highly responsive, except -- IMPORTANT OPTIMIZATION: This keeps the prompt highly responsive, except
@ -68,33 +59,30 @@ local function cache_onbeginedit()
end end
end end
-- Executable
local omp_executable = '::OMP::'
-- Configuration -- Configuration
local function omp_exe() os.setenv('POSH_THEME', '::CONFIG::')
return '"'..::OMP::..'"' 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))
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))
-- Execution helpers -- Execution helpers
local function can_async() local function can_async()
if (clink.version_encoded or 0) >= 10030001 then if (clink.version_encoded or 0) >= 10030001 then
return settings.get("prompt.async") return settings.get('prompt.async')
end end
end end
local function run_posh_command(command) local function run_posh_command(command)
command = '"'..command..'"' command = string.format('""%s" %s"', omp_executable, command)
local _, ismain = coroutine.running() local _, is_main = coroutine.running()
local output if is_main then
if ismain then return io.popen(command):read('*a')
output = io.popen(command):read("*a")
else
output = io.popenyield(command):read("*a")
end end
return output return io.popenyield(command):read('*a')
end end
-- Duration functions -- Duration functions
@ -105,10 +93,8 @@ local function os_clock_millis()
-- OMP to get the time in milliseconds. -- OMP to get the time in milliseconds.
if (clink.version_encoded or 0) >= 10020030 then if (clink.version_encoded or 0) >= 10020030 then
return math.floor(os.clock() * 1000) 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 end
return run_posh_command('get millis')
end end
local function duration_onbeginedit() local function duration_onbeginedit()
@ -125,7 +111,7 @@ end
local function duration_onendedit(input) local function duration_onendedit(input)
endedit_time = 0 endedit_time = 0
-- For an empty command, the execution time should not be evaluated. -- 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() endedit_time = os_clock_millis()
end end
end end
@ -134,43 +120,47 @@ end
local function execution_time_option() local function execution_time_option()
if last_duration ~= nil then if last_duration ~= nil then
return "--execution-time "..last_duration return '--execution-time=' .. last_duration
end end
return "" return ''
end end
local function error_level_option() local function status_option()
if os.geterrorlevel ~= nil and settings.get("cmd.get_errorlevel") then if os.geterrorlevel ~= nil and settings.get('cmd.get_errorlevel') then
return "--status "..os.geterrorlevel() return '--status=' .. os.geterrorlevel()
end end
return "" return ''
end end
local function no_exit_code_option() local function no_status_option()
if no_exit_code then if no_exit_code then
return "--no-status" return '--no-status'
end end
return "" return ''
end end
local function get_posh_prompt(rprompt) local function get_posh_prompt(prompt_type, ...)
local prompt = "primary" os.setenv('POSH_CURSOR_LINE', console.getnumlines())
if rprompt then local command = table.concat({
prompt = "right" 'print',
end prompt_type,
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()) '--shell=cmd',
return run_posh_command(prompt_exe) status_option(),
no_status_option(),
execution_time_option(),
...
}, ' ')
return run_posh_command(command)
end end
local function set_posh_tooltip(tip_command) 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. -- Escape special characters properly, if any.
local escaped_tip_command = string.gsub(tip_command, '(\\+)"', '%1%1"'):gsub('(\\+)$', '%1%1'):gsub('"', '\\"'):gsub('([&<>%(%)@%^|])', '^%1') 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 prompt_exe = string.format('%s print tooltip --shell=cmd %s --command="%s"', omp_exe(), error_level_option(), escaped_tip_command) local tooltip = get_posh_prompt('tooltip', command_option)
local tooltip = run_posh_command(prompt_exe)
-- Do not cache an empty tooltip. -- Do not cache an empty tooltip.
if tooltip == "" then if tooltip == '' then
return return
end end
cached_prompt.tip_command = tip_command cached_prompt.tip_command = tip_command
@ -185,25 +175,10 @@ local function display_cached_prompt()
cached_prompt.only_use_cache = nil cached_prompt.only_use_cache = nil
end 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) local function command_executed_mark(input)
if string.gsub(input, "^%s*(.-)%s*$", "%1") ~= "" then no_exit_code = string.gsub(input, '^%s*(.-)%s*$', '%1') == ''
no_exit_code = false if ftcs_marks_enabled then
else clink.print('\x1b]133;C\007', NONL)
no_exit_code = true
end
if "::FTCS_MARKS::" == "true" then
clink.print("\x1b]133;C\007", NONL)
end end
end end
@ -216,7 +191,7 @@ function p:filter(prompt)
-- Get a left prompt immediately if nothing is available yet. -- Get a left prompt immediately if nothing is available yet.
if not cached_prompt.left then if not cached_prompt.left then
cached_prompt.left = get_posh_prompt(false) cached_prompt.left = get_posh_prompt('primary')
need_left = false need_left = false
end end
@ -228,10 +203,10 @@ function p:filter(prompt)
-- function was defined. That way if a new prompt starts (which -- function was defined. That way if a new prompt starts (which
-- discards the old coroutine) and a new coroutine starts, the old -- discards the old coroutine) and a new coroutine starts, the old
-- coroutine won't stomp on the new cached_prompt table. -- coroutine won't stomp on the new cached_prompt table.
clink.promptcoroutine(function () clink.promptcoroutine(function()
-- Generate left prompt, if needed. -- Generate left prompt, if needed.
if need_left then if need_left then
cached_prompt.left = get_posh_prompt(false) cached_prompt.left = get_posh_prompt('primary')
end end
-- Generate right prompt, if needed. -- Generate right prompt, if needed.
if rprompt_enabled then if rprompt_enabled then
@ -239,17 +214,17 @@ function p:filter(prompt)
-- Show left side while right side is being generated. -- Show left side while right side is being generated.
display_cached_prompt() display_cached_prompt()
end end
cached_prompt.right = get_posh_prompt(true) cached_prompt.right = get_posh_prompt('right')
else else
cached_prompt.right = nil cached_prompt.right = nil
end end
end) end)
else else
if need_left then if need_left then
cached_prompt.left = get_posh_prompt(false) cached_prompt.left = get_posh_prompt('primary')
end end
if rprompt_enabled then if rprompt_enabled then
cached_prompt.right = get_posh_prompt(true) cached_prompt.right = get_posh_prompt('right')
end end
end end
end end
@ -269,10 +244,9 @@ function p:transientfilter(prompt)
return nil return nil
end end
local prompt_exe = string.format('%s print transient --shell=cmd %s %s', omp_exe(), error_level_option(), no_exit_code_option()) prompt = get_posh_prompt('transient')
prompt = run_posh_command(prompt_exe)
if prompt == "" then if prompt == '' then
prompt = nil prompt = nil
end end
@ -280,7 +254,7 @@ function p:transientfilter(prompt)
end end
function p:transientrightfilter(prompt) function p:transientrightfilter(prompt)
return "", false return '', false
end end
-- Event handlers -- Event handlers
@ -303,12 +277,12 @@ end
-- Tooltips -- 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 -- Insert space first, in case it might affect the tip word, e.g. it could
-- split "gitcommit" into "git commit". -- split "gitcommit" into "git commit".
rl_buffer:insert(" ") rl_buffer:insert(' ')
-- Get the first word of command line as tip. -- 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 asynchronously (via coroutine) if available, otherwise
-- generate a tooltip immediately. -- generate a tooltip immediately.
@ -318,7 +292,7 @@ function ohmyposh_space(rl_buffer)
elseif cached_prompt.coroutine then elseif cached_prompt.coroutine then
-- No action needed; a tooltip coroutine is already running. -- No action needed; a tooltip coroutine is already running.
else else
cached_prompt.coroutine = coroutine.create(function () cached_prompt.coroutine = coroutine.create(function()
set_posh_tooltip(tip_command) set_posh_tooltip(tip_command)
if cached_prompt.coroutine == coroutine.running() then if cached_prompt.coroutine == coroutine.running() then
cached_prompt.coroutine = nil cached_prompt.coroutine = nil
@ -333,5 +307,5 @@ local function enable_tooltips()
return return
end end
rl.setbinding(' ', [["luafunc:ohmyposh_space"]], 'emacs') rl.setbinding(' ', [["luafunc:_omp_space_keybinding"]], 'emacs')
end end

View file

@ -4,28 +4,47 @@ if ($env.config? | is-not-empty) {
} }
$env.POWERLINE_COMMAND = 'oh-my-posh' $env.POWERLINE_COMMAND = 'oh-my-posh'
$env.POSH_THEME = ::CONFIG:: $env.POSH_THEME = (echo ::CONFIG::)
$env.PROMPT_INDICATOR = "" $env.PROMPT_INDICATOR = ""
$env.POSH_PID = (random uuid) $env.POSH_PID = (random uuid)
$env.POSH_SHELL_VERSION = (version | get version) $env.POSH_SHELL_VERSION = (version | get version)
let _omp_executable: string = ::OMP:: let _omp_executable: string = (echo ::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
}
# PROMPTS # 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 # hack to set the cursor line to 1 when the user clears the screen
# this obviously isn't bulletproof, but it's a start # this obviously isn't bulletproof, but it's a start
mut clear = false mut clear = false
@ -37,9 +56,7 @@ $env.PROMPT_COMMAND = { ||
do --env $env.SET_POSHCONTEXT 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 = { || $env.PROMPT_COMMAND_RIGHT = {|| _omp_get_prompt 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)"
}

View file

@ -48,28 +48,24 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$env:POSH_THEME = (Resolve-Path -Path ::CONFIG::).ProviderPath $env:POSH_THEME = (Resolve-Path -Path ::CONFIG::).ProviderPath
} }
function Start-Utf8Process { function Invoke-Utf8Posh {
param( param([string[]]$Arguments = @())
[string]$FileName,
[string[]]$Arguments = @()
)
if ($script:ConstrainedLanguageMode) { if ($script:ConstrainedLanguageMode) {
$standardOut = Invoke-Expression "& `$FileName `$Arguments 2>&1" $output = Invoke-Expression "& `$global:_ompExecutable `$Arguments 2>&1"
$standardOut -join "`n" $output -join "`n"
return return
} }
$Process = New-Object System.Diagnostics.Process $Process = New-Object System.Diagnostics.Process
$StartInfo = $Process.StartInfo $StartInfo = $Process.StartInfo
$StartInfo.FileName = $FileName $StartInfo.FileName = $global:_ompExecutable
if ($StartInfo.ArgumentList.Add) { if ($StartInfo.ArgumentList.Add) {
# ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+) # 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-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 # 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($_) } $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) # escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
$escapedArgs = $Arguments | ForEach-Object { $escapedArgs = $Arguments | ForEach-Object {
# escape N consecutive backslash(es), which are followed by a double quote, to 2N consecutive ones # 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 $stdoutTask.Result
} }
function Set-PoshContext([bool]$originalStatus) {}
function Get-NonFSWD { function Get-NonFSWD {
# We only need to return a non-filesystem working directory. # We only need to return a non-filesystem working directory.
if ($PWD.Provider.Name -ne 'FileSystem') { if ($PWD.Provider.Name -ne 'FileSystem') {
@ -131,6 +125,191 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$terminalWidth $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 { function Enable-PoshTooltips {
if ($script:ConstrainedLanguageMode) { if ($script:ConstrainedLanguageMode) {
return return
@ -151,35 +330,21 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
} }
$script:TooltipCommand = $command $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 '' $output = (Get-PoshPrompt "tooltip" @(
if (!$standardOut) { "--column=$($Host.UI.RawUI.CursorPosition.X)"
"--command=$command"
)) -join ''
if (!$output) {
return return
} }
Write-Host $standardOut -NoNewline Write-Host $output -NoNewline
# Workaround to prevent the text after cursor from disappearing when the tooltip is printed. # Workaround to prevent the text after cursor from disappearing when the tooltip is printed.
[Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ') [Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ')
[Microsoft.PowerShell.PSConsoleReadLine]::Undo() [Microsoft.PowerShell.PSConsoleReadLine]::Undo()
} } finally {}
finally {}
}
}
function Set-TransientPrompt {
$previousOutputEncoding = [Console]::OutputEncoding
try {
$script:TransientPrompt = $true
[Console]::OutputEncoding = [Text.Encoding]::UTF8
[Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
}
finally {
[Console]::OutputEncoding = $previousOutputEncoding
} }
} }
@ -197,8 +362,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$script:TooltipCommand = '' $script:TooltipCommand = ''
Set-TransientPrompt Set-TransientPrompt
} }
} } finally {
finally {
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
if ($global:_ompFTCSMarks -and $executingCommand) { if ($global:_ompFTCSMarks -and $executingCommand) {
# Write FTCS_COMMAND_EXECUTED after accepting the input - it should still happen before execution # 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 = '' $script:TooltipCommand = ''
Set-TransientPrompt Set-TransientPrompt
} }
} } finally {
finally {
[Microsoft.PowerShell.PSConsoleReadLine]::CopyOrCancelLine() [Microsoft.PowerShell.PSConsoleReadLine]::CopyOrCancelLine()
} }
} }
} }
function Enable-PoshLineError { function Enable-PoshLineError {
$validLine = (Start-Utf8Process $global:_ompExecutable @("print", "valid", "--shell=$script:ShellName")) -join "`n" $validLine = (Invoke-Utf8Posh @("print", "valid", "--shell=$script:ShellName")) -join "`n"
$errorLine = (Start-Utf8Process $global:_ompExecutable @("print", "error", "--shell=$script:ShellName")) -join "`n" $errorLine = (Invoke-Utf8Posh @("print", "error", "--shell=$script:ShellName")) -join "`n"
Set-PSReadLineOption -PromptText $validLine, $errorLine Set-PSReadLineOption -PromptText $validLine, $errorLine
} }
@ -264,40 +427,24 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
$Format = 'json' $Format = 'json'
) )
$configString = Start-Utf8Process $global:_ompExecutable @("config", "export", "--format=$Format")
# if no path, copy to clipboard by default
if ($FilePath) { if ($FilePath) {
# https://stackoverflow.com/questions/3038337/powershell-resolve-path-that-might-not-exist # https://stackoverflow.com/questions/3038337/powershell-resolve-path-that-might-not-exist
$FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) $FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
[IO.File]::WriteAllLines($FilePath, $configString)
} }
else {
Set-Clipboard $configString
Write-Output "Theme copied to clipboard"
}
}
function Get-FileHyperlink { $output = Invoke-Utf8Posh @(
param( "config", "export"
[Parameter(Mandatory, ValuefromPipeline = $True)] "--format=$Format"
[string]$uri, "--output=$FilePath"
[Parameter(ValuefromPipeline = $True)]
[string]$name
) )
if (!$output) {
$esc = [char]27 Write-Host "Theme exported to $(Get-FileHyperlink $FilePath)."
if (!$name) { return
# if name not set, uri is used as the name of the hyperlink
$name = $uri
} }
if ($null -ne $env:WSL_DISTRO_NAME) { # When no path is provided, copy the output to clipboard.
# wsl conversion if needed Set-Clipboard $output
$uri = &wslpath -m $uri Write-Host 'Theme copied to clipboard.'
}
# return an ANSI formatted hyperlink
return "$esc]8;;file://$uri$esc\$name$esc]8;;$esc\"
} }
function Get-PoshThemes { function Get-PoshThemes {
@ -329,19 +476,28 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
Write-Host $logo Write-Host $logo
$themes = Get-ChildItem -Path "$Path/*" -Include '*.omp.json' | Sort-Object Name $themes = Get-ChildItem -Path "$Path/*" -Include '*.omp.json' | Sort-Object Name
if ($List -eq $true) { if ($List -eq $true) {
$themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders $themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -Uri $_.FullName } } | Format-Table -HideTableHeaders
} } else {
else {
$nonFSWD = Get-NonFSWD $nonFSWD = Get-NonFSWD
$stackCount = Get-PoshStackCount
$terminalWidth = Get-TerminalWidth
$themes | ForEach-Object -Process { $themes | ForEach-Object -Process {
Write-Host "Theme: $(Get-FileHyperlink -uri $_.FullName -Name ($_.BaseName -replace '\.omp$', ''))`n" 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") 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 "`n"
} }
} }
Write-Host @" Write-Host @"
Themes location: $(Get-FileHyperlink -uri "$Path") Themes location: $(Get-FileHyperlink -Uri "$Path")
To change your theme, adjust the init script in $PROFILE. To change your theme, adjust the init script in $PROFILE.
Example: 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 # perform cleanup on removal so a new initialization in current session works
if (!$script:ConstrainedLanguageMode) { if (!$script:ConstrainedLanguageMode) {
$ExecutionContext.SessionState.Module.OnRemove += { $ExecutionContext.SessionState.Module.OnRemove += {
@ -506,11 +529,6 @@ Example:
} }
} }
$notice = Start-Utf8Process $global:_ompExecutable @("notice")
if ($notice) {
Write-Host $notice -NoNewline
}
Export-ModuleMember -Function @( Export-ModuleMember -Function @(
"Set-PoshContext" "Set-PoshContext"
"Enable-PoshTooltips" "Enable-PoshTooltips"
@ -518,7 +536,6 @@ Example:
"Enable-PoshLineError" "Enable-PoshLineError"
"Export-PoshTheme" "Export-PoshTheme"
"Get-PoshThemes" "Get-PoshThemes"
"Start-Utf8Process"
"prompt" "prompt"
) )
} | Import-Module -Global } | Import-Module -Global

View file

@ -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

View file

@ -1,20 +1,33 @@
setenv POWERLINE_COMMAND "oh-my-posh"; setenv POWERLINE_COMMAND "oh-my-posh";
setenv POSH_THEME ::CONFIG::; setenv POSH_THEME ::CONFIG::;
setenv POSH_SHELL_VERSION ""; setenv POSH_SHELL_VERSION "$tcsh";
setenv POSH_PID $$; setenv POSH_PID $$;
setenv OSTYPE "$OSTYPE"; setenv OSTYPE "$OSTYPE";
if ("$OSTYPE" =~ {msys,cygwin}*) then if ( "$OSTYPE" =~ {msys,cygwin}* ) setenv POSH_PID "`cat /proc/$$/winpid`";
setenv POSH_PID "`cat /proc/$$/winpid`";
endif
set POSH_COMMAND = ::OMP::; if ( ! $?_omp_enabled ) alias precmd '
set USER_PRECMD = "`alias precmd`"; set _omp_status = $status;
set USER_POSTCMD = "`alias postcmd`"; set _omp_execution_time = -1;
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 _omp_last_cmd = `echo $_:q`;
set POSH_POSTCMD = 'set POSH_START_TIME = `$POSH_COMMAND get millis`'; 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"; if ( ! $?_omp_enabled ) alias postcmd '
alias postcmd "$POSH_POSTCMD;$USER_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;

49
src/shell/scripts/omp.xsh Normal file
View file

@ -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

View file

@ -1,7 +1,7 @@
export POSH_THEME=::CONFIG:: export POSH_THEME=::CONFIG::
export POSH_SHELL_VERSION=$ZSH_VERSION export POSH_SHELL_VERSION=$ZSH_VERSION
export POSH_PID=$$ export POSH_PID=$$
export POWERLINE_COMMAND="oh-my-posh" export POWERLINE_COMMAND='oh-my-posh'
export CONDA_PROMPT_MODIFIER=false export CONDA_PROMPT_MODIFIER=false
export POSH_PROMPT_COUNT=0 export POSH_PROMPT_COUNT=0
export ZLE_RPROMPT_INDENT=0 export ZLE_RPROMPT_INDENT=0
@ -12,6 +12,7 @@ if [[ $OSTYPE =~ ^(msys|cygwin) ]]; then
fi fi
_omp_executable=::OMP:: _omp_executable=::OMP::
_omp_tooltip_command=''
# switches to enable/disable features # switches to enable/disable features
_omp_cursor_positioning=0 _omp_cursor_positioning=0
@ -31,7 +32,7 @@ function _omp_set_cursor_position() {
stty raw -echo min 0 stty raw -echo min 0
local pos local pos
echo -en "\033[6n" >/dev/tty echo -en '\033[6n' >/dev/tty
read -r -d R pos read -r -d R pos
pos=${pos:2} # strip off the esc-[ pos=${pos:2} # strip off the esc-[
local parts=(${(s:;:)pos}) local parts=(${(s:;:)pos})
@ -49,27 +50,28 @@ function set_poshcontext() {
function _omp_preexec() { function _omp_preexec() {
if [[ $_omp_ftcs_marks == 0 ]]; then if [[ $_omp_ftcs_marks == 0 ]]; then
printf "\033]133;C\007" printf '\033]133;C\007'
fi fi
_omp_start_time=$($_omp_executable get millis) _omp_start_time=$($_omp_executable get millis)
} }
function _omp_precmd() { function _omp_precmd() {
_omp_status_cache=$? _omp_status=$?
_omp_pipestatus_cache=(${pipestatus[@]}) _omp_pipestatus=(${pipestatus[@]})
_omp_stack_count=${#dirstack[@]} _omp_stack_count=${#dirstack[@]}
_omp_elapsed=-1 _omp_execution_time=-1
_omp_no_exit_code="true" _omp_no_status=true
_omp_tooltip_command=''
if [ $_omp_start_time ]; then if [ $_omp_start_time ]; then
local omp_now=$($_omp_executable get millis --shell=zsh) local omp_now=$($_omp_executable get millis)
_omp_elapsed=$(($omp_now - $_omp_start_time)) _omp_execution_time=$(($omp_now - $_omp_start_time))
_omp_no_exit_code="false" _omp_no_status=false
fi fi
if [[ ${_omp_pipestatus_cache[-1]} != "$_omp_status_cache" ]]; then if [[ ${_omp_pipestatus[-1]} != "$_omp_status" ]]; then
_omp_pipestatus_cache=("$_omp_status_cache") _omp_pipestatus=("$_omp_status")
fi fi
count=$((POSH_PROMPT_COUNT + 1)) count=$((POSH_PROMPT_COUNT + 1))
@ -86,8 +88,8 @@ function _omp_precmd() {
setopt PROMPT_PERCENT setopt PROMPT_PERCENT
PS2=$_omp_secondary_prompt 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 unset _omp_start_time
} }
@ -116,6 +118,20 @@ function _omp_cleanup() {
_omp_cleanup _omp_cleanup
unset -f _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() { function _omp_render_tooltip() {
if [[ $KEYS != ' ' ]]; then if [[ $KEYS != ' ' ]]; then
return return
@ -130,7 +146,7 @@ function _omp_render_tooltip() {
fi fi
_omp_tooltip_command="$tooltip_command" _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 if [[ -z $tooltip ]]; then
return return
fi fi
@ -148,23 +164,21 @@ function _omp_zle-line-init() {
local -i ret=$? local -i ret=$?
(( $+zle_bracketed_paste )) && print -r -n - $zle_bracketed_paste[2] (( $+zle_bracketed_paste )) && print -r -n - $zle_bracketed_paste[2]
_omp_tooltip_command='' eval "$(_omp_get_prompt transient --eval)"
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")"
zle .reset-prompt zle .reset-prompt
# Exit the shell if we receive EOT.
if [[ $ret == 0 && $KEYS == $'\4' ]]; then
exit
fi
if ((ret)); then if ((ret)); then
# TODO (fix): this is not equal to sending a SIGINT, since the status code ($?) is set to 1 instead of 130. # TODO (fix): this is not equal to sending a SIGINT, since the status code ($?) is set to 1 instead of 130.
zle .send-break zle .send-break
else
# Enter
zle .accept-line
fi 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. # Helper function for calling a widget before the specified OMP function.
@ -205,7 +219,7 @@ function _omp_create_widget() {
} }
function enable_poshtooltips() { function enable_poshtooltips() {
local widget=${$(bindkey " "):2} local widget=${$(bindkey ' '):2}
if [[ -z $widget ]]; then if [[ -z $widget ]]; then
widget=self-insert widget=self-insert

View file

@ -2,6 +2,8 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt"
"strings"
) )
//go:embed scripts/omp.tcsh //go:embed scripts/omp.tcsh
@ -10,12 +12,23 @@ var tcshInit string
func (f Feature) Tcsh() Code { func (f Feature) Tcsh() Code {
switch f { switch f {
case Upgrade: case Upgrade:
return "$POSH_COMMAND upgrade;" return `"$_omp_executable" upgrade;`
case Notice: case Notice:
return "$POSH_COMMAND notice;" return `"$_omp_executable" notice;`
case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, FTCSMarks, CursorPositioning: case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, FTCSMarks, CursorPositioning:
fallthrough fallthrough
default: default:
return "" 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))
}

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,8 +11,22 @@ func TestTcshFeatures(t *testing.T) {
got := allFeatures.Lines(TCSH).String("// these are the features") got := allFeatures.Lines(TCSH).String("// these are the features")
want := `// these are the features want := `// these are the features
$POSH_COMMAND upgrade; "$_omp_executable" upgrade;
$POSH_COMMAND notice;` "$_omp_executable" notice;`
assert.Equal(t, want, got) 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))
}
}

View file

@ -2,20 +2,34 @@ package shell
import ( import (
_ "embed" _ "embed"
"fmt"
"strings"
) )
//go:embed scripts/omp.py //go:embed scripts/omp.xsh
var xonshInit string var xonshInit string
func (f Feature) Xonsh() Code { func (f Feature) Xonsh() Code {
switch f { switch f {
case Upgrade: case Upgrade:
return "@($POSH_EXECUTABLE) upgrade" return "@(_omp_executable) upgrade"
case Notice: case Notice:
return "@($POSH_EXECUTABLE) notice" return "@(_omp_executable) notice"
case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, CursorPositioning, FTCSMarks: case PromptMark, RPrompt, PoshGit, Azure, LineError, Jobs, Tooltips, Transient, CursorPositioning, FTCSMarks:
fallthrough fallthrough
default: default:
return "" return ""
} }
} }
func quotePythonStr(str string) string {
if len(str) == 0 {
return "''"
}
return fmt.Sprintf("'%s'", strings.NewReplacer(
"'", `'"'"'`,
`\`, `\\`,
"\n", `\n`,
).Replace(str))
}

View file

@ -1,6 +1,7 @@
package shell package shell
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,8 +11,22 @@ func TestXonshFeatures(t *testing.T) {
got := allFeatures.Lines(XONSH).String("// these are the features") got := allFeatures.Lines(XONSH).String("// these are the features")
want := `// these are the features want := `// these are the features
@($POSH_EXECUTABLE) upgrade @(_omp_executable) upgrade
@($POSH_EXECUTABLE) notice` @(_omp_executable) notice`
assert.Equal(t, want, got) 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))
}
}

View file

@ -7,21 +7,16 @@ import (
//go:embed scripts/omp.zsh //go:embed scripts/omp.zsh
var zshInit string var zshInit string
const (
unixUpgrade = "$_omp_executable upgrade"
unixNotice = "$_omp_executable notice"
)
func (f Feature) Zsh() Code { func (f Feature) Zsh() Code {
switch f { switch f {
case CursorPositioning: case CursorPositioning:
return "_omp_cursor_positioning=1" return unixCursorPositioning
case Tooltips: case Tooltips:
return "enable_poshtooltips" return "enable_poshtooltips"
case Transient: case Transient:
return "_omp_create_widget zle-line-init _omp_zle-line-init" return "_omp_create_widget zle-line-init _omp_zle-line-init"
case FTCSMarks: case FTCSMarks:
return "_omp_ftcs_marks=1" return unixFTCSMarks
case Upgrade: case Upgrade:
return unixUpgrade return unixUpgrade
case Notice: case Notice:

View file

@ -13,8 +13,8 @@ func TestZshFeatures(t *testing.T) {
enable_poshtooltips enable_poshtooltips
_omp_create_widget zle-line-init _omp_zle-line-init _omp_create_widget zle-line-init _omp_zle-line-init
_omp_ftcs_marks=1 _omp_ftcs_marks=1
$_omp_executable upgrade "$_omp_executable" upgrade
$_omp_executable notice "$_omp_executable" notice
_omp_cursor_positioning=1` _omp_cursor_positioning=1`
assert.Equal(t, want, got) assert.Equal(t, want, got)

View file

@ -183,34 +183,29 @@ func ClearAfter() string {
} }
func FormatTitle(title string) string { func FormatTitle(title string) string {
switch Shell {
// These shells don't support setting the console title. // These shells don't support setting the console title.
if Shell == shell.ELVISH || Shell == shell.XONSH { case shell.ELVISH, shell.XONSH, shell.TCSH:
return "" 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 { s.WriteRune(char)
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) 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 { func EscapeText(text string) string {