mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
Working on bug fixes and UX. Streams restarting, fixed lots of bugs, timing issues, concurrency bugs. Get status shipped to the FE to drive "shield" state display. Deal with stale streams. Also big UX changes to the block headers. Specialize the terminal headers to prioritize the connection (sense of place), remove old terminal icon and word "Terminal" from the header. Also drop "Web" and "Preview" labels on web/preview blocks. Added `wsh focusblock` command.
640 lines
17 KiB
Go
640 lines
17 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package shellutil
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
"github.com/wavetermdev/waveterm/pkg/utilds"
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
|
)
|
|
|
|
var (
|
|
//go:embed shellintegration/zsh_zprofile.sh
|
|
ZshStartup_Zprofile string
|
|
|
|
//go:embed shellintegration/zsh_zshrc.sh
|
|
ZshStartup_Zshrc string
|
|
|
|
//go:embed shellintegration/zsh_zlogin.sh
|
|
ZshStartup_Zlogin string
|
|
|
|
//go:embed shellintegration/zsh_zshenv.sh
|
|
ZshStartup_Zshenv string
|
|
|
|
//go:embed shellintegration/bash_bashrc.sh
|
|
BashStartup_Bashrc string
|
|
|
|
//go:embed shellintegration/bash_preexec.sh
|
|
BashStartup_Preexec string
|
|
|
|
//go:embed shellintegration/fish_wavefish.sh
|
|
FishStartup_Wavefish string
|
|
|
|
//go:embed shellintegration/pwsh_wavepwsh.sh
|
|
PwshStartup_wavepwsh string
|
|
|
|
ZshExtendedHistoryPattern = regexp.MustCompile(`^: [0-9]+:`)
|
|
)
|
|
|
|
const DefaultTermType = "xterm-256color"
|
|
const DefaultTermRows = 24
|
|
const DefaultTermCols = 80
|
|
|
|
var cachedMacUserShell string
|
|
var macUserShellOnce = &sync.Once{}
|
|
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
|
|
|
|
var gitBashCache = utilds.MakeSyncCache(findInstalledGitBash)
|
|
|
|
const DefaultShellPath = "/bin/bash"
|
|
|
|
const (
|
|
ShellType_bash = "bash"
|
|
ShellType_zsh = "zsh"
|
|
ShellType_fish = "fish"
|
|
ShellType_pwsh = "pwsh"
|
|
ShellType_unknown = "unknown"
|
|
)
|
|
|
|
const (
|
|
// there must be no spaces in these integration dir paths
|
|
ZshIntegrationDir = "shell/zsh"
|
|
BashIntegrationDir = "shell/bash"
|
|
PwshIntegrationDir = "shell/pwsh"
|
|
FishIntegrationDir = "shell/fish"
|
|
WaveHomeBinDir = "bin"
|
|
ZshHistoryFileName = ".zsh_history"
|
|
)
|
|
|
|
func DetectLocalShellPath() string {
|
|
if runtime.GOOS == "windows" {
|
|
if pwshPath, lpErr := exec.LookPath("pwsh"); lpErr == nil {
|
|
return pwshPath
|
|
}
|
|
if powershellPath, lpErr := exec.LookPath("powershell"); lpErr == nil {
|
|
return powershellPath
|
|
}
|
|
return "powershell.exe"
|
|
}
|
|
shellPath := GetMacUserShell()
|
|
if shellPath == "" {
|
|
shellPath = os.Getenv("SHELL")
|
|
}
|
|
if shellPath == "" {
|
|
return DefaultShellPath
|
|
}
|
|
return shellPath
|
|
}
|
|
|
|
func GetMacUserShell() string {
|
|
if runtime.GOOS != "darwin" {
|
|
return ""
|
|
}
|
|
macUserShellOnce.Do(func() {
|
|
cachedMacUserShell = internalMacUserShell()
|
|
})
|
|
return cachedMacUserShell
|
|
}
|
|
|
|
// dscl . -read /Users/[username] UserShell
|
|
// defaults to /bin/bash
|
|
func internalMacUserShell() string {
|
|
osUser, err := user.Current()
|
|
if err != nil {
|
|
return DefaultShellPath
|
|
}
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancelFn()
|
|
userStr := "/Users/" + osUser.Username
|
|
out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput()
|
|
if err != nil {
|
|
return DefaultShellPath
|
|
}
|
|
outStr := strings.TrimSpace(string(out))
|
|
m := userShellRegexp.FindStringSubmatch(outStr)
|
|
if m == nil {
|
|
return DefaultShellPath
|
|
}
|
|
return m[1]
|
|
}
|
|
|
|
func hasDirPart(dir string, part string) bool {
|
|
dir = filepath.Clean(dir)
|
|
part = strings.ToLower(part)
|
|
for {
|
|
base := strings.ToLower(filepath.Base(dir))
|
|
if base == part {
|
|
return true
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
return false
|
|
}
|
|
|
|
func FindGitBash(config *wconfig.FullConfigType, rescan bool) string {
|
|
if runtime.GOOS != "windows" {
|
|
return ""
|
|
}
|
|
|
|
if config != nil && config.Settings.TermGitBashPath != "" {
|
|
return config.Settings.TermGitBashPath
|
|
}
|
|
|
|
path, _ := gitBashCache.Get(rescan)
|
|
return path
|
|
}
|
|
|
|
func findInstalledGitBash() (string, error) {
|
|
// Try PATH first (skip system32, and only accept if in a Git directory)
|
|
pathEnv := os.Getenv("PATH")
|
|
pathDirs := filepath.SplitList(pathEnv)
|
|
for _, dir := range pathDirs {
|
|
dir = strings.Trim(dir, `"`)
|
|
if hasDirPart(dir, "system32") {
|
|
continue
|
|
}
|
|
if !hasDirPart(dir, "git") {
|
|
continue
|
|
}
|
|
bashPath := filepath.Join(dir, "bash.exe")
|
|
if _, err := os.Stat(bashPath); err == nil {
|
|
return bashPath, nil
|
|
}
|
|
}
|
|
|
|
// Try scoop location
|
|
userProfile := os.Getenv("USERPROFILE")
|
|
if userProfile != "" {
|
|
scoopPath := filepath.Join(userProfile, "scoop", "apps", "git", "current", "bin", "bash.exe")
|
|
if _, err := os.Stat(scoopPath); err == nil {
|
|
return scoopPath, nil
|
|
}
|
|
}
|
|
|
|
// Try LocalAppData\programs\git\bin
|
|
localAppData := os.Getenv("LOCALAPPDATA")
|
|
if localAppData != "" {
|
|
localPath := filepath.Join(localAppData, "programs", "git", "bin", "bash.exe")
|
|
if _, err := os.Stat(localPath); err == nil {
|
|
return localPath, nil
|
|
}
|
|
}
|
|
|
|
// Try C:\Program Files\Git\bin
|
|
programFilesPath := filepath.Join("C:\\", "Program Files", "Git", "bin", "bash.exe")
|
|
if _, err := os.Stat(programFilesPath); err == nil {
|
|
return programFilesPath, nil
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func DefaultTermSize() waveobj.TermSize {
|
|
return waveobj.TermSize{Rows: DefaultTermRows, Cols: DefaultTermCols}
|
|
}
|
|
|
|
func WaveshellLocalEnvVars(termType string) map[string]string {
|
|
rtn := make(map[string]string)
|
|
if termType != "" {
|
|
rtn["TERM"] = termType
|
|
}
|
|
// these are not necessary since they should be set with the swap token, but no harm in setting them here
|
|
rtn["TERM_PROGRAM"] = "waveterm"
|
|
rtn["WAVETERM"], _ = os.Executable()
|
|
rtn["WAVETERM_VERSION"] = wavebase.WaveVersion
|
|
rtn["WAVETERM_WSHBINDIR"] = filepath.Join(wavebase.GetWaveDataDir(), WaveHomeBinDir)
|
|
return rtn
|
|
}
|
|
|
|
func UpdateCmdEnv(cmd *exec.Cmd, envVars map[string]string) {
|
|
if len(envVars) == 0 {
|
|
return
|
|
}
|
|
found := make(map[string]bool)
|
|
var newEnv []string
|
|
for _, envStr := range cmd.Env {
|
|
envKey := GetEnvStrKey(envStr)
|
|
newEnvVal, ok := envVars[envKey]
|
|
if ok {
|
|
found[envKey] = true
|
|
if newEnvVal != "" {
|
|
newEnv = append(newEnv, envKey+"="+newEnvVal)
|
|
}
|
|
} else {
|
|
newEnv = append(newEnv, envStr)
|
|
}
|
|
}
|
|
for envKey, envVal := range envVars {
|
|
if found[envKey] {
|
|
continue
|
|
}
|
|
newEnv = append(newEnv, envKey+"="+envVal)
|
|
}
|
|
cmd.Env = newEnv
|
|
}
|
|
|
|
func GetEnvStrKey(envStr string) string {
|
|
eqIdx := strings.Index(envStr, "=")
|
|
if eqIdx == -1 {
|
|
return envStr
|
|
}
|
|
return envStr[0:eqIdx]
|
|
}
|
|
|
|
var initStartupFilesOnce = &sync.Once{}
|
|
|
|
// in a Once block so it can be called multiple times
|
|
// we run it at startup, but also before launching local shells so we know everything is initialized before starting the shell
|
|
func InitCustomShellStartupFiles() error {
|
|
var err error
|
|
initStartupFilesOnce.Do(func() {
|
|
err = initCustomShellStartupFilesInternal()
|
|
})
|
|
return err
|
|
}
|
|
|
|
func GetLocalBashRcFileOverride() string {
|
|
return filepath.Join(wavebase.GetWaveDataDir(), BashIntegrationDir, ".bashrc")
|
|
}
|
|
|
|
func GetLocalWaveFishFilePath() string {
|
|
return filepath.Join(wavebase.GetWaveDataDir(), FishIntegrationDir, "wave.fish")
|
|
}
|
|
|
|
func GetLocalWavePowershellEnv() string {
|
|
return filepath.Join(wavebase.GetWaveDataDir(), PwshIntegrationDir, "wavepwsh.ps1")
|
|
}
|
|
|
|
func GetLocalZshZDotDir() string {
|
|
return filepath.Join(wavebase.GetWaveDataDir(), ZshIntegrationDir)
|
|
}
|
|
|
|
func HasWaveZshHistory() (bool, int64) {
|
|
zshDir := GetLocalZshZDotDir()
|
|
historyFile := filepath.Join(zshDir, ZshHistoryFileName)
|
|
fileInfo, err := os.Stat(historyFile)
|
|
if err != nil {
|
|
return false, 0
|
|
}
|
|
return true, fileInfo.Size()
|
|
}
|
|
|
|
func IsExtendedZshHistoryFile(fileName string) (bool, error) {
|
|
file, err := os.Open(fileName)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
defer file.Close()
|
|
|
|
buf := make([]byte, 1024)
|
|
n, err := file.Read(buf)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
content := string(buf[:n])
|
|
lines := strings.Split(content, "\n")
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
return ZshExtendedHistoryPattern.MatchString(line), nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func GetLocalWshBinaryPath(version string, goos string, goarch string) (string, error) {
|
|
ext := ""
|
|
if goarch == "amd64" {
|
|
goarch = "x64"
|
|
}
|
|
if goarch == "aarch64" {
|
|
goarch = "arm64"
|
|
}
|
|
if goos == "windows" {
|
|
ext = ".exe"
|
|
}
|
|
if !wavebase.SupportedWshBinaries[fmt.Sprintf("%s-%s", goos, goarch)] {
|
|
return "", fmt.Errorf("unsupported wsh platform: %s-%s", goos, goarch)
|
|
}
|
|
baseName := fmt.Sprintf("wsh-%s-%s.%s%s", version, goos, goarch, ext)
|
|
return filepath.Join(wavebase.GetWaveAppBinPath(), baseName), nil
|
|
}
|
|
|
|
// absWshBinDir must be an absolute, expanded path (no ~ or $HOME, etc.)
|
|
// it will be hard-quoted appropriately for the shell
|
|
func InitRcFiles(waveHome string, absWshBinDir string) error {
|
|
// ensure directories exist
|
|
zshDir := filepath.Join(waveHome, ZshIntegrationDir)
|
|
err := wavebase.CacheEnsureDir(zshDir, ZshIntegrationDir, 0755, ZshIntegrationDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bashDir := filepath.Join(waveHome, BashIntegrationDir)
|
|
err = wavebase.CacheEnsureDir(bashDir, BashIntegrationDir, 0755, BashIntegrationDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fishDir := filepath.Join(waveHome, FishIntegrationDir)
|
|
err = wavebase.CacheEnsureDir(fishDir, FishIntegrationDir, 0755, FishIntegrationDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pwshDir := filepath.Join(waveHome, PwshIntegrationDir)
|
|
err = wavebase.CacheEnsureDir(pwshDir, PwshIntegrationDir, 0755, PwshIntegrationDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var pathSep string
|
|
if runtime.GOOS == "windows" {
|
|
pathSep = ";"
|
|
} else {
|
|
pathSep = ":"
|
|
}
|
|
params := map[string]string{
|
|
"WSHBINDIR": HardQuote(absWshBinDir),
|
|
"WSHBINDIR_PWSH": HardQuotePowerShell(absWshBinDir),
|
|
"PATHSEP": pathSep,
|
|
}
|
|
|
|
// write files to directory
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zprofile"), ZshStartup_Zprofile, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing zsh-integration .zprofile: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshrc"), ZshStartup_Zshrc, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing zsh-integration .zshrc: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zlogin"), ZshStartup_Zlogin, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing zsh-integration .zlogin: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshenv"), ZshStartup_Zshenv, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing zsh-integration .zshenv: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(bashDir, ".bashrc"), BashStartup_Bashrc, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing bash-integration .bashrc: %v", err)
|
|
}
|
|
err = os.WriteFile(filepath.Join(bashDir, "bash_preexec.sh"), []byte(BashStartup_Preexec), 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing bash-integration bash_preexec.sh: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(fishDir, "wave.fish"), FishStartup_Wavefish, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing fish-integration wave.fish: %v", err)
|
|
}
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(pwshDir, "wavepwsh.ps1"), PwshStartup_wavepwsh, params)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing pwsh-integration wavepwsh.ps1: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func initCustomShellStartupFilesInternal() error {
|
|
log.Printf("initializing wsh and shell startup files\n")
|
|
waveDataHome := wavebase.GetWaveDataDir()
|
|
binDir := filepath.Join(waveDataHome, WaveHomeBinDir)
|
|
err := InitRcFiles(waveDataHome, binDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = wavebase.CacheEnsureDir(binDir, WaveHomeBinDir, 0755, WaveHomeBinDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// copy the correct binary to bin
|
|
wshFullPath, err := GetLocalWshBinaryPath(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH)
|
|
if err != nil {
|
|
log.Printf("error (non-fatal), could not resolve wsh binary path: %v\n", err)
|
|
}
|
|
if _, err := os.Stat(wshFullPath); err != nil {
|
|
log.Printf("error (non-fatal), could not resolve wsh binary %q: %v\n", wshFullPath, err)
|
|
return nil
|
|
}
|
|
wshDstPath := filepath.Join(binDir, "wsh")
|
|
if runtime.GOOS == "windows" {
|
|
wshDstPath = wshDstPath + ".exe"
|
|
}
|
|
err = utilfn.AtomicRenameCopy(wshDstPath, wshFullPath, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("error copying wsh binary to bin: %v", err)
|
|
}
|
|
wshBaseName := filepath.Base(wshFullPath)
|
|
log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath)
|
|
return nil
|
|
}
|
|
|
|
func GetShellTypeFromShellPath(shellPath string) string {
|
|
shellBase := filepath.Base(shellPath)
|
|
if strings.Contains(shellBase, "bash") {
|
|
return ShellType_bash
|
|
}
|
|
if strings.Contains(shellBase, "zsh") {
|
|
return ShellType_zsh
|
|
}
|
|
if strings.Contains(shellBase, "fish") {
|
|
return ShellType_fish
|
|
}
|
|
if strings.Contains(shellBase, "pwsh") || strings.Contains(shellBase, "powershell") {
|
|
return ShellType_pwsh
|
|
}
|
|
return ShellType_unknown
|
|
}
|
|
|
|
var (
|
|
bashVersionRegexp = regexp.MustCompile(`\bversion\s+(\d+\.\d+)`)
|
|
zshVersionRegexp = regexp.MustCompile(`\bzsh\s+(\d+\.\d+)`)
|
|
fishVersionRegexp = regexp.MustCompile(`\bversion\s+(\d+\.\d+)`)
|
|
pwshVersionRegexp = regexp.MustCompile(`(?:PowerShell\s+)?(\d+\.\d+)`)
|
|
)
|
|
|
|
func DetectShellTypeAndVersion() (string, string, error) {
|
|
shellPath := DetectLocalShellPath()
|
|
return DetectShellTypeAndVersionFromPath(shellPath)
|
|
}
|
|
|
|
func DetectShellTypeAndVersionFromPath(shellPath string) (string, string, error) {
|
|
shellType := GetShellTypeFromShellPath(shellPath)
|
|
if shellType == ShellType_unknown {
|
|
return shellType, "", fmt.Errorf("unknown shell type: %s", shellPath)
|
|
}
|
|
|
|
shellBase := filepath.Base(shellPath)
|
|
if shellType == ShellType_pwsh && strings.Contains(shellBase, "powershell") && !strings.Contains(shellBase, "pwsh") {
|
|
return "powershell", "", nil
|
|
}
|
|
|
|
version, err := getShellVersion(shellPath, shellType)
|
|
if err != nil {
|
|
return shellType, "", err
|
|
}
|
|
|
|
return shellType, version, nil
|
|
}
|
|
|
|
func getShellVersion(shellPath string, shellType string) (string, error) {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancelFn()
|
|
|
|
var cmd *exec.Cmd
|
|
var versionRegex *regexp.Regexp
|
|
|
|
switch shellType {
|
|
case ShellType_bash:
|
|
cmd = exec.CommandContext(ctx, shellPath, "--version")
|
|
versionRegex = bashVersionRegexp
|
|
case ShellType_zsh:
|
|
cmd = exec.CommandContext(ctx, shellPath, "--version")
|
|
versionRegex = zshVersionRegexp
|
|
case ShellType_fish:
|
|
cmd = exec.CommandContext(ctx, shellPath, "--version")
|
|
versionRegex = fishVersionRegexp
|
|
case ShellType_pwsh:
|
|
cmd = exec.CommandContext(ctx, shellPath, "--version")
|
|
versionRegex = pwshVersionRegexp
|
|
default:
|
|
return "", fmt.Errorf("unsupported shell type: %s", shellType)
|
|
}
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get version for %s: %w", shellType, err)
|
|
}
|
|
|
|
outputStr := strings.TrimSpace(string(output))
|
|
matches := versionRegex.FindStringSubmatch(outputStr)
|
|
if len(matches) < 2 {
|
|
return "", fmt.Errorf("failed to parse version from output: %q", outputStr)
|
|
}
|
|
|
|
return matches[1], nil
|
|
}
|
|
|
|
func FixupWaveZshHistory() error {
|
|
if runtime.GOOS != "darwin" {
|
|
return nil
|
|
}
|
|
|
|
hasHistory, size := HasWaveZshHistory()
|
|
if !hasHistory {
|
|
return nil
|
|
}
|
|
|
|
zshDir := GetLocalZshZDotDir()
|
|
waveHistFile := filepath.Join(zshDir, ZshHistoryFileName)
|
|
|
|
if size == 0 {
|
|
err := os.Remove(waveHistFile)
|
|
if err != nil {
|
|
log.Printf("error removing wave zsh history file %s: %v\n", waveHistFile, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
log.Printf("merging wave zsh history %s into ~/.zsh_history\n", waveHistFile)
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting home directory: %w", err)
|
|
}
|
|
realHistFile := filepath.Join(homeDir, ".zsh_history")
|
|
|
|
isExtended, err := IsExtendedZshHistoryFile(realHistFile)
|
|
if err != nil {
|
|
return fmt.Errorf("error checking if history is extended: %w", err)
|
|
}
|
|
|
|
hasExtendedStr := "false"
|
|
if isExtended {
|
|
hasExtendedStr = "true"
|
|
}
|
|
|
|
quotedWaveHistFile := utilfn.ShellQuote(waveHistFile, true, -1)
|
|
|
|
script := fmt.Sprintf(`
|
|
HISTFILE=~/.zsh_history
|
|
HISTSIZE=999999
|
|
SAVEHIST=999999
|
|
has_extended_history=%s
|
|
[[ $has_extended_history == true ]] && setopt EXTENDED_HISTORY
|
|
fc -RI
|
|
fc -RI %s
|
|
fc -W
|
|
`, hasExtendedStr, quotedWaveHistFile)
|
|
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelFn()
|
|
|
|
cmd := exec.CommandContext(ctx, "zsh", "-f", "-i", "-c", script)
|
|
cmd.Stdin = nil
|
|
envStr := envutil.SliceToEnv(os.Environ())
|
|
envStr = envutil.RmEnv(envStr, "ZDOTDIR")
|
|
cmd.Env = envutil.EnvToSlice(envStr)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("error executing zsh history fixup script: %w, output: %s", err, string(output))
|
|
}
|
|
|
|
err = os.Remove(waveHistFile)
|
|
if err != nil {
|
|
log.Printf("error removing wave zsh history file %s: %v\n", waveHistFile, err)
|
|
}
|
|
log.Printf("successfully merged wave zsh history %s into ~/.zsh_history\n", waveHistFile)
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetTerminalResetSeq() string {
|
|
resetSeq := "\x1b[0m" // reset attributes
|
|
resetSeq += "\x1b[?25h" // show cursor
|
|
resetSeq += "\x1b[?1l" // normal cursor keys
|
|
resetSeq += "\x1b[?7h" // wraparound on
|
|
resetSeq += "\x1b[?1000l" // disable mouse tracking
|
|
resetSeq += "\x1b[?1007l" // disable alternate scroll mode
|
|
resetSeq += "\x1b[?1004l" // disable focus reporting (FocusIn/FocusOut)
|
|
resetSeq += "\x1b[?2004l" // disable bracketed paste mode
|
|
resetSeq += FormatOSC(16162, "R") // disable alternate screen mode
|
|
return resetSeq
|
|
}
|
|
|
|
func FormatOSC(oscNum int, parts ...string) string {
|
|
if len(parts) == 0 {
|
|
return fmt.Sprintf("\x1b]%d\x07", oscNum)
|
|
}
|
|
return fmt.Sprintf("\x1b]%d;%s\x07", oscNum, strings.Join(parts, ";"))
|
|
}
|