diff --git a/.gitignore b/.gitignore index 36f971e..dc4d504 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin/* +.vscode/* \ No newline at end of file diff --git a/Makefile b/Makefile index 9b86acd..cc67099 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,8 @@ sanitize: # # -build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon bin/resh-evaluate bin/resh-sanitize bin/resh-control +build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\ + bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect test_go: # Running tests @@ -58,7 +59,7 @@ rebuild: make build clean: - rm resh-* + rm bin/resh-* install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/config.toml scripts/uuid.sh \ | $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d @@ -82,15 +83,20 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c [ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json # Adding resh shellrc to .bashrc ... grep '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\ - echo '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' >> ~/.bashrc + echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' >> ~/.bashrc # Adding bash-preexec to .bashrc ... grep '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\ - echo '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc + echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc # Adding resh shellrc to .zshrc ... grep '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\ - echo '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' >> ~/.zshrc + echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' >> ~/.zshrc + @# Deleting zsh completion cache - for future use + @# [ ! -e ~/.zcompdump ] || rm ~/.zcompdump # Restarting resh daemon ... - -[ ! -f ~/.resh/resh.pid ] || kill -SIGTERM $$(cat ~/.resh/resh.pid) + -if [ -f ~/.resh/resh.pid ]; then\ + kill -SIGTERM $$(cat ~/.resh/resh.pid);\ + rm ~/.resh/resh.pid;\ + fi nohup resh-daemon &>/dev/null & disown # Reloading rc files . ~/.resh/shellrc @@ -131,9 +137,7 @@ uninstall: # Uninstalling ... -rm -rf ~/.resh/ -bin/resh-control: cmd/control/cmd/*.go - -bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION +bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION cmd/control/cmd/*.go cmd/control/status/status.go go build ${GOFLAGS} -o $@ cmd/$*/*.go $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d: diff --git a/VERSION b/VERSION index 227cea2..7ec1d6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.1.0 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 5699ac6..49ccbde 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -169,74 +169,79 @@ func main() { // *osReleasePrettyName = "Linux" // } - rec := records.Record{ - // posix - Cols: *cols, - Lines: *lines, - // core - BaseRecord: records.BaseRecord{ - RecallHistno: *recallHistno, - - CmdLine: *cmdLine, - ExitCode: *exitCode, - Shell: *shell, - Uname: *uname, - SessionID: *sessionID, - - // posix - Home: *home, - Lang: *lang, - LcAll: *lcAll, - Login: *login, - // Path: *path, - Pwd: *pwd, - ShellEnv: *shellEnv, - Term: *term, - - // non-posix - RealPwd: realPwd, - Pid: *pid, - SessionPID: *sessionPid, - Host: *host, - Hosttype: *hosttype, - Ostype: *ostype, - Machtype: *machtype, - Shlvl: *shlvl, - - // before after - TimezoneBefore: *timezoneBefore, - - RealtimeBefore: realtimeBefore, - RealtimeBeforeLocal: realtimeBeforeLocal, - - RealtimeSinceSessionStart: realtimeSinceSessionStart, - RealtimeSinceBoot: realtimeSinceBoot, - - GitDir: gitDir, - GitRealDir: gitRealDir, - GitOriginRemote: *gitRemote, - MachineID: collect.ReadFileContent(machineIDPath), - - OsReleaseID: *osReleaseID, - OsReleaseVersionID: *osReleaseVersionID, - OsReleaseIDLike: *osReleaseIDLike, - OsReleaseName: *osReleaseName, - OsReleasePrettyName: *osReleasePrettyName, - - PartOne: true, - - ReshUUID: collect.ReadFileContent(reshUUIDPath), - ReshVersion: Version, - ReshRevision: Revision, - - RecallActionsRaw: *recallActions, - RecallPrefix: *recallPrefix, - RecallStrategy: *recallStrategy, - }, - } if *recall { + rec := records.SlimRecord{ + SessionID: *sessionID, + RecallHistno: *recallHistno, + RecallPrefix: *recallPrefix, + } fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port))) } else { + rec := records.Record{ + // posix + Cols: *cols, + Lines: *lines, + // core + BaseRecord: records.BaseRecord{ + RecallHistno: *recallHistno, + + CmdLine: *cmdLine, + ExitCode: *exitCode, + Shell: *shell, + Uname: *uname, + SessionID: *sessionID, + + // posix + Home: *home, + Lang: *lang, + LcAll: *lcAll, + Login: *login, + // Path: *path, + Pwd: *pwd, + ShellEnv: *shellEnv, + Term: *term, + + // non-posix + RealPwd: realPwd, + Pid: *pid, + SessionPID: *sessionPid, + Host: *host, + Hosttype: *hosttype, + Ostype: *ostype, + Machtype: *machtype, + Shlvl: *shlvl, + + // before after + TimezoneBefore: *timezoneBefore, + + RealtimeBefore: realtimeBefore, + RealtimeBeforeLocal: realtimeBeforeLocal, + + RealtimeSinceSessionStart: realtimeSinceSessionStart, + RealtimeSinceBoot: realtimeSinceBoot, + + GitDir: gitDir, + GitRealDir: gitRealDir, + GitOriginRemote: *gitRemote, + MachineID: collect.ReadFileContent(machineIDPath), + + OsReleaseID: *osReleaseID, + OsReleaseVersionID: *osReleaseVersionID, + OsReleaseIDLike: *osReleaseIDLike, + OsReleaseName: *osReleaseName, + OsReleasePrettyName: *osReleasePrettyName, + + PartOne: true, + + ReshUUID: collect.ReadFileContent(reshUUIDPath), + ReshVersion: Version, + ReshRevision: Revision, + + RecallActionsRaw: *recallActions, + RecallPrefix: *recallPrefix, + RecallStrategy: *recallStrategy, + }, + } collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") } } diff --git a/cmd/config/main.go b/cmd/config/main.go new file mode 100644 index 0000000..49fe9ee --- /dev/null +++ b/cmd/config/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" +) + +func main() { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, ".config/resh.toml") + + var config cfg.Config + _, err := toml.DecodeFile(configPath, &config) + if err != nil { + fmt.Println("Error reading config", err) + os.Exit(1) + } + + configKey := flag.String("key", "", "Key of the requested config entry") + flag.Parse() + + if *configKey == "" { + fmt.Println("Error: expected option --key!") + os.Exit(1) + } + + switch *configKey { + case "BindArrowKeysBash": + fallthrough + case "bindArrowKeysBash": + printBoolNormalized(config.BindArrowKeysBash) + case "BindArrowKeysZsh": + fallthrough + case "bindArrowKeysZsh": + printBoolNormalized(config.BindArrowKeysZsh) + default: + fmt.Println("Error: illegal --key!") + os.Exit(1) + } +} + +// this might be unnecessary but I'm too lazy to look it up +func printBoolNormalized(x bool) { + if x { + fmt.Println("true") + } else { + fmt.Println("false") + } +} diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go index bf09a67..e80fa56 100644 --- a/cmd/control/cmd/completion.go +++ b/cmd/control/cmd/completion.go @@ -10,20 +10,20 @@ import ( // completionCmd represents the completion command var completionCmd = &cobra.Command{ Use: "completion", - Short: "Generates bash/zsh completion scripts", + Short: "generate bash/zsh completion scripts", Long: `To load completion run . <(reshctl completion bash) OR -. <(reshctl completion zsh) +. <(reshctl completion zsh) && compdef _reshctl reshctl `, } var completionBashCmd = &cobra.Command{ Use: "bash", - Short: "Generates bash completion scripts", + Short: "generate bash completion scripts", Long: `To load completion run . <(reshctl completion bash) @@ -36,10 +36,10 @@ var completionBashCmd = &cobra.Command{ var completionZshCmd = &cobra.Command{ Use: "zsh", - Short: "Generates zsh completion scripts", + Short: "generate zsh completion scripts", Long: `To load completion run -. <(reshctl completion zsh) +. <(reshctl completion zsh) && compdef _reshctl reshctl `, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenZshCompletion(os.Stdout) diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go index fc10550..951cc66 100644 --- a/cmd/control/cmd/debug.go +++ b/cmd/control/cmd/debug.go @@ -12,22 +12,30 @@ import ( var debugCmd = &cobra.Command{ Use: "debug", - Short: "Debug utils for resh", + Short: "debug utils for resh", Long: "Reloads resh rc files. Shows logs and output from last runs of resh", } var debugReloadCmd = &cobra.Command{ Use: "reload", - Short: "Reload resh rc files", + Short: "reload resh rc files", Long: "Reload resh rc files", Run: func(cmd *cobra.Command, args []string) { exitCode = status.ReloadRcFiles }, } +var debugInspectCmd = &cobra.Command{ + Use: "inspect", + Short: "inspect session history", + Run: func(cmd *cobra.Command, args []string) { + exitCode = status.InspectSessionHistory + }, +} + var debugOutputCmd = &cobra.Command{ Use: "output", - Short: "Shows output from last runs of resh", + Short: "shows output from last runs of resh", Long: "Shows output from last runs of resh", Run: func(cmd *cobra.Command, args []string) { files := []string{ diff --git a/cmd/control/cmd/disable.go b/cmd/control/cmd/disable.go index b0dead5..f661585 100644 --- a/cmd/control/cmd/disable.go +++ b/cmd/control/cmd/disable.go @@ -7,18 +7,24 @@ import ( var disableCmd = &cobra.Command{ Use: "disable", - Short: "disable RESH features", - Long: `Disables RESH bindings for arrows and C-R.`, + Short: "disable RESH features (arrow key bindings)", +} + +var disableArrowKeyBindingsCmd = &cobra.Command{ + Use: "arrow_key_bindings", + Short: "disable bindings for arrow keys (up/down) FOR THIS SHELL SESSION", Run: func(cmd *cobra.Command, args []string) { - exitCode = status.DisableAll + exitCode = status.DisableArrowKeyBindings }, } -// var disableRecallingCmd = &cobra.Command{ -// Use: "keybind", -// Short: "Disables RESH bindings for arrows and C-R.", -// Long: `Disables RESH bindings for arrows and C-R.`, -// Run: func(cmd *cobra.Command, args []string) { -// exitCode = status.DisableAll -// }, -// } +var disableArrowKeyBindingsGlobalCmd = &cobra.Command{ + Use: "arrow_key_bindings_global", + Short: "disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS", + Long: "Disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" + + "Note that this only affects sessions of the same shell.\n" + + "(e.g. running this in zsh will only affect future zsh sessions)", + Run: func(cmd *cobra.Command, args []string) { + exitCode = enableDisableArrowKeyBindingsGlobally(false) + }, +} diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go index ab4b144..f2913a4 100644 --- a/cmd/control/cmd/enable.go +++ b/cmd/control/cmd/enable.go @@ -1,24 +1,107 @@ package cmd import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/BurntSushi/toml" "github.com/curusarn/resh/cmd/control/status" + "github.com/curusarn/resh/pkg/cfg" "github.com/spf13/cobra" ) var enableCmd = &cobra.Command{ Use: "enable", - Short: "enable RESH features", - Long: `Enables RESH bindings for arrows and C-R.`, + Short: "enable RESH features (arrow key bindings)", +} + +var enableArrowKeyBindingsCmd = &cobra.Command{ + Use: "arrow_key_bindings", + Short: "enable bindings for arrow keys (up/down) FOR THIS SHELL SESSION", Run: func(cmd *cobra.Command, args []string) { - exitCode = status.EnableAll + exitCode = status.EnableArrowKeyBindings }, } -// var enableRecallingCmd = &cobra.Command{ -// Use: "keybind", -// Short: "Enables RESH bindings for arrows and C-R.", -// Long: `Enables RESH bindings for arrows and C-R.`, -// Run: func(cmd *cobra.Command, args []string) { -// exitCode = status.EnableAll -// }, -// } +var enableArrowKeyBindingsGlobalCmd = &cobra.Command{ + Use: "arrow_key_bindings_global", + Short: "enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS", + Long: "Enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" + + "Note that this only affects sessions of the same shell.\n" + + "(e.g. running this in zsh will only affect future zsh sessions)", + Run: func(cmd *cobra.Command, args []string) { + exitCode = enableDisableArrowKeyBindingsGlobally(true) + }, +} + +func enableDisableArrowKeyBindingsGlobally(value bool) status.Code { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, ".config/resh.toml") + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + fmt.Println("Error reading config", err) + return status.Fail + } + shell, found := os.LookupEnv("__RESH_ctl_shell") + // shell env variable must be set and must be equal to either bash or zsh + if found == false || (shell != "bash" && shell != "zsh") { + fmt.Println("Error while determining a shell you are using - your RESH instalation is probably broken. Please reinstall RESH - exiting!") + fmt.Println("found=", found, "shell=", shell) + return status.Fail + } + if shell == "bash" { + err := setConfigBindArrowKey(configPath, &config, &config.BindArrowKeysBash, shell, value) + if err != nil { + return status.Fail + } + } else if shell == "zsh" { + err := setConfigBindArrowKey(configPath, &config, &config.BindArrowKeysZsh, shell, value) + if err != nil { + return status.Fail + } + } else { + fmt.Println("FATAL ERROR while determining a shell you are using - your RESH instalation is probably broken. Please reinstall RESH - exiting!") + } + return status.Success +} + +// I don't like the interface this function has - passing both config structure and a part of it feels wrong +// It's ugly and could lead to future errors +func setConfigBindArrowKey(configPath string, config *cfg.Config, configField *bool, shell string, value bool) error { + if *configField == value { + if value { + fmt.Println("The RESH arrow key bindings are ALREADY GLOBALLY ENABLED for all future " + shell + " sessions - nothing to do - exiting.") + } else { + fmt.Println("The RESH arrow key bindings are ALREADY GLOBALLY DISABLED for all future " + shell + " sessions - nothing to do - exiting.") + } + return nil + } + if value { + fmt.Println("ENABLING the RESH arrow key bindings GLOBALLY (in " + shell + ") ...") + } else { + fmt.Println("DISABLING the RESH arrow key bindings GLOBALLY (in " + shell + ") ...") + } + *configField = value + + f, err := os.Create(configPath) + if err != nil { + fmt.Println("Error: Failed to create/open file:", configPath, "; error:", err) + return err + } + defer f.Close() + if err := toml.NewEncoder(f).Encode(config); err != nil { + fmt.Println("Error: Failed to encode and write the config values to hdd. error:", err) + return err + } + if value { + fmt.Println("SUCCESSFULLY ENABLED the RESH arrow key bindings GLOBALLY (in " + shell + ") " + + "- every new (" + shell + ") session will start with enabled RESH arrow key bindings!") + } else { + fmt.Println("SUCCESSFULLY DISABLED the RESH arrow key bindings GLOBALLY (in " + shell + ") " + + "- every new (" + shell + ") session will start with " + shell + " default arrow key bindings!") + } + return nil +} diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index a00d342..518e1b2 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -11,17 +11,18 @@ var exitCode status.Code var rootCmd = &cobra.Command{ Use: "reshctl", - Short: "Reshctl (RESH control) - enables you to enable/disable features and more.", - Long: `Enables you to enable/disable RESH bindings for arrows and C-R.`, + Short: "Reshctl (RESH control) - enable/disable RESH features and more.", } // Execute reshctl func Execute() status.Code { rootCmd.AddCommand(disableCmd) - // disableCmd.AddCommand(disableRecallingCmd) + disableCmd.AddCommand(disableArrowKeyBindingsCmd) + disableCmd.AddCommand(disableArrowKeyBindingsGlobalCmd) rootCmd.AddCommand(enableCmd) - // enableCmd.AddCommand(enableRecallingCmd) + enableCmd.AddCommand(enableArrowKeyBindingsCmd) + enableCmd.AddCommand(enableArrowKeyBindingsGlobalCmd) rootCmd.AddCommand(completionCmd) completionCmd.AddCommand(completionBashCmd) @@ -29,6 +30,7 @@ func Execute() status.Code { rootCmd.AddCommand(debugCmd) debugCmd.AddCommand(debugReloadCmd) + debugCmd.AddCommand(debugInspectCmd) debugCmd.AddCommand(debugOutputCmd) if err := rootCmd.Execute(); err != nil { fmt.Println(err) diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go index 8b9eeec..2d3bf37 100644 --- a/cmd/control/status/status.go +++ b/cmd/control/status/status.go @@ -8,10 +8,12 @@ const ( Success Code = 0 // Fail exit code Fail = 1 - // EnableAll exit code - tells reshctl() wrapper to enable_all - EnableAll = 100 - // DisableAll exit code - tells reshctl() wrapper to disable_all - DisableAll = 110 + // EnableArrowKeyBindings exit code - tells reshctl() wrapper to enable arrow key bindings + EnableArrowKeyBindings = 101 + // DisableArrowKeyBindings exit code - tells reshctl() wrapper to disable arrow key bindings + DisableArrowKeyBindings = 111 // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file ReloadRcFiles = 200 + // InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon + InspectSessionHistory = 201 ) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index a0ff6df..8045c5c 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -31,7 +31,9 @@ func main() { dir := usr.HomeDir pidfilePath := filepath.Join(dir, ".resh/resh.pid") configPath := filepath.Join(dir, ".config/resh.toml") - historyPath := filepath.Join(dir, ".resh_history.json") + reshHistoryPath := filepath.Join(dir, ".resh_history.json") + bashHistoryPath := filepath.Join(dir, ".bash_history") + zshHistoryPath := filepath.Join(dir, ".zsh_history") logPath := filepath.Join(dir, ".resh/daemon.log") f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) @@ -48,6 +50,10 @@ func main() { log.Println("Error reading config", err) return } + if config.Debug { + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + } + res, err := isDaemonRunning(config.Port) if err != nil { log.Println("Error while checking if the daemon is runnnig", err) @@ -69,11 +75,13 @@ func main() { if err != nil { log.Fatal("Could not create pidfile", err) } - runServer(config, historyPath) + runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath) + log.Println("main: Removing pidfile ...") err = os.Remove(pidfilePath) if err != nil { log.Println("Could not delete pidfile", err) } + log.Println("main: Shutdown - bye") } func statusHandler(w http.ResponseWriter, r *http.Request) { @@ -92,7 +100,7 @@ func killDaemon(pidfile string) error { if err != nil { log.Fatal("Pidfile contents are malformed", err) } - cmd := exec.Command("kill", strconv.Itoa(pid)) + cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) err = cmd.Run() if err != nil { log.Printf("Command finished with error: %v", err) diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index 7c5efeb..43e4e6b 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/curusarn/resh/pkg/collect" + "github.com/curusarn/resh/pkg/msg" "github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/sesshist" ) @@ -16,26 +17,31 @@ type recallHandler struct { } func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("/recall START") + log.Println("/recall reading body ...") jsn, err := ioutil.ReadAll(r.Body) if err != nil { log.Println("Error reading the body", err) return } - rec := records.Record{} + rec := records.SlimRecord{} + log.Println("/recall unmarshaling record ...") err = json.Unmarshal(jsn, &rec) if err != nil { log.Println("Decoding error:", err) log.Println("Payload:", jsn) return } + log.Println("/recall recalling ...") cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno, rec.RecallPrefix) if err != nil { log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ERROR") log.Println("Recall error:", err) return } - resp := collect.SingleResponse{cmd} + resp := collect.SingleResponse{CmdLine: cmd} + log.Println("/recall marshaling response ...") jsn, err = json.Marshal(&resp) if err != nil { log.Println("Encoding error:", err) @@ -43,6 +49,49 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } log.Println(string(jsn)) + log.Println("/recall writing response ...") w.Write(jsn) - log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) + log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) +} + +type inspectHandler struct { + sesshistDispatch *sesshist.Dispatch +} + +func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("/inspect START") + log.Println("/inspect reading body ...") + jsn, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading the body", err) + return + } + + mess := msg.InspectMsg{} + log.Println("/inspect unmarshaling record ...") + err = json.Unmarshal(jsn, &mess) + if err != nil { + log.Println("Decoding error:", err) + log.Println("Payload:", jsn) + return + } + log.Println("/inspect recalling ...") + cmds, err := h.sesshistDispatch.Inspect(mess.SessionID, int(mess.Count)) + if err != nil { + log.Println("/inspect - sess id:", mess.SessionID, " - count:", mess.Count, " -> ERROR") + log.Println("Inspect error:", err) + return + } + resp := msg.MultiResponse{CmdLines: cmds} + log.Println("/inspect marshaling response ...") + jsn, err = json.Marshal(&resp) + if err != nil { + log.Println("Encoding error:", err) + log.Println("Response:", resp) + return + } + // log.Println(string(jsn)) + log.Println("/inspect writing response ...") + w.Write(jsn) + log.Println("/inspect END - sess id:", mess.SessionID, " - count:", mess.Count) } diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 6eb44b7..d1a87b7 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -2,6 +2,7 @@ package main import ( "net/http" + "os" "strconv" "github.com/curusarn/resh/pkg/cfg" @@ -9,12 +10,16 @@ import ( "github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/sesshist" "github.com/curusarn/resh/pkg/sesswatch" + "github.com/curusarn/resh/pkg/signalhandler" ) -func runServer(config cfg.Config, historyPath string) { +func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) { var recordSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record var sessionDropSubscribers []chan string + var signalSubscribers []chan os.Signal + + shutdown := make(chan string) // sessshist sesshistSessionsToInit := make(chan records.Record) @@ -29,10 +34,19 @@ func runServer(config cfg.Config, historyPath string) { recordSubscribers = append(recordSubscribers, histfileRecords) histfileSessionsToDrop := make(chan string) sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) - histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop) + histfileSignals := make(chan os.Signal) + signalSubscribers = append(signalSubscribers, histfileSignals) + maxHistSize := 10000 // lines + minHistSizeKB := 2000 // roughly lines + histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop, + reshHistoryPath, bashHistoryPath, zshHistoryPath, + maxHistSize, minHistSizeKB, + histfileSignals, shutdown) // sesshist New - sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords, histfileBox, config.SesshistInitHistorySize) + sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, + sesshistRecords, histfileBox, + config.SesshistInitHistorySize) // sesswatch sesswatchSessionsToWatch := make(chan records.Record) @@ -40,9 +54,16 @@ func runServer(config cfg.Config, historyPath string) { sesswatch.Go(sesswatchSessionsToWatch, sessionDropSubscribers, config.SesswatchPeriodSeconds) // handlers - http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) - http.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) - http.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) - http.ListenAndServe(":"+strconv.Itoa(config.Port), nil) + mux := http.NewServeMux() + mux.HandleFunc("/status", statusHandler) + mux.Handle("/record", &recordHandler{subscribers: recordSubscribers}) + mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) + mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) + mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) + + server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux} + go server.ListenAndServe() + + // signalhandler - takes over the main goroutine so when signal handler exists the whole program exits + signalhandler.Run(signalSubscribers, shutdown, server) } diff --git a/cmd/inspect/main.go b/cmd/inspect/main.go new file mode 100644 index 0000000..4edc41e --- /dev/null +++ b/cmd/inspect/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/msg" + + "os/user" + "path/filepath" + "strconv" +) + +// Version from git set during build +var Version string + +// Revision from git set during build +var Revision string + +func main() { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, "/.config/resh.toml") + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + + sessionID := flag.String("sessionID", "", "resh generated session id") + count := flag.Uint("count", 10, "Number of cmdLines to return") + flag.Parse() + + if *sessionID == "" { + fmt.Println("Error: you need to specify sessionId") + } + + m := msg.InspectMsg{SessionID: *sessionID, Count: *count} + resp := SendInspectMsg(m, strconv.Itoa(config.Port)) + for _, cmdLine := range resp.CmdLines { + fmt.Println("`" + cmdLine + "'") + } +} + +// SendInspectMsg to daemon +func SendInspectMsg(m msg.InspectMsg, port string) msg.MultiResponse { + recJSON, err := json.Marshal(m) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/inspect", + bytes.NewBuffer(recJSON)) + if err != nil { + log.Fatal("send err 2", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatal("resh-daemon is not running :(") + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal("read response error") + } + // log.Println(string(body)) + response := msg.MultiResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + return response +} diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go index a7415b1..e5630de 100644 --- a/cmd/sanitize/main.go +++ b/cmd/sanitize/main.go @@ -173,11 +173,42 @@ func (s *sanitizer) sanitizeRecord(record *records.Record) error { log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) } + if len(record.RecallActionsRaw) > 0 { + record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw) + if err != nil { + log.Fatal("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) + } + } // add a flag to signify that the record has been sanitized record.Sanitized = true return nil } +// sanitizes the recall actions by replacing the recall prefix with it's length +func (s *sanitizer) sanitizeRecallActions(str string) (string, error) { + sanStr := "" + for x, actionStr := range strings.Split(str, ";") { + if x == 0 { + continue + } + if len(actionStr) == 0 { + return str, errors.New("Action can't be empty; idx=" + strconv.Itoa(x)) + } + fields := strings.Split(actionStr, ":") + if len(fields) != 2 { + return str, errors.New("Action should have exactly one ':' - encountered:" + actionStr) + } + action := fields[0] + if action != "arrow_up" && action != "arrow_down" { + return str, errors.New("Action (part 1) should be either 'arrow_up' or 'arrow_down' - encountered:" + action) + } + prefix := fields[1] + sanPrefix := strconv.Itoa(len(prefix)) + sanStr += ";" + action + ":" + sanPrefix + } + return sanStr, nil +} + func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ... const optionAllowedChars = "-_" // characters commonly found inside of options diff --git a/conf/config.toml b/conf/config.toml index 1970f34..7dc647c 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1,3 +1,6 @@ port = 2627 sesswatchPeriodSeconds = 120 sesshistInitHistorySize = 1000 +debug = true +bindArrowKeysBash = false +bindArrowKeysZsh = true diff --git a/go.mod b/go.mod index 910abe6..75c5200 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,11 @@ go 1.12 require ( github.com/BurntSushi/toml v0.3.1 - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 github.com/mattn/go-shellwords v1.0.6 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b github.com/schollz/progressbar v1.0.0 github.com/spf13/cobra v0.0.5 - github.com/wcharczuk/go-chart v2.0.1+incompatible github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa - golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 // indirect ) diff --git a/go.sum b/go.sum index beb087d..3252295 100644 --- a/go.sum +++ b/go.sum @@ -7,9 +7,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= @@ -37,15 +36,12 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/wcharczuk/go-chart v2.0.1+incompatible h1:0pz39ZAycJFF7ju/1mepnk26RLVLBCWz1STcD3doU0A= -github.com/wcharczuk/go-chart v2.0.1+incompatible/go.mod h1:PF5tmL4EIx/7Wf+hEkpCqYi5He4u90sw+0+6FhrryuE= github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa h1:rW+Lu6281ed/4XGuVIa4/YebTRNvoUJlfJ44ktEVwZk= github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 h1:4dQcAORh9oYBwVSBVIkP489LUPC+f1HBkTYXgmqfR+o= -golang.org/x/image v0.0.0-20190902063713-cb417be4ba39/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 16726bb..2cfdbc2 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -5,4 +5,7 @@ type Config struct { Port int SesswatchPeriodSeconds uint SesshistInitHistorySize int + Debug bool + BindArrowKeysBash bool + BindArrowKeysZsh bool } diff --git a/pkg/collect/collect.go b/pkg/collect/collect.go index 7f26b4e..56ee374 100644 --- a/pkg/collect/collect.go +++ b/pkg/collect/collect.go @@ -19,7 +19,7 @@ type SingleResponse struct { } // SendRecallRequest to daemon -func SendRecallRequest(r records.Record, port string) string { +func SendRecallRequest(r records.SlimRecord, port string) string { recJSON, err := json.Marshal(r) if err != nil { log.Fatal("send err 1", err) diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 5964603..d2740d6 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -3,10 +3,12 @@ package histfile import ( "encoding/json" "log" + "math" "os" "strconv" "sync" + "github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/records" ) @@ -16,29 +18,71 @@ type Histfile struct { sessions map[string]records.Record historyPath string - recentMutex sync.Mutex - recentRecords []records.Record - recentCmdLines []string // deduplicated - cmdLinesLastIndex map[string]int + recentMutex sync.Mutex + recentRecords []records.Record + + // NOTE: we have separate histories which only differ if there was not enough resh_history + // resh_history itself is common for both bash and zsh + bashCmdLines histlist.Histlist + zshCmdLines histlist.Histlist } -// New creates new histfile and runs two gorutines on it -func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop chan string) *Histfile { +// New creates new histfile and runs its gorutines +func New(input chan records.Record, sessionsToDrop chan string, + reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, + maxInitHistSize int, minInitHistSizeKB int, + signals chan os.Signal, shutdownDone chan string) *Histfile { + hf := Histfile{ - sessions: map[string]records.Record{}, - historyPath: historyPath, - cmdLinesLastIndex: map[string]int{}, + sessions: map[string]records.Record{}, + historyPath: reshHistoryPath, + bashCmdLines: histlist.New(), + zshCmdLines: histlist.New(), } - go hf.loadHistory(initHistSize) - go hf.writer(input) + go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) + go hf.writer(input, signals, shutdownDone) go hf.sessionGC(sessionsToDrop) return &hf } -func (h *Histfile) loadHistory(initHistSize int) { +// loadsHistory from resh_history and if there is not enough of it also load native shell histories +func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { h.recentMutex.Lock() defer h.recentMutex.Unlock() - h.recentCmdLines = records.LoadCmdLinesFromFile(h.historyPath, initHistSize) + log.Println("histfile: Checking if resh_history is large enough ...") + fi, err := os.Stat(h.historyPath) + var size int + if err != nil { + log.Println("histfile ERROR: failed to stat resh_history file:", err) + } else { + size = int(fi.Size()) + } + useNativeHistories := false + if size/1024 < minInitHistSizeKB { + useNativeHistories = true + log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...") + h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath) + log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List)) + h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath) + log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List)) + // no maxInitHistSize when using native histories + maxInitHistSize = math.MaxInt32 + } + log.Println("histfile: Loading resh history from file ...") + reshCmdLines := histlist.New() + // NOTE: keeping this weird interface for now because we might use it in the future + // when we only load bash or zsh history + records.LoadCmdLinesFromFile(&reshCmdLines, h.historyPath, maxInitHistSize) + log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List)) + if useNativeHistories == false { + h.bashCmdLines = reshCmdLines + h.zshCmdLines = histlist.Copy(reshCmdLines) + return + } + h.bashCmdLines.AddHistlist(reshCmdLines) + log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List)) + h.zshCmdLines.AddHistlist(reshCmdLines) + log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List)) } // sessionGC reads sessionIDs from channel and deletes them from histfile struct @@ -61,33 +105,52 @@ func (h *Histfile) sessionGC(sessionsToDrop chan string) { } // writer reads records from channel, merges them and writes them to file -func (h *Histfile) writer(input chan records.Record) { +func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) { for { func() { - record := <-input - h.sessionsMutex.Lock() - defer h.sessionsMutex.Unlock() + select { + case record := <-input: + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() - // allows nested sessions to merge records properly - mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) - if record.PartOne { - if _, found := h.sessions[mergeID]; found { - log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + - "(this happens in bash because bash-preexec runs when it's not supposed to)") - } - h.sessions[mergeID] = record - } else { - if part1, found := h.sessions[mergeID]; found == false { - log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") + // allows nested sessions to merge records properly + mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) + if record.PartOne { + if _, found := h.sessions[mergeID]; found { + log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + + "(this happens in bash because bash-preexec runs when it's not supposed to)") + } + h.sessions[mergeID] = record } else { - delete(h.sessions, mergeID) - go h.mergeAndWriteRecord(part1, record) + if part1, found := h.sessions[mergeID]; found == false { + log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") + } else { + delete(h.sessions, mergeID) + go h.mergeAndWriteRecord(part1, record) + } + } + case sig := <-signals: + log.Println("histfile: Got signal " + sig.String()) + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + log.Println("histfile DEBUG: Unlocked mutex") + + for sessID, record := range h.sessions { + log.Panicln("histfile WARN: Writing incomplete record for session " + sessID) + h.writeRecord(record) } + log.Println("histfile DEBUG: Shutdown success") + shutdownDone <- "histfile" + return } }() } } +func (h *Histfile) writeRecord(part1 records.Record) { + writeRecord(part1, h.historyPath) +} + func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { err := part1.Merge(part2) if err != nil { @@ -100,12 +163,8 @@ func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { defer h.recentMutex.Unlock() h.recentRecords = append(h.recentRecords, part1) cmdLine := part1.CmdLine - idx, found := h.cmdLinesLastIndex[cmdLine] - if found { - h.recentCmdLines = append(h.recentCmdLines[:idx], h.recentCmdLines[idx+1:]...) - } - h.cmdLinesLastIndex[cmdLine] = len(h.recentCmdLines) - h.recentCmdLines = append(h.recentCmdLines, cmdLine) + h.bashCmdLines.AddCmdLine(cmdLine) + h.zshCmdLines.AddCmdLine(cmdLine) }() writeRecord(part1, h.historyPath) @@ -132,6 +191,21 @@ func writeRecord(rec records.Record, outputPath string) { } // GetRecentCmdLines returns recent cmdLines -func (h *Histfile) GetRecentCmdLines(limit int) []string { - return h.recentCmdLines +func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist { + // NOTE: limit does nothing atm + h.recentMutex.Lock() + defer h.recentMutex.Unlock() + log.Println("histfile: History requested ...") + var hl histlist.Histlist + if shell == "bash" { + hl = histlist.Copy(h.bashCmdLines) + log.Println("histfile: history copied (bash) - cmdLine count:", len(hl.List)) + return hl + } + if shell != "zsh" { + log.Println("histfile ERROR: Unknown shell: ", shell) + } + hl = histlist.Copy(h.zshCmdLines) + log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List)) + return hl } diff --git a/pkg/histlist/histlist.go b/pkg/histlist/histlist.go new file mode 100644 index 0000000..a3f4334 --- /dev/null +++ b/pkg/histlist/histlist.go @@ -0,0 +1,64 @@ +package histlist + +import "log" + +// Histlist is a deduplicated list of cmdLines +type Histlist struct { + // list of commands lines (deduplicated) + List []string + // lookup: cmdLine -> last index + LastIndex map[string]int +} + +// New Histlist +func New() Histlist { + return Histlist{LastIndex: make(map[string]int)} +} + +// Copy Histlist +func Copy(hl Histlist) Histlist { + newHl := New() + // copy list + newHl.List = make([]string, len(hl.List)) + copy(newHl.List, hl.List) + // copy map + for k, v := range hl.LastIndex { + newHl.LastIndex[k] = v + } + return newHl +} + +// AddCmdLine to the histlist +func (h *Histlist) AddCmdLine(cmdLine string) { + // lenBefore := len(h.List) + // lookup + idx, found := h.LastIndex[cmdLine] + if found { + // remove duplicate + if cmdLine != h.List[idx] { + log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx]) + } + h.List = append(h.List[:idx], h.List[idx+1:]...) + // idx++ + for idx < len(h.List) { + cmdLn := h.List[idx] + h.LastIndex[cmdLn]-- + if idx != h.LastIndex[cmdLn] { + log.Println("histlist ERROR: Shifting LastIndex idx:", idx, " != LastIndex[cmdLn]:", h.LastIndex[cmdLn]) + } + idx++ + } + } + // update last index + h.LastIndex[cmdLine] = len(h.List) + // append new cmdline + h.List = append(h.List, cmdLine) + // log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) +} + +// AddHistlist contents of another histlist to this histlist +func (h *Histlist) AddHistlist(h2 Histlist) { + for _, cmdLine := range h2.List { + h.AddCmdLine(cmdLine) + } +} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go new file mode 100644 index 0000000..e402904 --- /dev/null +++ b/pkg/msg/msg.go @@ -0,0 +1,12 @@ +package msg + +// InspectMsg struct +type InspectMsg struct { + SessionID string `json:"sessionId"` + Count uint `json:"count"` +} + +// MultiResponse struct +type MultiResponse struct { + CmdLines []string `json:"cmdlines"` +} diff --git a/pkg/records/records.go b/pkg/records/records.go index 3b4170a..29477fe 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/curusarn/resh/pkg/histlist" "github.com/mattn/go-shellwords" ) @@ -129,6 +130,21 @@ type FallbackRecord struct { Lines int `json:"lines"` // notice the int type } +// SlimRecord used for recalling because unmarshalling record w/ 50+ fields is too slow +type SlimRecord struct { + SessionID string `json:"sessionId"` + RecallHistno int `json:"recallHistno,omitempty"` + RecallPrefix string `json:"recallPrefix,omitempty"` + + // extra recall - we might use these in the future + // Pwd string `json:"pwd"` + // RealPwd string `json:"realPwd"` + // GitDir string `json:"gitDir"` + // GitRealDir string `json:"gitRealDir"` + // GitOriginRemote string `json:"gitOriginRemote"` + +} + // Convert from FallbackRecord to Record func Convert(r *FallbackRecord) Record { return Record{ @@ -439,9 +455,10 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { return dist } -// LoadCmdLinesFromFile loads limit cmdlines from file -func LoadCmdLinesFromFile(fname string, limit int) []string { +// LoadCmdLinesFromFile loads cmdlines from file +func LoadCmdLinesFromFile(hl *histlist.Histlist, fname string, limit int) { recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique + // go from bottom and deduplicate var cmdLines []string cmdLinesSet := map[string]bool{} for i := len(recs) - 1; i >= 0; i-- { @@ -455,18 +472,24 @@ func LoadCmdLinesFromFile(fname string, limit int) []string { break } } - return cmdLines + // add everything to histlist + for _, cmdLine := range cmdLines { + hl.AddCmdLine(cmdLine) + } } -// LoadFromFile loads at most 'limit' records from 'fname' file +// LoadFromFile loads records from 'fname' file func LoadFromFile(fname string, limit int) []Record { + // NOTE: limit does nothing atm + var recs []Record file, err := os.Open(fname) if err != nil { - log.Fatal("Open() resh history file error:", err) + log.Println("Open() resh history file error:", err) + log.Println("WARN: Skipping reading resh history!") + return recs } defer file.Close() - var recs []Record scanner := bufio.NewScanner(file) for scanner.Scan() { record := Record{} @@ -485,3 +508,70 @@ func LoadFromFile(fname string, limit int) []Record { } return recs } + +// LoadCmdLinesFromZshFile loads cmdlines from zsh history file +func LoadCmdLinesFromZshFile(fname string) histlist.Histlist { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + hl := histlist.New() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + var cmd string + // zsh format EXTENDED_HISTORY + // : 1576270617:0;make install + // zsh format no EXTENDED_HISTORY + // make install + if len(line) == 0 { + // skip empty + continue + } + if strings.Contains(line, ":") && strings.Contains(line, ";") && + len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { + // contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY + cmd = strings.Split(line, ";")[1] + } else { + cmd = line + } + hl.AddCmdLine(cmd) + } + return hl +} + +// LoadCmdLinesFromBashFile loads cmdlines from bash history file +func LoadCmdLinesFromBashFile(fname string) histlist.Histlist { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + hl := histlist.New() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + // trim spaces from left + line = strings.TrimLeft(line, " ") + // bash format (two lines) + // #1576199174 + // make install + if strings.HasPrefix(line, "#") { + // is either timestamp or comment => skip + continue + } + if len(line) == 0 { + // skip empty + continue + } + hl.AddCmdLine(line) + } + return hl +} diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 50b79ec..ac8bd92 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/curusarn/resh/pkg/histfile" + "github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/records" ) @@ -39,7 +40,7 @@ func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) { for { record := <-sessionsToInit log.Println("sesshist: got session to init - " + record.SessionID) - s.initSession(record.SessionID) + s.initSession(record.SessionID, record.Shell) } } @@ -57,13 +58,28 @@ func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { if record.PartOne { log.Println("sesshist: got record to add - " + record.CmdLine) s.addRecentRecord(record.SessionID, record) + } else { + // this inits session on RESH update + s.checkSession(record.SessionID, record.Shell) } // TODO: we will need to handle part2 as well eventually } } +func (s *Dispatch) checkSession(sessionID, shell string) { + s.mutex.RLock() + _, found := s.sessions[sessionID] + s.mutex.RUnlock() + if found == false { + err := s.initSession(sessionID, shell) + if err != nil { + log.Println("sesshist: Error while checking session:", err) + } + } +} + // InitSession struct -func (s *Dispatch) initSession(sessionID string) error { +func (s *Dispatch) initSession(sessionID, shell string) error { log.Println("sesshist: initializing session - " + sessionID) s.mutex.RLock() _, found := s.sessions[sessionID] @@ -74,14 +90,13 @@ func (s *Dispatch) initSession(sessionID string) error { } log.Println("sesshist: loading history to populate session - " + sessionID) - historyCmdLines := s.history.GetRecentCmdLines(s.historyInitSize) + historyCmdLines := s.history.GetRecentCmdLines(shell, s.historyInitSize) s.mutex.Lock() defer s.mutex.Unlock() // init sesshist and populate it with history loaded from file s.sessions[sessionID] = &sesshist{ - recentCmdLines: historyCmdLines, - cmdLinesLastIndex: map[string]int{}, + recentCmdLines: historyCmdLines, } log.Println("sesshist: session init done - " + sessionID) return nil @@ -105,35 +120,33 @@ func (s *Dispatch) dropSession(sessionID string) error { // AddRecent record to session func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { + log.Println("sesshist: Adding a record, RLocking main lock ...") s.mutex.RLock() + log.Println("sesshist: Getting a session ...") session, found := s.sessions[sessionID] + log.Println("sesshist: RUnlocking main lock ...") s.mutex.RUnlock() if found == false { - log.Println("sesshist ERROR: addRecontRecord(): No session history for SessionID " + sessionID + " - creating session history.") - s.initSession(sessionID) + log.Println("sesshist ERROR: addRecentRecord(): No session history for SessionID " + sessionID + " - creating session history.") + s.initSession(sessionID, record.Shell) return s.addRecentRecord(sessionID, record) } + log.Println("sesshist: RLocking session lock (w/ defer) ...") session.mutex.Lock() defer session.mutex.Unlock() session.recentRecords = append(session.recentRecords, record) - // remove previous occurance of record - cmdLine := record.CmdLine - idx, found := session.cmdLinesLastIndex[cmdLine] - if found { - session.recentCmdLines = append(session.recentCmdLines[:idx], session.recentCmdLines[idx+1:]...) - } - session.cmdLinesLastIndex[cmdLine] = len(session.recentCmdLines) - // append new record - session.recentCmdLines = append(session.recentCmdLines, cmdLine) + session.recentCmdLines.AddCmdLine(record.CmdLine) log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, - "; session len:", len(session.recentCmdLines), "; session len w/ dups:", len(session.recentRecords)) + "; session len:", len(session.recentCmdLines.List), "; session len (records):", len(session.recentRecords)) return nil } // Recall command from recent session history func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { + log.Println("sesshist - recall: RLocking main lock ...") s.mutex.RLock() + log.Println("sesshist - recall: Getting session history struct ...") session, found := s.sessions[sessionID] s.mutex.RUnlock() @@ -141,21 +154,49 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, // go s.initSession(sessionID) return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") } + log.Println("sesshist - recall: Locking session lock ...") + session.mutex.Lock() + defer session.mutex.Unlock() if prefix == "" { - session.mutex.Lock() - defer session.mutex.Unlock() + log.Println("sesshist - recall: Getting records by histno ...") return session.getRecordByHistno(histno) } + log.Println("sesshist - recall: Searching for records by prefix ...") + return session.searchRecordByPrefix(prefix, histno) +} + +// Inspect commands in recent session history +func (s *Dispatch) Inspect(sessionID string, count int) ([]string, error) { + prefix := "" + log.Println("sesshist - inspect: RLocking main lock ...") + s.mutex.RLock() + log.Println("sesshist - inspect: Getting session history struct ...") + session, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + // go s.initSession(sessionID) + return nil, errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") + } + log.Println("sesshist - inspect: Locking session lock ...") session.mutex.Lock() defer session.mutex.Unlock() - return session.searchRecordByPrefix(prefix, histno) + if prefix == "" { + log.Println("sesshist - inspect: Getting records by histno ...") + idx := len(session.recentCmdLines.List) - count + if idx < 0 { + idx = 0 + } + return session.recentCmdLines.List[idx:], nil + } + log.Println("sesshist - inspect: Searching for records by prefix ... ERROR - Not implemented") + return nil, errors.New("sesshist ERROR: Inspect - Searching for records by prefix Not implemented yet") } type sesshist struct { - recentRecords []records.Record - recentCmdLines []string // deduplicated - cmdLinesLastIndex map[string]int - mutex sync.Mutex + mutex sync.Mutex + recentRecords []records.Record + recentCmdLines histlist.Histlist } func (s *sesshist) getRecordByHistno(histno int) (string, error) { @@ -167,11 +208,11 @@ func (s *sesshist) getRecordByHistno(histno int) (string, error) { if histno < 0 { return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") } - index := len(s.recentCmdLines) - histno + index := len(s.recentCmdLines.List) - histno if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") } - return s.recentCmdLines[index], nil + return s.recentCmdLines.List[index], nil } func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, error) { @@ -181,14 +222,14 @@ func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, erro if histno < 0 { return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") } - index := len(s.recentCmdLines) - histno + index := len(s.recentCmdLines.List) - histno if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") } cmdLines := []string{} - for i := len(s.recentCmdLines) - 1; i >= 0; i-- { - if strings.HasPrefix(s.recentCmdLines[i], prefix) { - cmdLines = append(cmdLines, s.recentCmdLines[i]) + for i := len(s.recentCmdLines.List) - 1; i >= 0; i-- { + if strings.HasPrefix(s.recentCmdLines.List[i], prefix) { + cmdLines = append(cmdLines, s.recentCmdLines.List[i]) if len(cmdLines) >= histno { break } diff --git a/pkg/signalhandler/signalhander.go b/pkg/signalhandler/signalhander.go new file mode 100644 index 0000000..c3c201b --- /dev/null +++ b/pkg/signalhandler/signalhander.go @@ -0,0 +1,57 @@ +package signalhandler + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" +) + +func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { + for _, sub := range subscribers { + sub <- sig + } + chanCount := len(subscribers) + start := time.Now() + delay := time.Millisecond * 100 + timeout := time.Millisecond * 2000 + + for { + select { + case _ = <-done: + chanCount-- + if chanCount == 0 { + log.Println("signalhandler: All boxes shut down successfully") + return + } + default: + time.Sleep(delay) + } + if time.Since(start) > timeout { + log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) + return + } + } +} + +// Run catches and handles signals +func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { + signals := make(chan os.Signal, 1) + + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + sig := <-signals + log.Println("signalhandler: Got signal " + sig.String()) + + log.Println("signalhandler: Sending signals to Subscribers") + sendSignals(sig, subscribers, done) + + log.Println("signalhandler: Shutting down the server") + if err := server.Shutdown(context.Background()); err != nil { + log.Printf("HTTP server Shutdown: %v", err) + } +} diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 4963f95..8da69c3 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -1,6 +1,7 @@ __resh_reset_variables() { __RESH_HISTNO=0 + __RESH_HISTNO_MAX="" __RESH_HISTNO_ZERO_LINE="" __RESH_HIST_PREV_LINE="" __RESH_HIST_RECALL_ACTIONS="" diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 2656055..0eb9fa2 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -5,22 +5,56 @@ . ~/.resh/widgets.sh __resh_bind_arrows() { - bindfunc '\e[A' __resh_widget_arrow_up_compat - bindfunc '\e[B' __resh_widget_arrow_down_compat + if [ "${__RESH_arrow_keys_bind_enabled-0}" != 0 ]; then + echo "RESH arrow key bindings are already enabled!" + return 1 + fi + bindfunc --revert '\eOA' __resh_widget_arrow_up_compat + __RESH_bindfunc_revert_arrow_up_bind=$_bindfunc_revert + bindfunc --revert '\e[A' __resh_widget_arrow_up_compat + __RESH_bindfunc_revert_arrow_up_bind_vim=$_bindfunc_revert + bindfunc --revert '\eOB' __resh_widget_arrow_down_compat + __RESH_bindfunc_revert_arrow_down_bind=$_bindfunc_revert + bindfunc --revert '\e[B' __resh_widget_arrow_down_compat + __RESH_bindfunc_revert_arrow_down_bind_vim=$_bindfunc_revert + __RESH_arrow_keys_bind_enabled=1 return 0 } __resh_bind_control_R() { + # TODO echo "bindfunc __resh_widget_control_R_compat" return 0 } + __resh_unbind_arrows() { - echo "\ bindfunc __resh_widget_arrow_up_compat" - echo "\ bindfunc __resh_widget_arrow_down_compat" + if [ "${__RESH_arrow_keys_bind_enabled-0}" != 1 ]; then + echo "Error: Can't disable arrow key bindings because they are not enabled!" + return 1 + fi + + if [ -z "${__RESH_bindfunc_revert_arrow_up_bind+x}" ]; then + echo "Warn: Couldn't revert arrow UP binding because 'revert command' is empty." + else + eval "$__RESH_bindfunc_revert_arrow_up_bind" + [ -z "${__RESH_bindfunc_revert_arrow_up_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_up_bind_vim" + echo "RESH arrow up binding successfully disabled ✓" + __RESH_arrow_keys_bind_enabled=0 + fi + + if [ -z "${__RESH_bindfunc_revert_arrow_down_bind+x}" ]; then + echo "Warn: Couldn't revert arrow DOWN binding because 'revert command' is empty." + else + eval "$__RESH_bindfunc_revert_arrow_down_bind" + [ -z "${__RESH_bindfunc_revert_arrow_down_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_down_bind_vim" + echo "RESH arrow down binding successfully disabled ✓" + __RESH_arrow_keys_bind_enabled=0 + fi return 0 } __resh_unbind_control_R() { + # TODO echo "\ bindfunc __resh_widget_control_R_compat" return 0 } @@ -36,25 +70,42 @@ __resh_unbind_all() { } reshctl() { + # local log=~/.resh/reshctl.log + # export current shell because resh-control needs to know + export __RESH_ctl_shell=$__RESH_SHELL # run resh-control aka the real reshctl resh-control "$@" + # modify current shell session based on exit status local _status=$? + # echo $_status + # unexport current shell + unset __RESH_ctl_shell case "$_status" in 0|1) # success | fail return "$_status" ;; # enable - 100) - # enable all - __resh_bind_all + # 100) + # # enable all + # __resh_bind_all + # return 0 + # ;; + 101) + # enable arrow keys + __resh_bind_arrows return 0 ;; # disable - 110) - # disable all - __resh_unbind_all + # 110) + # # disable all + # __resh_unbind_all + # return 0 + # ;; + 111) + # disable arrow keys + __resh_unbind_arrows return 0 ;; 200) @@ -62,6 +113,12 @@ reshctl() { . ~/.resh/shellrc return 0 ;; + 201) + # inspect session history + # reshctl debug inspect N + resh-inspect --sessionID "$__RESH_SESSION_ID" --count "${3-10}" + return 0 + ;; *) echo "reshctl() FATAL ERROR: unknown status" >&2 return "$_status" diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index c4d3afc..a6b9ebc 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -23,13 +23,13 @@ else echo "resh PANIC unrecognized OS" fi -if [ -n "$ZSH_VERSION" ]; then +if [ -n "${ZSH_VERSION-}" ]; then # shellcheck disable=SC1009 __RESH_SHELL="zsh" __RESH_HOST="$HOST" __RESH_HOSTTYPE="$CPUTYPE" __resh_zsh_completion_init -elif [ -n "$BASH_VERSION" ]; then +elif [ -n "${BASH_VERSION-}" ]; then __RESH_SHELL="bash" __RESH_HOST="$HOSTNAME" __RESH_HOSTTYPE="$HOSTTYPE" @@ -86,5 +86,14 @@ if [ -z "${__RESH_INIT_DONE+x}" ]; then __resh_reset_variables + if [ "$__RESH_SHELL" = bash ] ; then + [ "$(resh-config --key BindArrowKeysBash)" = true ] && reshctl enable arrow_key_bindings + elif [ "$__RESH_SHELL" = zsh ] ; then + [ "$(resh-config --key BindArrowKeysZsh)" = true ] && reshctl enable arrow_key_bindings + else + echo "RESH error: unknown shell (init)" + echo "$__RESH_SHELL" + fi + __RESH_INIT_DONE=1 fi diff --git a/scripts/test.sh b/scripts/test.sh index cbd1de6..34fa3e0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,15 +1,17 @@ #!/usr/bin/env bash +# very simple tests to catch simple errors in scripts +# shellcheck disable=SC2016 [ "${BASH_SOURCE[0]}" != "scripts/test.sh" ] && echo 'Run this script using `make test`' && exit 1 for f in scripts/*.sh; do echo "Running shellcheck on $f ..." - shellcheck $f --shell=bash --severity=error || exit 1 + shellcheck "$f" --shell=bash --severity=error || exit 1 done for f in scripts/{shellrc,util,reshctl,hooks}.sh; do echo "Checking Zsh syntax of $f ..." - ! zsh -n scripts/shellrc.sh && echo "Zsh syntax check failed!" && exit 1 + ! zsh -n "$f" && echo "Zsh syntax check failed!" && exit 1 done for sh in bash zsh; do diff --git a/scripts/util.sh b/scripts/util.sh index bbdceb4..23e365b 100644 --- a/scripts/util.sh +++ b/scripts/util.sh @@ -63,8 +63,19 @@ __resh_bash_completion_init() { } __resh_zsh_completion_init() { - # shellcheck disable=SC2206 - fpath=(~/.resh/zsh_completion.d $fpath) + # NOTE: this is hacky - each completion needs to be added individually + # TODO: fix later + # fpath=(~/.resh/zsh_completion.d $fpath) + # we should be using fpath but that doesn't work well with oh-my-zsh + # so we are just adding it manually + # shellcheck disable=1090 + source ~/.resh/zsh_completion.d/_reshctl && compdef _reshctl reshctl + + # TODO: test and use this + # NOTE: this is not how globbing works + # for f in ~/.resh/zsh_completion.d/_*; do + # source ~/.resh/zsh_completion.d/_$f && compdef _$f $f + # done } __resh_session_init() { diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 82761bd..24143b8 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -37,8 +37,12 @@ __resh_widget_arrow_up() { __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX" # increment histno __RESH_HISTNO=$((__RESH_HISTNO+1)) - # back at histno == 0 => restore original line - if [ "$__RESH_HISTNO" -eq 0 ]; then + if [ "${#__RESH_HISTNO_MAX}" -gt 0 ] && [ "${__RESH_HISTNO}" -gt "${__RESH_HISTNO_MAX}" ]; then + # end of the session -> don't recall, do nothing + # fix histno + __RESH_HISTNO=$((__RESH_HISTNO-1)) + elif [ "$__RESH_HISTNO" -eq 0 ]; then + # back at histno == 0 => restore original line BUFFER=$__RESH_HISTNO_ZERO_LINE else # run recall @@ -46,7 +50,12 @@ __resh_widget_arrow_up() { NEW_BUFFER="$(__resh_collect --recall --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_up_last_run_out.txt)" # IF new buffer in non-empty THEN use the new buffer ELSE revert histno change # shellcheck disable=SC2015 - [ "${#NEW_BUFFER}" -gt 0 ] && BUFFER=$NEW_BUFFER || __RESH_HISTNO=$((__RESH_HISTNO-1)) + if [ "${#NEW_BUFFER}" -gt 0 ]; then + BUFFER=$NEW_BUFFER + else + __RESH_HISTNO=$((__RESH_HISTNO-1)) + __RESH_HISTNO_MAX=$__RESH_HISTNO + fi fi # run post helper __resh_helper_arrow_post diff --git a/submodules/bash-preexec b/submodules/bash-preexec index 9811ba8..f558191 160000 --- a/submodules/bash-preexec +++ b/submodules/bash-preexec @@ -1 +1 @@ -Subproject commit 9811ba8b7694cdbd9debed931e922b67e439197a +Subproject commit f558191d74ae33654d63a0f091034ef27f4b44f9 diff --git a/submodules/bash-zsh-compat-widgets b/submodules/bash-zsh-compat-widgets index 7dde81e..c3077fc 160000 --- a/submodules/bash-zsh-compat-widgets +++ b/submodules/bash-zsh-compat-widgets @@ -1 +1 @@ -Subproject commit 7dde81eaa09cbed11ebc70ea892bcae24ea1606c +Subproject commit c3077fcdf2e20efb95dd27a53766b78533ab7bc4