Merge pull request #30 from curusarn/dev3

version 2.1.0
pull/58/head
Šimon Let 6 years ago committed by GitHub
commit d4a5ad8e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 22
      Makefile
  3. 2
      VERSION
  4. 11
      cmd/collect/main.go
  5. 56
      cmd/config/main.go
  6. 10
      cmd/control/cmd/completion.go
  7. 14
      cmd/control/cmd/debug.go
  8. 28
      cmd/control/cmd/disable.go
  9. 105
      cmd/control/cmd/enable.go
  10. 10
      cmd/control/cmd/root.go
  11. 10
      cmd/control/status/status.go
  12. 14
      cmd/daemon/main.go
  13. 55
      cmd/daemon/recall.go
  14. 37
      cmd/daemon/run-server.go
  15. 84
      cmd/inspect/main.go
  16. 31
      cmd/sanitize/main.go
  17. 3
      conf/config.toml
  18. 3
      go.mod
  19. 8
      go.sum
  20. 3
      pkg/cfg/cfg.go
  21. 2
      pkg/collect/collect.go
  22. 114
      pkg/histfile/histfile.go
  23. 64
      pkg/histlist/histlist.go
  24. 12
      pkg/msg/msg.go
  25. 102
      pkg/records/records.go
  26. 99
      pkg/sesshist/sesshist.go
  27. 57
      pkg/signalhandler/signalhander.go
  28. 1
      scripts/hooks.sh
  29. 77
      scripts/reshctl.sh
  30. 13
      scripts/shellrc.sh
  31. 6
      scripts/test.sh
  32. 15
      scripts/util.sh
  33. 13
      scripts/widgets.sh
  34. 2
      submodules/bash-preexec
  35. 2
      submodules/bash-zsh-compat-widgets

1
.gitignore vendored

@ -1 +1,2 @@
bin/* bin/*
.vscode/*

@ -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: test_go:
# Running tests # Running tests
@ -58,7 +59,7 @@ rebuild:
make build make build
clean: clean:
rm resh-* rm bin/resh-*
install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/config.toml scripts/uuid.sh \ 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 | $(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 [ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json
# Adding resh shellrc to .bashrc ... # Adding resh shellrc to .bashrc ...
grep '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.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 ... # Adding bash-preexec to .bashrc ...
grep '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.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 ... # Adding resh shellrc to .zshrc ...
grep '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.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 ... # 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 nohup resh-daemon &>/dev/null & disown
# Reloading rc files # Reloading rc files
. ~/.resh/shellrc . ~/.resh/shellrc
@ -131,9 +137,7 @@ uninstall:
# Uninstalling ... # Uninstalling ...
-rm -rf ~/.resh/ -rm -rf ~/.resh/
bin/resh-control: cmd/control/cmd/*.go bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION cmd/control/cmd/*.go cmd/control/status/status.go
bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION
go build ${GOFLAGS} -o $@ cmd/$*/*.go go build ${GOFLAGS} -o $@ cmd/$*/*.go
$(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d: $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d:

@ -1 +1 @@
2.0.0 2.1.0

@ -169,6 +169,14 @@ func main() {
// *osReleasePrettyName = "Linux" // *osReleasePrettyName = "Linux"
// } // }
if *recall {
rec := records.SlimRecord{
SessionID: *sessionID,
RecallHistno: *recallHistno,
RecallPrefix: *recallPrefix,
}
fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port)))
} else {
rec := records.Record{ rec := records.Record{
// posix // posix
Cols: *cols, Cols: *cols,
@ -234,9 +242,6 @@ func main() {
RecallStrategy: *recallStrategy, RecallStrategy: *recallStrategy,
}, },
} }
if *recall {
fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port)))
} else {
collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") collect.SendRecord(rec, strconv.Itoa(config.Port), "/record")
} }
} }

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

@ -10,20 +10,20 @@ import (
// completionCmd represents the completion command // completionCmd represents the completion command
var completionCmd = &cobra.Command{ var completionCmd = &cobra.Command{
Use: "completion", Use: "completion",
Short: "Generates bash/zsh completion scripts", Short: "generate bash/zsh completion scripts",
Long: `To load completion run Long: `To load completion run
. <(reshctl completion bash) . <(reshctl completion bash)
OR OR
. <(reshctl completion zsh) . <(reshctl completion zsh) && compdef _reshctl reshctl
`, `,
} }
var completionBashCmd = &cobra.Command{ var completionBashCmd = &cobra.Command{
Use: "bash", Use: "bash",
Short: "Generates bash completion scripts", Short: "generate bash completion scripts",
Long: `To load completion run Long: `To load completion run
. <(reshctl completion bash) . <(reshctl completion bash)
@ -36,10 +36,10 @@ var completionBashCmd = &cobra.Command{
var completionZshCmd = &cobra.Command{ var completionZshCmd = &cobra.Command{
Use: "zsh", Use: "zsh",
Short: "Generates zsh completion scripts", Short: "generate zsh completion scripts",
Long: `To load completion run Long: `To load completion run
. <(reshctl completion zsh) . <(reshctl completion zsh) && compdef _reshctl reshctl
`, `,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout) rootCmd.GenZshCompletion(os.Stdout)

@ -12,22 +12,30 @@ import (
var debugCmd = &cobra.Command{ var debugCmd = &cobra.Command{
Use: "debug", 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", Long: "Reloads resh rc files. Shows logs and output from last runs of resh",
} }
var debugReloadCmd = &cobra.Command{ var debugReloadCmd = &cobra.Command{
Use: "reload", Use: "reload",
Short: "Reload resh rc files", Short: "reload resh rc files",
Long: "Reload resh rc files", Long: "Reload resh rc files",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
exitCode = status.ReloadRcFiles 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{ var debugOutputCmd = &cobra.Command{
Use: "output", 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", Long: "Shows output from last runs of resh",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
files := []string{ files := []string{

@ -7,18 +7,24 @@ import (
var disableCmd = &cobra.Command{ var disableCmd = &cobra.Command{
Use: "disable", Use: "disable",
Short: "disable RESH features", Short: "disable RESH features (arrow key bindings)",
Long: `Disables RESH bindings for arrows and C-R.`, }
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) { Run: func(cmd *cobra.Command, args []string) {
exitCode = status.DisableAll exitCode = status.DisableArrowKeyBindings
}, },
} }
// var disableRecallingCmd = &cobra.Command{ var disableArrowKeyBindingsGlobalCmd = &cobra.Command{
// Use: "keybind", Use: "arrow_key_bindings_global",
// Short: "Disables RESH bindings for arrows and C-R.", Short: "disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS",
// Long: `Disables RESH bindings for arrows and C-R.`, Long: "Disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" +
// Run: func(cmd *cobra.Command, args []string) { "Note that this only affects sessions of the same shell.\n" +
// exitCode = status.DisableAll "(e.g. running this in zsh will only affect future zsh sessions)",
// }, Run: func(cmd *cobra.Command, args []string) {
// } exitCode = enableDisableArrowKeyBindingsGlobally(false)
},
}

@ -1,24 +1,107 @@
package cmd package cmd
import ( import (
"fmt"
"os"
"os/user"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/cmd/control/status" "github.com/curusarn/resh/cmd/control/status"
"github.com/curusarn/resh/pkg/cfg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var enableCmd = &cobra.Command{ var enableCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "enable RESH features", Short: "enable RESH features (arrow key bindings)",
Long: `Enables RESH bindings for arrows and C-R.`, }
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) { Run: func(cmd *cobra.Command, args []string) {
exitCode = status.EnableAll exitCode = status.EnableArrowKeyBindings
}, },
} }
// var enableRecallingCmd = &cobra.Command{ var enableArrowKeyBindingsGlobalCmd = &cobra.Command{
// Use: "keybind", Use: "arrow_key_bindings_global",
// Short: "Enables RESH bindings for arrows and C-R.", Short: "enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS",
// Long: `Enables RESH bindings for arrows and C-R.`, Long: "Enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" +
// Run: func(cmd *cobra.Command, args []string) { "Note that this only affects sessions of the same shell.\n" +
// exitCode = status.EnableAll "(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
}

@ -11,17 +11,18 @@ var exitCode status.Code
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "reshctl", Use: "reshctl",
Short: "Reshctl (RESH control) - enables you to enable/disable features and more.", Short: "Reshctl (RESH control) - enable/disable RESH features and more.",
Long: `Enables you to enable/disable RESH bindings for arrows and C-R.`,
} }
// Execute reshctl // Execute reshctl
func Execute() status.Code { func Execute() status.Code {
rootCmd.AddCommand(disableCmd) rootCmd.AddCommand(disableCmd)
// disableCmd.AddCommand(disableRecallingCmd) disableCmd.AddCommand(disableArrowKeyBindingsCmd)
disableCmd.AddCommand(disableArrowKeyBindingsGlobalCmd)
rootCmd.AddCommand(enableCmd) rootCmd.AddCommand(enableCmd)
// enableCmd.AddCommand(enableRecallingCmd) enableCmd.AddCommand(enableArrowKeyBindingsCmd)
enableCmd.AddCommand(enableArrowKeyBindingsGlobalCmd)
rootCmd.AddCommand(completionCmd) rootCmd.AddCommand(completionCmd)
completionCmd.AddCommand(completionBashCmd) completionCmd.AddCommand(completionBashCmd)
@ -29,6 +30,7 @@ func Execute() status.Code {
rootCmd.AddCommand(debugCmd) rootCmd.AddCommand(debugCmd)
debugCmd.AddCommand(debugReloadCmd) debugCmd.AddCommand(debugReloadCmd)
debugCmd.AddCommand(debugInspectCmd)
debugCmd.AddCommand(debugOutputCmd) debugCmd.AddCommand(debugOutputCmd)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) fmt.Println(err)

@ -8,10 +8,12 @@ const (
Success Code = 0 Success Code = 0
// Fail exit code // Fail exit code
Fail = 1 Fail = 1
// EnableAll exit code - tells reshctl() wrapper to enable_all // EnableArrowKeyBindings exit code - tells reshctl() wrapper to enable arrow key bindings
EnableAll = 100 EnableArrowKeyBindings = 101
// DisableAll exit code - tells reshctl() wrapper to disable_all // DisableArrowKeyBindings exit code - tells reshctl() wrapper to disable arrow key bindings
DisableAll = 110 DisableArrowKeyBindings = 111
// ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file
ReloadRcFiles = 200 ReloadRcFiles = 200
// InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon
InspectSessionHistory = 201
) )

@ -31,7 +31,9 @@ func main() {
dir := usr.HomeDir dir := usr.HomeDir
pidfilePath := filepath.Join(dir, ".resh/resh.pid") pidfilePath := filepath.Join(dir, ".resh/resh.pid")
configPath := filepath.Join(dir, ".config/resh.toml") 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") logPath := filepath.Join(dir, ".resh/daemon.log")
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 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) log.Println("Error reading config", err)
return return
} }
if config.Debug {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
}
res, err := isDaemonRunning(config.Port) res, err := isDaemonRunning(config.Port)
if err != nil { if err != nil {
log.Println("Error while checking if the daemon is runnnig", err) log.Println("Error while checking if the daemon is runnnig", err)
@ -69,11 +75,13 @@ func main() {
if err != nil { if err != nil {
log.Fatal("Could not create pidfile", err) log.Fatal("Could not create pidfile", err)
} }
runServer(config, historyPath) runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath)
log.Println("main: Removing pidfile ...")
err = os.Remove(pidfilePath) err = os.Remove(pidfilePath)
if err != nil { if err != nil {
log.Println("Could not delete pidfile", err) log.Println("Could not delete pidfile", err)
} }
log.Println("main: Shutdown - bye")
} }
func statusHandler(w http.ResponseWriter, r *http.Request) { func statusHandler(w http.ResponseWriter, r *http.Request) {
@ -92,7 +100,7 @@ func killDaemon(pidfile string) error {
if err != nil { if err != nil {
log.Fatal("Pidfile contents are malformed", err) 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() err = cmd.Run()
if err != nil { if err != nil {
log.Printf("Command finished with error: %v", err) log.Printf("Command finished with error: %v", err)

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"github.com/curusarn/resh/pkg/collect" "github.com/curusarn/resh/pkg/collect"
"github.com/curusarn/resh/pkg/msg"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/pkg/sesshist" "github.com/curusarn/resh/pkg/sesshist"
) )
@ -16,26 +17,31 @@ type recallHandler struct {
} }
func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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) jsn, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
log.Println("Error reading the body", err) log.Println("Error reading the body", err)
return return
} }
rec := records.Record{} rec := records.SlimRecord{}
log.Println("/recall unmarshaling record ...")
err = json.Unmarshal(jsn, &rec) err = json.Unmarshal(jsn, &rec)
if err != nil { if err != nil {
log.Println("Decoding error:", err) log.Println("Decoding error:", err)
log.Println("Payload:", jsn) log.Println("Payload:", jsn)
return return
} }
log.Println("/recall recalling ...")
cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno, rec.RecallPrefix) cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno, rec.RecallPrefix)
if err != nil { if err != nil {
log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ERROR") log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ERROR")
log.Println("Recall error:", err) log.Println("Recall error:", err)
return return
} }
resp := collect.SingleResponse{cmd} resp := collect.SingleResponse{CmdLine: cmd}
log.Println("/recall marshaling response ...")
jsn, err = json.Marshal(&resp) jsn, err = json.Marshal(&resp)
if err != nil { if err != nil {
log.Println("Encoding error:", err) log.Println("Encoding error:", err)
@ -43,6 +49,49 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Println(string(jsn)) log.Println(string(jsn))
log.Println("/recall writing response ...")
w.Write(jsn) 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)
} }

@ -2,6 +2,7 @@ package main
import ( import (
"net/http" "net/http"
"os"
"strconv" "strconv"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/pkg/cfg"
@ -9,12 +10,16 @@ import (
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/pkg/sesshist" "github.com/curusarn/resh/pkg/sesshist"
"github.com/curusarn/resh/pkg/sesswatch" "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 recordSubscribers []chan records.Record
var sessionInitSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record
var sessionDropSubscribers []chan string var sessionDropSubscribers []chan string
var signalSubscribers []chan os.Signal
shutdown := make(chan string)
// sessshist // sessshist
sesshistSessionsToInit := make(chan records.Record) sesshistSessionsToInit := make(chan records.Record)
@ -29,10 +34,19 @@ func runServer(config cfg.Config, historyPath string) {
recordSubscribers = append(recordSubscribers, histfileRecords) recordSubscribers = append(recordSubscribers, histfileRecords)
histfileSessionsToDrop := make(chan string) histfileSessionsToDrop := make(chan string)
sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) 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 // sesshist New
sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords, histfileBox, config.SesshistInitHistorySize) sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop,
sesshistRecords, histfileBox,
config.SesshistInitHistorySize)
// sesswatch // sesswatch
sesswatchSessionsToWatch := make(chan records.Record) sesswatchSessionsToWatch := make(chan records.Record)
@ -40,9 +54,16 @@ func runServer(config cfg.Config, historyPath string) {
sesswatch.Go(sesswatchSessionsToWatch, sessionDropSubscribers, config.SesswatchPeriodSeconds) sesswatch.Go(sesswatchSessionsToWatch, sessionDropSubscribers, config.SesswatchPeriodSeconds)
// handlers // handlers
http.HandleFunc("/status", statusHandler) mux := http.NewServeMux()
http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) mux.HandleFunc("/status", statusHandler)
http.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) mux.Handle("/record", &recordHandler{subscribers: recordSubscribers})
http.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers})
http.ListenAndServe(":"+strconv.Itoa(config.Port), nil) 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)
} }

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

@ -173,11 +173,42 @@ func (s *sanitizer) sanitizeRecord(record *records.Record) error {
log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) 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 // add a flag to signify that the record has been sanitized
record.Sanitized = true record.Sanitized = true
return nil 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) { func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) {
const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ... const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ...
const optionAllowedChars = "-_" // characters commonly found inside of options const optionAllowedChars = "-_" // characters commonly found inside of options

@ -1,3 +1,6 @@
port = 2627 port = 2627
sesswatchPeriodSeconds = 120 sesswatchPeriodSeconds = 120
sesshistInitHistorySize = 1000 sesshistInitHistorySize = 1000
debug = true
bindArrowKeysBash = false
bindArrowKeysZsh = true

@ -4,14 +4,11 @@ go 1.12
require ( require (
github.com/BurntSushi/toml v0.3.1 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/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629
github.com/mattn/go-shellwords v1.0.6 github.com/mattn/go-shellwords v1.0.6
github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b
github.com/schollz/progressbar v1.0.0 github.com/schollz/progressbar v1.0.0
github.com/spf13/cobra v0.0.5 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 github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa
golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 // indirect
) )

@ -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/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/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/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/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/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 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8=
github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= 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/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/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/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 h1:rW+Lu6281ed/4XGuVIa4/YebTRNvoUJlfJ44ktEVwZk=
github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= 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= 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/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/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= 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/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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

@ -5,4 +5,7 @@ type Config struct {
Port int Port int
SesswatchPeriodSeconds uint SesswatchPeriodSeconds uint
SesshistInitHistorySize int SesshistInitHistorySize int
Debug bool
BindArrowKeysBash bool
BindArrowKeysZsh bool
} }

@ -19,7 +19,7 @@ type SingleResponse struct {
} }
// SendRecallRequest to daemon // SendRecallRequest to daemon
func SendRecallRequest(r records.Record, port string) string { func SendRecallRequest(r records.SlimRecord, port string) string {
recJSON, err := json.Marshal(r) recJSON, err := json.Marshal(r)
if err != nil { if err != nil {
log.Fatal("send err 1", err) log.Fatal("send err 1", err)

@ -3,10 +3,12 @@ package histfile
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"math"
"os" "os"
"strconv" "strconv"
"sync" "sync"
"github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/records"
) )
@ -18,27 +20,69 @@ type Histfile struct {
recentMutex sync.Mutex recentMutex sync.Mutex
recentRecords []records.Record recentRecords []records.Record
recentCmdLines []string // deduplicated
cmdLinesLastIndex map[string]int // 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 // New creates new histfile and runs its gorutines
func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop chan string) *Histfile { 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{ hf := Histfile{
sessions: map[string]records.Record{}, sessions: map[string]records.Record{},
historyPath: historyPath, historyPath: reshHistoryPath,
cmdLinesLastIndex: map[string]int{}, bashCmdLines: histlist.New(),
zshCmdLines: histlist.New(),
} }
go hf.loadHistory(initHistSize) go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
go hf.writer(input) go hf.writer(input, signals, shutdownDone)
go hf.sessionGC(sessionsToDrop) go hf.sessionGC(sessionsToDrop)
return &hf 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() h.recentMutex.Lock()
defer h.recentMutex.Unlock() 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 // sessionGC reads sessionIDs from channel and deletes them from histfile struct
@ -61,10 +105,11 @@ func (h *Histfile) sessionGC(sessionsToDrop chan string) {
} }
// writer reads records from channel, merges them and writes them to file // 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 { for {
func() { func() {
record := <-input select {
case record := <-input:
h.sessionsMutex.Lock() h.sessionsMutex.Lock()
defer h.sessionsMutex.Unlock() defer h.sessionsMutex.Unlock()
@ -84,10 +129,28 @@ func (h *Histfile) writer(input chan records.Record) {
go h.mergeAndWriteRecord(part1, record) 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) { func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) {
err := part1.Merge(part2) err := part1.Merge(part2)
if err != nil { if err != nil {
@ -100,12 +163,8 @@ func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) {
defer h.recentMutex.Unlock() defer h.recentMutex.Unlock()
h.recentRecords = append(h.recentRecords, part1) h.recentRecords = append(h.recentRecords, part1)
cmdLine := part1.CmdLine cmdLine := part1.CmdLine
idx, found := h.cmdLinesLastIndex[cmdLine] h.bashCmdLines.AddCmdLine(cmdLine)
if found { h.zshCmdLines.AddCmdLine(cmdLine)
h.recentCmdLines = append(h.recentCmdLines[:idx], h.recentCmdLines[idx+1:]...)
}
h.cmdLinesLastIndex[cmdLine] = len(h.recentCmdLines)
h.recentCmdLines = append(h.recentCmdLines, cmdLine)
}() }()
writeRecord(part1, h.historyPath) writeRecord(part1, h.historyPath)
@ -132,6 +191,21 @@ func writeRecord(rec records.Record, outputPath string) {
} }
// GetRecentCmdLines returns recent cmdLines // GetRecentCmdLines returns recent cmdLines
func (h *Histfile) GetRecentCmdLines(limit int) []string { func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist {
return h.recentCmdLines // 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
} }

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

@ -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"`
}

@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/curusarn/resh/pkg/histlist"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
) )
@ -129,6 +130,21 @@ type FallbackRecord struct {
Lines int `json:"lines"` // notice the int type 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 // Convert from FallbackRecord to Record
func Convert(r *FallbackRecord) Record { func Convert(r *FallbackRecord) Record {
return Record{ return Record{
@ -439,9 +455,10 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 {
return dist return dist
} }
// LoadCmdLinesFromFile loads limit cmdlines from file // LoadCmdLinesFromFile loads cmdlines from file
func LoadCmdLinesFromFile(fname string, limit int) []string { func LoadCmdLinesFromFile(hl *histlist.Histlist, fname string, limit int) {
recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique
// go from bottom and deduplicate
var cmdLines []string var cmdLines []string
cmdLinesSet := map[string]bool{} cmdLinesSet := map[string]bool{}
for i := len(recs) - 1; i >= 0; i-- { for i := len(recs) - 1; i >= 0; i-- {
@ -455,18 +472,24 @@ func LoadCmdLinesFromFile(fname string, limit int) []string {
break 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 { func LoadFromFile(fname string, limit int) []Record {
// NOTE: limit does nothing atm
var recs []Record
file, err := os.Open(fname) file, err := os.Open(fname)
if err != nil { 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() defer file.Close()
var recs []Record
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
record := Record{} record := Record{}
@ -485,3 +508,70 @@ func LoadFromFile(fname string, limit int) []Record {
} }
return recs 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
}

@ -8,6 +8,7 @@ import (
"sync" "sync"
"github.com/curusarn/resh/pkg/histfile" "github.com/curusarn/resh/pkg/histfile"
"github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/records"
) )
@ -39,7 +40,7 @@ func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) {
for { for {
record := <-sessionsToInit record := <-sessionsToInit
log.Println("sesshist: got session to init - " + record.SessionID) 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 { if record.PartOne {
log.Println("sesshist: got record to add - " + record.CmdLine) log.Println("sesshist: got record to add - " + record.CmdLine)
s.addRecentRecord(record.SessionID, record) 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 // 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 // InitSession struct
func (s *Dispatch) initSession(sessionID string) error { func (s *Dispatch) initSession(sessionID, shell string) error {
log.Println("sesshist: initializing session - " + sessionID) log.Println("sesshist: initializing session - " + sessionID)
s.mutex.RLock() s.mutex.RLock()
_, found := s.sessions[sessionID] _, found := s.sessions[sessionID]
@ -74,14 +90,13 @@ func (s *Dispatch) initSession(sessionID string) error {
} }
log.Println("sesshist: loading history to populate session - " + sessionID) 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() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
// init sesshist and populate it with history loaded from file // init sesshist and populate it with history loaded from file
s.sessions[sessionID] = &sesshist{ s.sessions[sessionID] = &sesshist{
recentCmdLines: historyCmdLines, recentCmdLines: historyCmdLines,
cmdLinesLastIndex: map[string]int{},
} }
log.Println("sesshist: session init done - " + sessionID) log.Println("sesshist: session init done - " + sessionID)
return nil return nil
@ -105,35 +120,33 @@ func (s *Dispatch) dropSession(sessionID string) error {
// AddRecent record to session // AddRecent record to session
func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error {
log.Println("sesshist: Adding a record, RLocking main lock ...")
s.mutex.RLock() s.mutex.RLock()
log.Println("sesshist: Getting a session ...")
session, found := s.sessions[sessionID] session, found := s.sessions[sessionID]
log.Println("sesshist: RUnlocking main lock ...")
s.mutex.RUnlock() s.mutex.RUnlock()
if found == false { if found == false {
log.Println("sesshist ERROR: addRecontRecord(): No session history for SessionID " + sessionID + " - creating session history.") log.Println("sesshist ERROR: addRecentRecord(): No session history for SessionID " + sessionID + " - creating session history.")
s.initSession(sessionID) s.initSession(sessionID, record.Shell)
return s.addRecentRecord(sessionID, record) return s.addRecentRecord(sessionID, record)
} }
log.Println("sesshist: RLocking session lock (w/ defer) ...")
session.mutex.Lock() session.mutex.Lock()
defer session.mutex.Unlock() defer session.mutex.Unlock()
session.recentRecords = append(session.recentRecords, record) session.recentRecords = append(session.recentRecords, record)
// remove previous occurance of record session.recentCmdLines.AddCmdLine(record.CmdLine)
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)
log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, 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 return nil
} }
// Recall command from recent session history // Recall command from recent session history
func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) {
log.Println("sesshist - recall: RLocking main lock ...")
s.mutex.RLock() s.mutex.RLock()
log.Println("sesshist - recall: Getting session history struct ...")
session, found := s.sessions[sessionID] session, found := s.sessions[sessionID]
s.mutex.RUnlock() s.mutex.RUnlock()
@ -141,21 +154,49 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string,
// go s.initSession(sessionID) // go s.initSession(sessionID)
return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?")
} }
if prefix == "" { log.Println("sesshist - recall: Locking session lock ...")
session.mutex.Lock() session.mutex.Lock()
defer session.mutex.Unlock() defer session.mutex.Unlock()
if prefix == "" {
log.Println("sesshist - recall: Getting records by histno ...")
return session.getRecordByHistno(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() session.mutex.Lock()
defer session.mutex.Unlock() 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 { 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) { func (s *sesshist) getRecordByHistno(histno int) (string, error) {
@ -167,11 +208,11 @@ func (s *sesshist) getRecordByHistno(histno int) (string, error) {
if histno < 0 { if histno < 0 {
return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") 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 { 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) { 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 { if histno < 0 {
return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") 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 { 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{} cmdLines := []string{}
for i := len(s.recentCmdLines) - 1; i >= 0; i-- { for i := len(s.recentCmdLines.List) - 1; i >= 0; i-- {
if strings.HasPrefix(s.recentCmdLines[i], prefix) { if strings.HasPrefix(s.recentCmdLines.List[i], prefix) {
cmdLines = append(cmdLines, s.recentCmdLines[i]) cmdLines = append(cmdLines, s.recentCmdLines.List[i])
if len(cmdLines) >= histno { if len(cmdLines) >= histno {
break break
} }

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

@ -1,6 +1,7 @@
__resh_reset_variables() { __resh_reset_variables() {
__RESH_HISTNO=0 __RESH_HISTNO=0
__RESH_HISTNO_MAX=""
__RESH_HISTNO_ZERO_LINE="" __RESH_HISTNO_ZERO_LINE=""
__RESH_HIST_PREV_LINE="" __RESH_HIST_PREV_LINE=""
__RESH_HIST_RECALL_ACTIONS="" __RESH_HIST_RECALL_ACTIONS=""

@ -5,22 +5,56 @@
. ~/.resh/widgets.sh . ~/.resh/widgets.sh
__resh_bind_arrows() { __resh_bind_arrows() {
bindfunc '\e[A' __resh_widget_arrow_up_compat if [ "${__RESH_arrow_keys_bind_enabled-0}" != 0 ]; then
bindfunc '\e[B' __resh_widget_arrow_down_compat 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 return 0
} }
__resh_bind_control_R() { __resh_bind_control_R() {
# TODO
echo "bindfunc __resh_widget_control_R_compat" echo "bindfunc __resh_widget_control_R_compat"
return 0 return 0
} }
__resh_unbind_arrows() { __resh_unbind_arrows() {
echo "\ bindfunc __resh_widget_arrow_up_compat" if [ "${__RESH_arrow_keys_bind_enabled-0}" != 1 ]; then
echo "\ bindfunc __resh_widget_arrow_down_compat" 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 return 0
} }
__resh_unbind_control_R() { __resh_unbind_control_R() {
# TODO
echo "\ bindfunc __resh_widget_control_R_compat" echo "\ bindfunc __resh_widget_control_R_compat"
return 0 return 0
} }
@ -36,25 +70,42 @@ __resh_unbind_all() {
} }
reshctl() { 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 # run resh-control aka the real reshctl
resh-control "$@" resh-control "$@"
# modify current shell session based on exit status # modify current shell session based on exit status
local _status=$? local _status=$?
# echo $_status
# unexport current shell
unset __RESH_ctl_shell
case "$_status" in case "$_status" in
0|1) 0|1)
# success | fail # success | fail
return "$_status" return "$_status"
;; ;;
# enable # enable
100) # 100)
# enable all # # enable all
__resh_bind_all # __resh_bind_all
# return 0
# ;;
101)
# enable arrow keys
__resh_bind_arrows
return 0 return 0
;; ;;
# disable # disable
110) # 110)
# disable all # # disable all
__resh_unbind_all # __resh_unbind_all
# return 0
# ;;
111)
# disable arrow keys
__resh_unbind_arrows
return 0 return 0
;; ;;
200) 200)
@ -62,6 +113,12 @@ reshctl() {
. ~/.resh/shellrc . ~/.resh/shellrc
return 0 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 echo "reshctl() FATAL ERROR: unknown status" >&2
return "$_status" return "$_status"

@ -23,13 +23,13 @@ else
echo "resh PANIC unrecognized OS" echo "resh PANIC unrecognized OS"
fi fi
if [ -n "$ZSH_VERSION" ]; then if [ -n "${ZSH_VERSION-}" ]; then
# shellcheck disable=SC1009 # shellcheck disable=SC1009
__RESH_SHELL="zsh" __RESH_SHELL="zsh"
__RESH_HOST="$HOST" __RESH_HOST="$HOST"
__RESH_HOSTTYPE="$CPUTYPE" __RESH_HOSTTYPE="$CPUTYPE"
__resh_zsh_completion_init __resh_zsh_completion_init
elif [ -n "$BASH_VERSION" ]; then elif [ -n "${BASH_VERSION-}" ]; then
__RESH_SHELL="bash" __RESH_SHELL="bash"
__RESH_HOST="$HOSTNAME" __RESH_HOST="$HOSTNAME"
__RESH_HOSTTYPE="$HOSTTYPE" __RESH_HOSTTYPE="$HOSTTYPE"
@ -86,5 +86,14 @@ if [ -z "${__RESH_INIT_DONE+x}" ]; then
__resh_reset_variables __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 __RESH_INIT_DONE=1
fi fi

@ -1,15 +1,17 @@
#!/usr/bin/env bash #!/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 [ "${BASH_SOURCE[0]}" != "scripts/test.sh" ] && echo 'Run this script using `make test`' && exit 1
for f in scripts/*.sh; do for f in scripts/*.sh; do
echo "Running shellcheck on $f ..." echo "Running shellcheck on $f ..."
shellcheck $f --shell=bash --severity=error || exit 1 shellcheck "$f" --shell=bash --severity=error || exit 1
done done
for f in scripts/{shellrc,util,reshctl,hooks}.sh; do for f in scripts/{shellrc,util,reshctl,hooks}.sh; do
echo "Checking Zsh syntax of $f ..." 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 done
for sh in bash zsh; do for sh in bash zsh; do

@ -63,8 +63,19 @@ __resh_bash_completion_init() {
} }
__resh_zsh_completion_init() { __resh_zsh_completion_init() {
# shellcheck disable=SC2206 # NOTE: this is hacky - each completion needs to be added individually
fpath=(~/.resh/zsh_completion.d $fpath) # 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() { __resh_session_init() {

@ -37,8 +37,12 @@ __resh_widget_arrow_up() {
__RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX" __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX"
# increment histno # increment histno
__RESH_HISTNO=$((__RESH_HISTNO+1)) __RESH_HISTNO=$((__RESH_HISTNO+1))
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 # back at histno == 0 => restore original line
if [ "$__RESH_HISTNO" -eq 0 ]; then
BUFFER=$__RESH_HISTNO_ZERO_LINE BUFFER=$__RESH_HISTNO_ZERO_LINE
else else
# run recall # 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)" 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 # IF new buffer in non-empty THEN use the new buffer ELSE revert histno change
# shellcheck disable=SC2015 # 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 fi
# run post helper # run post helper
__resh_helper_arrow_post __resh_helper_arrow_post

@ -1 +1 @@
Subproject commit 9811ba8b7694cdbd9debed931e922b67e439197a Subproject commit f558191d74ae33654d63a0f091034ef27f4b44f9

@ -1 +1 @@
Subproject commit 7dde81eaa09cbed11ebc70ea892bcae24ea1606c Subproject commit c3077fcdf2e20efb95dd27a53766b78533ab7bc4
Loading…
Cancel
Save