diff --git a/VERSION b/VERSION index e25d8d9..0664a8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.5 +1.1.6 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 64ff6c8..55235a8 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -35,21 +35,28 @@ func main() { if _, err := toml.DecodeFile(configPath, &config); err != nil { log.Fatal("Error reading config:", err) } + // recall command recall := flag.Bool("recall", false, "Recall command on position --histno") recallHistno := flag.Int("histno", 0, "Recall command on position --histno") + recallPrefix := flag.String("prefix-search", "", "Recall command based on prefix --prefix-search") + // version showVersion := flag.Bool("version", false, "Show version and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit") requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") + // core cmdLine := flag.String("cmdLine", "", "command line") exitCode := flag.Int("exitCode", -1, "exit code") shell := flag.String("shell", "", "actual shell") uname := flag.String("uname", "", "uname") sessionID := flag.String("sessionId", "", "resh generated session id") + // recall metadata + recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command") + // posix variables cols := flag.String("cols", "-1", "$COLUMNS") lines := flag.String("lines", "-1", "$LINES") @@ -121,6 +128,11 @@ func main() { log.Println("Option '--recall' only works with '--histno' option - exiting!") os.Exit(4) } + if *recallPrefix != "" && *recall == false { + log.Println("Option '--prefix-search' only works with '--recall' option - exiting!") + os.Exit(4) + } + realtimeBefore, err := strconv.ParseFloat(*rtb, 64) if err != nil { log.Fatal("Flag Parsing error (rtb):", err) @@ -150,15 +162,15 @@ func main() { *gitRemote = "" } - if *osReleaseID == "" { - *osReleaseID = "linux" - } - if *osReleaseName == "" { - *osReleaseName = "Linux" - } - if *osReleasePrettyName == "" { - *osReleasePrettyName = "Linux" - } + // if *osReleaseID == "" { + // *osReleaseID = "linux" + // } + // if *osReleaseName == "" { + // *osReleaseName = "Linux" + // } + // if *osReleasePrettyName == "" { + // *osReleasePrettyName = "Linux" + // } rec := records.Record{ // posix @@ -219,6 +231,9 @@ func main() { ReshUUID: collect.ReadFileContent(reshUUIDPath), ReshVersion: Version, ReshRevision: Revision, + + RecallActions: []string{*recallActions}, + RecallPrefix: *recallPrefix, }, } if *recall { diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index d9ba682..7c5efeb 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -29,7 +29,7 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("Payload:", jsn) return } - cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno) + 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) diff --git a/pkg/records/records.go b/pkg/records/records.go index ce89a8b..d3668a5 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -86,6 +86,9 @@ type BaseRecord struct { RecallStrategy string `json:"recallStrategy,omitempty"` RecallActions []string `json:"recallActions,omitempty"` + // recall command + RecallPrefix string `json:"recallPrefix,omitempty"` + // added by sanitizatizer Sanitized bool `json:"sanitized,omitempty"` CmdLength int `json:"cmdLength,omitempty"` diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 7a36492..09f7e69 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -4,6 +4,7 @@ import ( "errors" "log" "strconv" + "strings" "sync" "github.com/curusarn/resh/pkg/records" @@ -94,13 +95,22 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro } session.mutex.Lock() defer session.mutex.Unlock() - session.recent = append(session.recent, record) - log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, "; session len:", len(session.recent)) + session.recentRecords = append(session.recentRecords, record) + // remove previous occurance of record + for i := len(session.recentCmdLines) - 1; i >= 0; i-- { + if session.recentCmdLines[i] == record.CmdLine { + session.recentCmdLines = append(session.recentCmdLines[:i], session.recentCmdLines[i+1:]...) + } + } + // append new record + session.recentCmdLines = append(session.recentCmdLines, record.CmdLine) + log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, + "; session len:", len(session.recentCmdLines), "; session len w/ dups:", len(session.recentRecords)) return nil } // Recall command from recent session history -func (s *Dispatch) Recall(sessionID string, histno int) (string, error) { +func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { s.mutex.RLock() session, found := s.sessions[sessionID] s.mutex.RUnlock() @@ -108,18 +118,25 @@ func (s *Dispatch) Recall(sessionID string, histno int) (string, error) { if found == false { return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID) } + if prefix == "" { + session.mutex.Lock() + defer session.mutex.Unlock() + return session.getRecordByHistno(histno) + } session.mutex.Lock() defer session.mutex.Unlock() - return session.getRecordByHistno(histno) + return session.searchRecordByPrefix(prefix, histno) } type sesshist struct { - recent []records.Record - mutex sync.Mutex + recentRecords []records.Record + recentCmdLines []string // deduplicated + // cmdLines map[string]int + mutex sync.Mutex } func (s *sesshist) getRecordByHistno(histno int) (string, error) { - // records get appended to the end of the slice + // addRecords() appends records to the end of the slice // -> this func handles the indexing if histno == 0 { return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") @@ -127,9 +144,35 @@ 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.recent) - histno + index := len(s.recentCmdLines) - histno + if index < 0 { + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + } + return s.recentCmdLines[index], nil +} + +func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, error) { + if histno == 0 { + return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") + } + if histno < 0 { + return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") + } + index := len(s.recentCmdLines) - histno if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recent)) + ")") + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + } + cmdLines := []string{} + for i := len(s.recentCmdLines) - 1; i >= 0; i-- { + if strings.HasPrefix(s.recentCmdLines[i], prefix) { + cmdLines = append(cmdLines, s.recentCmdLines[i]) + if len(cmdLines) >= histno { + break + } + } + } + if len(cmdLines) < histno { + return "", errors.New("sesshist ERROR: 'histno > number of commands matching with given prefix' (" + strconv.Itoa(len(cmdLines)) + ")") } - return s.recent[index].CmdLine, nil + return cmdLines[histno-1], nil } diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 322b5fc..be11652 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -1,11 +1,20 @@ -__resh_preexec() { +__resh_reset_variables() { __RESH_HISTNO=0 + __RESH_HISTNO_ZERO_LINE="" + __RESH_HIST_PREV_LINE="" + __RESH_HIST_RECALL_ACTIONS="" + __RESH_HIST_NO_PREFIX_MODE=0 +} + +__resh_preexec() { # core __RESH_COLLECT=1 __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check) - __resh_collect --cmdLine "$__RESH_CMDLINE" \ + __resh_collect --cmdLine "$__RESH_CMDLINE" --recall-actions "$__RESH_HIST_RECALL_ACTIONS" \ &>~/.resh/collect_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/collect_last_run_out.txt)" + + __resh_reset_variables } # used for collect and collect --recall @@ -90,6 +99,7 @@ __resh_collect() { -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ -osReleaseName "$__RESH_OS_RELEASE_NAME" \ -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ + -histno "$__RESH_HISTNO" \ "$@" fi } diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index 865e2fd..d5357fd 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -73,6 +73,7 @@ if [ -z "${__RESH_SESSION_ID+x}" ]; then export __RESH_SESSION_ID; __RESH_SESSION_ID=$(__resh_get_uuid) export __RESH_SESSION_PID="$$" # TODO add sesson time + __resh_reset_variables __resh_session_init fi diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 77a7306..9960da7 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -2,15 +2,71 @@ # shellcheck source=hooks.sh . ~/.resh/hooks.sh +__resh_helper_arrow_pre() { + # set prefix + __RESH_PREFIX=${BUFFER:0:CURSOR} + # cursor not at the end of the line => end "NO_PREFIX_MODE" + [ "$CURSOR" -ne "${#BUFFER}" ] && __RESH_HIST_NO_PREFIX_MODE=0 + # if user made any edits from last recall action => restart histno AND deactivate "NO_PREFIX_MODE" + [ "$BUFFER" != "$__RESH_HIST_PREV_LINE" ] && __RESH_HISTNO=0 && __RESH_HIST_NO_PREFIX_MODE=0 + # "NO_PREFIX_MODE" => set prefix to empty string + [ "$__RESH_HIST_NO_PREFIX_MODE" -eq 1 ] && __RESH_PREFIX="" + # append curent recall action + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX" + # histno == 0 => save current line + [ $__RESH_HISTNO -eq 0 ] && __RESH_HISTNO_ZERO_LINE=$BUFFER +} +__resh_helper_arrow_post() { + # cursor at the beginning of the line => activate "NO_PREFIX_MODE" + [ "$CURSOR" -eq 0 ] && __RESH_HIST_NO_PREFIX_MODE=1 + # "NO_PREFIX_MODE" => move cursor to the end of the line + [ "$__RESH_HIST_NO_PREFIX_MODE" -eq 1 ] && CURSOR=${#BUFFER} + # save current line so we can spot user edits next time + __RESH_HIST_PREV_LINE=$BUFFER +} + __resh_widget_arrow_up() { + # run helper function + __resh_helper_arrow_pre + # increment histno (( __RESH_HISTNO++ )) - BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_up_last_run_out.txt)" + # back at histno == 0 => restore original line + if [ $__RESH_HISTNO -eq 0 ]; then + BUFFER=$__RESH_HISTNO_ZERO_LINE + else + # run recall + local NEW_BUFFER + NEW_BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" --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-- )) + fi + # run post helper + __resh_helper_arrow_post } __resh_widget_arrow_down() { + # run helper function + __resh_helper_arrow_pre + # increment histno (( __RESH_HISTNO-- )) - BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_down_last_run_out.txt)" + # prevent HISTNO from getting negative (for now) + [ $__RESH_HISTNO -lt 0 ] && __RESH_HISTNO=0 + # back at histno == 0 => restore original line + if [ $__RESH_HISTNO -eq 0 ]; then + BUFFER=$__RESH_HISTNO_ZERO_LINE + else + # run recall + local NEW_BUFFER + NEW_BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_down_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++ )) + fi + __resh_helper_arrow_post } __resh_widget_control_R() { + local __RESH_LBUFFER=${BUFFER:0:CURSOR} + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_LBUFFER" # resh-collect --hstr hstr }